Compare commits
8 Commits
ed79cae77d
...
feat-gesti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fbda8dc5e | ||
|
|
665c15b3d7 | ||
|
|
d7bcbce50d | ||
|
|
0ae7fa316e | ||
|
|
95095016d2 | ||
|
|
afa5fab55d | ||
|
|
87c33f25cf | ||
|
|
f21a5fd4e6 |
@@ -5,7 +5,7 @@ export type AgentStatus = 'ACTIF' | 'INACTIF' | 'SUSPENDU';
|
|||||||
export interface Agent {
|
export interface Agent {
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
profile: string; // ex. AGENT, SUPERVISEUR, CAISSIER
|
profil: string; // ex. AGENT, SUPERVISEUR, CAISSIER
|
||||||
principalCode?: string; // Agent principal
|
principalCode?: string; // Agent principal
|
||||||
caisseProfile?: string;
|
caisseProfile?: string;
|
||||||
statut: AgentStatus;
|
statut: AgentStatus;
|
||||||
@@ -48,7 +48,7 @@ export interface Agent {
|
|||||||
autreTelephone?: string;
|
autreTelephone?: string;
|
||||||
|
|
||||||
// TPE assignés (actifs seulement)
|
// TPE assignés (actifs seulement)
|
||||||
tpes?: TpeDevice[];
|
terminauxIds?: number[] | number;
|
||||||
|
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
|
|||||||
41
src/app/core/interfaces/gain.ts
Normal file
41
src/app/core/interfaces/gain.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Course, CourseType } from "./course";
|
||||||
|
|
||||||
|
export type TypeFormule =
|
||||||
|
| 'UNITAIRE'
|
||||||
|
| 'CHAMP_X'
|
||||||
|
| 'CHAMP_TOTAL'
|
||||||
|
| 'FORMULE_COMPLETE';
|
||||||
|
|
||||||
|
|
||||||
|
export interface RapportDetail {
|
||||||
|
id: number;
|
||||||
|
libelle: string;
|
||||||
|
rapport: number;
|
||||||
|
nombreGagnants: number;
|
||||||
|
massePartageeRang: number;
|
||||||
|
gainsFormule: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Formule {
|
||||||
|
id: number;
|
||||||
|
gains: string;
|
||||||
|
typePari: CourseType;
|
||||||
|
typeFormule: TypeFormule;
|
||||||
|
masseInitiale: number;
|
||||||
|
masseApresPrelevements: number;
|
||||||
|
masseFinale: number;
|
||||||
|
totalPari: number;
|
||||||
|
totalGagnants: number;
|
||||||
|
rapportsDetails: RapportDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface ResultatCagnotte {
|
||||||
|
id: number;
|
||||||
|
course: Course;
|
||||||
|
montantCagnotte: number;
|
||||||
|
montantARembourser: number;
|
||||||
|
dateCalcul: string;
|
||||||
|
formules: Formule[];
|
||||||
|
}
|
||||||
12
src/app/core/interfaces/points-ventes.ts
Normal file
12
src/app/core/interfaces/points-ventes.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export type PointVenteStatut = 'ACTIF' | 'INACTIF';
|
||||||
|
|
||||||
|
export interface PointVente {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
nom: string;
|
||||||
|
adresse: string;
|
||||||
|
ville: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
statut: PointVenteStatut;
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ export interface Resultat {
|
|||||||
totalMises: number;
|
totalMises: number;
|
||||||
masseAPartager: number;
|
masseAPartager: number;
|
||||||
prelevementsLegaux: number;
|
prelevementsLegaux: number;
|
||||||
|
statut: ResultatStatut;
|
||||||
montantRembourse: number;
|
montantRembourse: number;
|
||||||
montantCagnotte: number;
|
montantCagnotte: number;
|
||||||
adeadHeat: boolean;
|
adeadHeat: boolean;
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import { Agent } from './agent';
|
|
||||||
|
|
||||||
export type TpeStatus =
|
export type TpeStatus =
|
||||||
| 'VALIDE'
|
| 'ACTIF'
|
||||||
| 'INVALIDE'
|
| 'HORS_SERVICE';
|
||||||
| 'EN_PANNE'
|
|
||||||
| 'BLOQUE'
|
|
||||||
| 'DISPONIBLE'
|
|
||||||
| 'AFFECTE'
|
|
||||||
| 'EN_MAINTENANCE'
|
|
||||||
| 'HORS_SERVICE'
|
|
||||||
| 'VOLE';
|
|
||||||
export type TpeType = 'POS' | 'OTHER';
|
export type TpeType = 'POS' | 'OTHER';
|
||||||
|
|
||||||
export interface TpeDevice {
|
export interface TpeDevice {
|
||||||
id: string;
|
id: string;
|
||||||
imei: string;
|
numeroSerie: string;
|
||||||
serial: string;
|
pointDeVenteId: string;
|
||||||
type: TpeType;
|
|
||||||
marque: string;
|
|
||||||
modele: string;
|
|
||||||
statut: TpeStatus;
|
statut: TpeStatus;
|
||||||
agent?: Agent;
|
versionLogicielle: string;
|
||||||
assigne: boolean;
|
typeTerminal: string;
|
||||||
createdAt?: string;
|
plateforme: string;
|
||||||
updatedAt?: string;
|
modeleAppareil: string;
|
||||||
|
systemeExploitation: string;
|
||||||
|
versionOs: string;
|
||||||
|
adresseIp: string;
|
||||||
|
adresseMac: string;
|
||||||
|
assigned: boolean;
|
||||||
|
agentConnecteId: string;
|
||||||
|
derniereConnexionAgent: string;
|
||||||
|
derniereDeconnexionAgent: string;
|
||||||
|
journalSession: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { TpeDevice, TpeStatus, TpeType } from '../interfaces/tpe';
|
|||||||
import { environment } from 'src/environments/environment.development';
|
import { environment } from 'src/environments/environment.development';
|
||||||
import { normalizePage } from '@shared/paging/normalize-page';
|
import { normalizePage } from '@shared/paging/normalize-page';
|
||||||
import { ListParams, PagedResult } from '@shared/paging/paging';
|
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||||
|
import { ServicesUtils } from './services-utils';
|
||||||
|
|
||||||
const USE_SERVER = true;
|
const USE_SERVER = true;
|
||||||
const API_BASE = '/api/v1/agents';
|
const API_BASE = '/api/agents';
|
||||||
|
|
||||||
// Interface to match the API response structure for TPE (nested in Agent)
|
// Interface to match the API response structure for TPE (nested in Agent)
|
||||||
// Note: When TPE is nested in Agent's tpes array, the agent field might be omitted or be a reference
|
// Note: When TPE is nested in Agent's tpes array, the agent field might be omitted or be a reference
|
||||||
@@ -31,7 +32,7 @@ interface TpeApiResponse {
|
|||||||
interface AgentApiResponse {
|
interface AgentApiResponse {
|
||||||
id: number;
|
id: number;
|
||||||
code: string;
|
code: string;
|
||||||
profile: string;
|
profil: string;
|
||||||
principalCode?: string;
|
principalCode?: string;
|
||||||
caisseProfile?: string;
|
caisseProfile?: string;
|
||||||
statut: string;
|
statut: string;
|
||||||
@@ -65,7 +66,7 @@ interface AgentApiResponse {
|
|||||||
statutMarital?: string;
|
statutMarital?: string;
|
||||||
epoux?: string;
|
epoux?: string;
|
||||||
autreTelephone?: string;
|
autreTelephone?: string;
|
||||||
tpes?: TpeApiResponse[];
|
terminauxIds?: number[] | number;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
@@ -75,7 +76,7 @@ interface AgentApiResponse {
|
|||||||
export class AgentService {
|
export class AgentService {
|
||||||
private apiUrl = environment.apiBaseUrl + API_BASE;
|
private apiUrl = environment.apiBaseUrl + API_BASE;
|
||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient, private serviceUtils:ServicesUtils) {}
|
||||||
|
|
||||||
// Helper method to get ngrok bypass headers
|
// Helper method to get ngrok bypass headers
|
||||||
private getNgrokHeaders(): Record<string, string> {
|
private getNgrokHeaders(): Record<string, string> {
|
||||||
@@ -87,51 +88,44 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transform API TPE response to TpeDevice
|
// Transform API TPE response to TpeDevice
|
||||||
private transformTpe(apiTpe: TpeApiResponse): TpeDevice {
|
// private transformTpe(apiTpe: TpeApiResponse): TpeDevice {
|
||||||
const transformStatut = (apiStatut: string): TpeStatus => {
|
// const transformStatut = (apiStatut: string): TpeStatus => {
|
||||||
const upperStatut = apiStatut.toUpperCase() as TpeStatus;
|
// const upperStatut = apiStatut.toUpperCase() as TpeStatus;
|
||||||
const validStatuses: TpeStatus[] = [
|
// const validStatuses: TpeStatus[] = [
|
||||||
'VALIDE',
|
// 'ACTIF',
|
||||||
'INVALIDE',
|
// 'HORS_SERVICE'
|
||||||
'EN_PANNE',
|
// ];
|
||||||
'BLOQUE',
|
// return validStatuses.includes(upperStatut) ? upperStatut : 'INVALIDE';
|
||||||
'DISPONIBLE',
|
// };
|
||||||
'AFFECTE',
|
|
||||||
'EN_MAINTENANCE',
|
|
||||||
'HORS_SERVICE',
|
|
||||||
'VOLE',
|
|
||||||
];
|
|
||||||
return validStatuses.includes(upperStatut) ? upperStatut : 'INVALIDE';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Transform agent if it's an object (not just a string reference)
|
// // Transform agent if it's an object (not just a string reference)
|
||||||
let transformedAgent: Agent | undefined = undefined;
|
// let transformedAgent: Agent | undefined = undefined;
|
||||||
if (apiTpe.agent && typeof apiTpe.agent === 'object' && apiTpe.agent.id) {
|
// if (apiTpe.agent && typeof apiTpe.agent === 'object' && apiTpe.agent.id) {
|
||||||
// If agent is a full object, transform it
|
// // If agent is a full object, transform it
|
||||||
transformedAgent = this.transformAgent(apiTpe.agent as any);
|
// transformedAgent = this.transformAgent(apiTpe.agent as any);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
id: String(apiTpe.id),
|
// id: String(apiTpe.id),
|
||||||
imei: apiTpe.imei,
|
// imei: apiTpe.imei,
|
||||||
serial: apiTpe.serial,
|
// serial: apiTpe.serial,
|
||||||
type: apiTpe.type as TpeType,
|
// type: apiTpe.type as TpeType,
|
||||||
marque: apiTpe.marque,
|
// marque: apiTpe.marque,
|
||||||
modele: apiTpe.modele,
|
// modele: apiTpe.modele,
|
||||||
statut: transformStatut(apiTpe.statut),
|
// statut: transformStatut(apiTpe.statut),
|
||||||
agent: transformedAgent,
|
// agent: transformedAgent,
|
||||||
assigne: apiTpe.assigne,
|
// assigne: apiTpe.assigne,
|
||||||
createdAt: apiTpe.createdAt,
|
// createdAt: apiTpe.createdAt,
|
||||||
updatedAt: apiTpe.updatedAt,
|
// updatedAt: apiTpe.updatedAt,
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Transform API response to Agent
|
// Transform API response to Agent
|
||||||
private transformAgent(apiAgent: AgentApiResponse): Agent {
|
private transformAgent(apiAgent: AgentApiResponse): Agent {
|
||||||
return {
|
return {
|
||||||
id: String(apiAgent.id),
|
id: String(apiAgent.id),
|
||||||
code: apiAgent.code,
|
code: apiAgent.code,
|
||||||
profile: apiAgent.profile,
|
profil: apiAgent.profil,
|
||||||
principalCode: apiAgent.principalCode,
|
principalCode: apiAgent.principalCode,
|
||||||
caisseProfile: apiAgent.caisseProfile,
|
caisseProfile: apiAgent.caisseProfile,
|
||||||
statut: apiAgent.statut as AgentStatus,
|
statut: apiAgent.statut as AgentStatus,
|
||||||
@@ -165,10 +159,7 @@ export class AgentService {
|
|||||||
statutMarital: apiAgent.statutMarital,
|
statutMarital: apiAgent.statutMarital,
|
||||||
epoux: apiAgent.epoux,
|
epoux: apiAgent.epoux,
|
||||||
autreTelephone: apiAgent.autreTelephone,
|
autreTelephone: apiAgent.autreTelephone,
|
||||||
tpes: apiAgent.tpes?.map((tpe) => {
|
terminauxIds: apiAgent.terminauxIds ,
|
||||||
const transformed = this.transformTpe(tpe);
|
|
||||||
return transformed;
|
|
||||||
}),
|
|
||||||
createdAt: apiAgent.createdAt,
|
createdAt: apiAgent.createdAt,
|
||||||
updatedAt: apiAgent.updatedAt,
|
updatedAt: apiAgent.updatedAt,
|
||||||
createdBy: apiAgent.createdBy,
|
createdBy: apiAgent.createdBy,
|
||||||
@@ -193,7 +184,7 @@ export class AgentService {
|
|||||||
private transformToApiPayload(agent: Partial<Agent>): any {
|
private transformToApiPayload(agent: Partial<Agent>): any {
|
||||||
const payload: any = {};
|
const payload: any = {};
|
||||||
if (agent.code !== undefined) payload.code = agent.code;
|
if (agent.code !== undefined) payload.code = agent.code;
|
||||||
if (agent.profile !== undefined) payload.profile = agent.profile;
|
if (agent.profil !== undefined) payload.profil = agent.profil;
|
||||||
if (agent.principalCode !== undefined) payload.principalCode = agent.principalCode;
|
if (agent.principalCode !== undefined) payload.principalCode = agent.principalCode;
|
||||||
if (agent.caisseProfile !== undefined) payload.caisseProfile = agent.caisseProfile;
|
if (agent.caisseProfile !== undefined) payload.caisseProfile = agent.caisseProfile;
|
||||||
if (agent.statut !== undefined) payload.statut = agent.statut;
|
if (agent.statut !== undefined) payload.statut = agent.statut;
|
||||||
@@ -233,25 +224,26 @@ export class AgentService {
|
|||||||
if (agent.epoux !== undefined) payload.epoux = agent.epoux;
|
if (agent.epoux !== undefined) payload.epoux = agent.epoux;
|
||||||
if (agent.autreTelephone !== undefined) payload.autreTelephone = agent.autreTelephone;
|
if (agent.autreTelephone !== undefined) payload.autreTelephone = agent.autreTelephone;
|
||||||
if (agent.createdBy !== undefined) payload.createdBy = agent.createdBy;
|
if (agent.createdBy !== undefined) payload.createdBy = agent.createdBy;
|
||||||
// Include tpes if provided - transform to API format
|
|
||||||
if (agent.tpes !== undefined) {
|
|
||||||
payload.tpes = agent.tpes.map((tpe) => ({
|
|
||||||
id: tpe.id ? Number(tpe.id) : undefined,
|
|
||||||
imei: tpe.imei,
|
|
||||||
serial: tpe.serial,
|
|
||||||
type: tpe.type,
|
|
||||||
marque: tpe.marque,
|
|
||||||
modele: tpe.modele,
|
|
||||||
statut: tpe.statut,
|
|
||||||
agent: undefined, // Will be set by backend
|
|
||||||
assigne: tpe.assigne,
|
|
||||||
createdAt: tpe.createdAt,
|
|
||||||
updatedAt: tpe.updatedAt,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
assigner(tpeIds: string[], agentId:string):Observable<Agent | undefined>{
|
||||||
|
const payload = {
|
||||||
|
terminalIds: [
|
||||||
|
...tpeIds
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return this.http.put<Agent>(`${this.apiUrl}/${agentId}/terminaux`,
|
||||||
|
payload, {headers: this.getNgrokHeaders()}).pipe(map(res=> res),
|
||||||
|
catchError((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
return of(undefined);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/v1/agents/{id} - Get by ID
|
// GET /api/v1/agents/{id} - Get by ID
|
||||||
getById(id: string): Observable<Agent | undefined> {
|
getById(id: string): Observable<Agent | undefined> {
|
||||||
if (USE_SERVER) {
|
if (USE_SERVER) {
|
||||||
@@ -269,55 +261,43 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/v1/agents - List all
|
// GET /api/v1/agents - List all
|
||||||
list(params?: ListParams): Observable<PagedResult<Agent>> {
|
list(params: ListParams): Observable<PagedResult<Agent>> {
|
||||||
if (USE_SERVER) {
|
// let httpParams = new HttpParams();
|
||||||
let httpParams = new HttpParams();
|
// if (params) {
|
||||||
if (params) {
|
// if (params.page) httpParams = httpParams.set('page', params.page.toString());
|
||||||
if (params.page) httpParams = httpParams.set('page', params.page.toString());
|
// if (params.size) httpParams = httpParams.set('perPage', params.size.toString());
|
||||||
if (params.size) httpParams = httpParams.set('perPage', params.size.toString());
|
// if (params.search) httpParams = httpParams.set('search', params.search);
|
||||||
if (params.search) httpParams = httpParams.set('search', params.search);
|
// if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey);
|
||||||
if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey);
|
// if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir);
|
||||||
if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir);
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
return this.http
|
return this.http
|
||||||
.get<AgentApiResponse[]>(this.apiUrl, {
|
.get<PagedResult<AgentApiResponse>>(this.apiUrl, {
|
||||||
params: httpParams,
|
params: this.serviceUtils.getParamsFromModel(params),
|
||||||
headers: this.getNgrokHeaders(),
|
headers: this.getNgrokHeaders(),
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((list) => {
|
map((res) => {
|
||||||
const agents = list.map((apiAgent) => {
|
const agents = res.content.map((apiAgent) => {
|
||||||
const transformed = this.transformAgent(apiAgent);
|
const transformed = this.transformAgent(apiAgent);
|
||||||
return transformed;
|
return transformed;
|
||||||
});
|
});
|
||||||
// If pagination params provided, return paginated result
|
// If pagination params provided, return paginated result
|
||||||
if (params) {
|
const resAgent = {
|
||||||
return normalizePage<Agent>(
|
...res,
|
||||||
{ data: agents, meta: { total: agents.length } },
|
content: agents
|
||||||
params.page || 1,
|
|
||||||
params.size || 10
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Otherwise return all as single page
|
// Otherwise return all as single page
|
||||||
return normalizePage<Agent>(
|
return resAgent;
|
||||||
{ data: agents, meta: { total: agents.length } },
|
|
||||||
1,
|
|
||||||
agents.length
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error('Error fetching agents:', err);
|
console.error('Error fetching agents:', err);
|
||||||
return of(normalizePage<Agent>({ data: [], meta: { total: 0 } }, 1, 10));
|
return of(normalizePage<Agent>({ content: [], meta: { total: 0 } }, 1, 10));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return of(normalizePage<Agent>({ data: [], meta: { total: 0 } }, 1, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/v1/agents - Create
|
// POST /api/agents - Create
|
||||||
create(payload: Omit<Agent, 'id' | 'createdAt' | 'updatedAt'>): Observable<Agent> {
|
create(payload: Omit<Agent, 'id' | 'createdAt' | 'updatedAt'>): Observable<Agent> {
|
||||||
if (USE_SERVER) {
|
|
||||||
const apiPayload = this.transformToApiPayload(payload);
|
const apiPayload = this.transformToApiPayload(payload);
|
||||||
return this.http
|
return this.http
|
||||||
.post<AgentApiResponse>(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() })
|
.post<AgentApiResponse>(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() })
|
||||||
@@ -329,8 +309,6 @@ export class AgentService {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error('Server mode is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT /api/v1/agents/{id} - Update
|
// PUT /api/v1/agents/{id} - Update
|
||||||
update(id: string, payload: Partial<Agent>): Observable<Agent | undefined> {
|
update(id: string, payload: Partial<Agent>): Observable<Agent | undefined> {
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ export interface CourseApiResponse {
|
|||||||
typesParisOuverts: Array<string>;
|
typesParisOuverts: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NonApiRequest {
|
||||||
|
nonPartants: String[]
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CourseService {
|
export class CourseService {
|
||||||
private apiUrl = environment.apiBaseUrl + API_BASE;
|
private apiUrl = environment.apiBaseUrl + API_BASE;
|
||||||
@@ -153,7 +157,6 @@ export class CourseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getById(id: string): Observable<Course | undefined> {
|
getById(id: string): Observable<Course | undefined> {
|
||||||
if (USE_SERVER) {
|
|
||||||
return this.http
|
return this.http
|
||||||
.get<CourseApiResponse>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() })
|
.get<CourseApiResponse>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() })
|
||||||
.pipe(
|
.pipe(
|
||||||
@@ -161,9 +164,6 @@ export class CourseService {
|
|||||||
// Fetch the reunion (non-partants are already included in the API response)
|
// Fetch the reunion (non-partants are already included in the API response)
|
||||||
return this.hippodromeService.getById(String(apiCourse.hippodromeId)).pipe(
|
return this.hippodromeService.getById(String(apiCourse.hippodromeId)).pipe(
|
||||||
map((hippodrome) => {
|
map((hippodrome) => {
|
||||||
if (!hippodrome) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
id: String(apiCourse.id),
|
id: String(apiCourse.id),
|
||||||
hippodrome: hippodrome ?? undefined,
|
hippodrome: hippodrome ?? undefined,
|
||||||
@@ -193,8 +193,6 @@ export class CourseService {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return of(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
// getByReunionId(reunionId: string): Observable<Course[]> {
|
// getByReunionId(reunionId: string): Observable<Course[]> {
|
||||||
// if (USE_SERVER) {
|
// if (USE_SERVER) {
|
||||||
@@ -454,34 +452,20 @@ export class CourseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addNonPartant(courseId: string, npList: string[]) {
|
addNonPartant(courseId: string, npList: string[]) {
|
||||||
|
const payload = {
|
||||||
|
nonPartants: [...npList]
|
||||||
|
}
|
||||||
console.warn('addNonPartant is deprecated. Use setNonPartants instead.');
|
console.warn('addNonPartant is deprecated. Use setNonPartants instead.');
|
||||||
return this.setNonPartants(courseId, npList);
|
return this.setNonPartants(courseId, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
setNonPartants(courseId: string, npList: string[]): Observable<Course | undefined> {
|
setNonPartants(courseId: string, npList: NonApiRequest): Observable<CourseApiResponse | undefined> {
|
||||||
if (USE_SERVER) {
|
|
||||||
// Use PUT endpoint to replace the entire list
|
|
||||||
return this.nonPartantService.replaceNonPartants(courseId, npList).pipe(
|
return this.nonPartantService.replaceNonPartants(courseId, npList).pipe(
|
||||||
switchMap((updatedNonPartants) => {
|
map((updatedNonPartants) => updatedNonPartants),
|
||||||
// Fetch the updated course to return it
|
|
||||||
return this.getById(courseId).pipe(
|
|
||||||
map((course) => {
|
|
||||||
if (course) {
|
|
||||||
return {
|
|
||||||
...course,
|
|
||||||
nonPartants: updatedNonPartants,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error(`Error setting nonPartants for course ${courseId}:`, err);
|
console.error(`Error setting nonPartants for course ${courseId}:`, err);
|
||||||
return of(undefined);
|
return of(undefined);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error('Server mode is required');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/app/core/services/depouillement.spec.ts
Normal file
40
src/app/core/services/depouillement.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
import { Depouillement, ResultatCourse } from './depouillement';
|
||||||
|
import { environment } from 'src/environments/environment.development';
|
||||||
|
|
||||||
|
describe('Depouillement', () => {
|
||||||
|
let service: Depouillement;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({ imports: [HttpClientTestingModule] });
|
||||||
|
service = TestBed.inject(Depouillement);
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => httpMock.verify());
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should POST resultat to depouillement endpoint', () => {
|
||||||
|
const payload: ResultatCourse = {
|
||||||
|
id: 12,
|
||||||
|
course: { id: '1', hippodrome: undefined, reunionNumero: 0, reunionDate: '', nom: 'C1', numero: 1, heureDepartPrevue: '', discipline: '', distanceMetres: 0, categorie: '', nombrePartants: 0, statut: '', annulee: false, reporteeMemeJour: false, reporteeAutreJour: false, incidentTechnique: false, nonPartants: [], typesParisOuverts: [] },
|
||||||
|
statut: 0 as any,
|
||||||
|
ordreArrivee: '1,2,3',
|
||||||
|
} as ResultatCourse;
|
||||||
|
|
||||||
|
service.sendResultat(payload).subscribe((res) => {
|
||||||
|
expect(res).toBeTruthy();
|
||||||
|
expect(res.id).toEqual(payload.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(environment.apiBaseUrl + '/api/depouillement');
|
||||||
|
expect(req.request.method).toBe('POST');
|
||||||
|
expect(req.request.body).toEqual(payload);
|
||||||
|
req.flush(payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
48
src/app/core/services/depouillement.ts
Normal file
48
src/app/core/services/depouillement.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { catchError, map } from 'rxjs/operators';
|
||||||
|
import { Course } from '../interfaces/course';
|
||||||
|
import { ResultatStatut } from '../interfaces/resultat';
|
||||||
|
import { environment } from 'src/environments/environment.development';
|
||||||
|
|
||||||
|
export interface ResultatCourse {
|
||||||
|
id: number;
|
||||||
|
course: Partial<Course>;
|
||||||
|
statut: ResultatStatut;
|
||||||
|
ordreArrivee: string;
|
||||||
|
datePublication?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_BASE = '/api/v1/depouillement';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class Depouillement {
|
||||||
|
private apiUrl = environment.depouillementBaseUrl + API_BASE;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
private getNgrokHeaders(): Record<string, string> {
|
||||||
|
const isNgrok =
|
||||||
|
environment.apiBaseUrl.includes('ngrok-free.app') ||
|
||||||
|
environment.apiBaseUrl.includes('ngrok.io') ||
|
||||||
|
environment.apiBaseUrl.includes('ngrok');
|
||||||
|
return isNgrok ? { 'ngrok-skip-browser-warning': 'true' } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a resultat to the dépouillement endpoint.
|
||||||
|
* The backend expects a payload shaped like ResultatCourse.
|
||||||
|
*/
|
||||||
|
sendResultat(resultat: Omit<ResultatCourse, "id">): Observable<{}> {
|
||||||
|
return this.http.post<ResultatCourse>(this.apiUrl, resultat, { headers: this.getNgrokHeaders() }).pipe(
|
||||||
|
map((res) => res as ResultatCourse),
|
||||||
|
catchError((err) => {
|
||||||
|
console.error('Error sending resultat to depouillement:', err);
|
||||||
|
return of(resultat); // return original payload on error to allow UI to proceed gracefully
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/app/core/services/gain.spec.ts
Normal file
16
src/app/core/services/gain.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Gain } from './gain';
|
||||||
|
|
||||||
|
describe('Gain', () => {
|
||||||
|
let service: Gain;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(Gain);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
107
src/app/core/services/gain.ts
Normal file
107
src/app/core/services/gain.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { map, catchError, switchMap } from 'rxjs/operators';
|
||||||
|
import { ResultatCagnotte, Formule } from '../interfaces/gain';
|
||||||
|
import { normalizePage } from '@shared/paging/normalize-page';
|
||||||
|
import { PaginatedHttpService } from '@shared/paging/paginated-http.service';
|
||||||
|
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||||
|
import { environment } from 'src/environments/environment.development';
|
||||||
|
import { ServicesUtils } from './services-utils';
|
||||||
|
|
||||||
|
const USE_SERVER = true;
|
||||||
|
const API_BASE = '/api/v1/gains';
|
||||||
|
|
||||||
|
export interface ResultatCagnotteApi extends ResultatCagnotte {}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class Gain {
|
||||||
|
private apiUrl = environment.depouillementBaseUrl + API_BASE;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient, private pager: PaginatedHttpService, private servicesUtil: ServicesUtils) {}
|
||||||
|
|
||||||
|
private getNgrokHeaders(): Record<string, string> {
|
||||||
|
const isNgrok =
|
||||||
|
environment.apiBaseUrl.includes('ngrok-free.app') ||
|
||||||
|
environment.apiBaseUrl.includes('ngrok.io') ||
|
||||||
|
environment.apiBaseUrl.includes('ngrok');
|
||||||
|
return isNgrok ? { 'ngrok-skip-browser-warning': 'true' } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// List (paginated) ResultatCagnotte
|
||||||
|
list(params: ListParams): Observable<PagedResult<ResultatCagnotte>> {
|
||||||
|
if (USE_SERVER) {
|
||||||
|
const url = this.apiUrl;
|
||||||
|
return this.pager.fetch<ResultatCagnotteApi>(url, params).pipe(
|
||||||
|
map((res) => {
|
||||||
|
const content = (res.content ?? []).map((api) => api as ResultatCagnotte);
|
||||||
|
return {
|
||||||
|
pageable: res.pageable,
|
||||||
|
totalPages: res.totalPages,
|
||||||
|
totalElements: res.totalElements,
|
||||||
|
content,
|
||||||
|
} as PagedResult<ResultatCagnotte>;
|
||||||
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
console.error('Error fetching gains list:', err);
|
||||||
|
return of({ content: [], pageable: { pageNumber: 1, pageSize: 0, total: 0 }, totalPages: 1, totalElements: 0 } as PagedResult<ResultatCagnotte>);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return of({ content: [], pageable: { pageNumber: 1, pageSize: 0, total: 0 }, totalPages: 1, totalElements: 0 } as PagedResult<ResultatCagnotte>);
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(id: string): Observable<ResultatCagnotte | undefined> {
|
||||||
|
if (USE_SERVER) {
|
||||||
|
return this.http.get<ResultatCagnotteApi>(`${this.apiUrl}/rapport/${id}`, { headers: this.getNgrokHeaders() }).pipe(
|
||||||
|
map((api) => api as ResultatCagnotte),
|
||||||
|
catchError((err) => {
|
||||||
|
console.error(`Error fetching gain ${id}:`, err);
|
||||||
|
return of(undefined);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(payload: Partial<ResultatCagnotteApi>): Observable<ResultatCagnotte> {
|
||||||
|
if (USE_SERVER) {
|
||||||
|
return this.http.post<ResultatCagnotteApi>(this.apiUrl, payload, { headers: this.getNgrokHeaders() }).pipe(
|
||||||
|
map((api) => api as ResultatCagnotte),
|
||||||
|
catchError((err) => {
|
||||||
|
console.error('Error creating gain:', err);
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new Error('Server mode required');
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, payload: Partial<ResultatCagnotteApi>): Observable<ResultatCagnotte | undefined> {
|
||||||
|
if (USE_SERVER) {
|
||||||
|
return this.http.put<ResultatCagnotteApi>(`${this.apiUrl}/${id}`, payload, { headers: this.getNgrokHeaders() }).pipe(
|
||||||
|
map((api) => api as ResultatCagnotte),
|
||||||
|
catchError((err) => {
|
||||||
|
console.error(`Error updating gain ${id}:`, err);
|
||||||
|
return of(undefined);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id: string): Observable<void> {
|
||||||
|
if (USE_SERVER) {
|
||||||
|
return this.http.delete<void>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }).pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
console.error(`Error deleting gain ${id}:`, err);
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return of(void 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { map, catchError } from 'rxjs/operators';
|
import { map, catchError } from 'rxjs/operators';
|
||||||
import { environment } from 'src/environments/environment.development';
|
import { environment } from 'src/environments/environment.development';
|
||||||
|
import { CourseApiResponse, NonApiRequest } from './course';
|
||||||
|
|
||||||
const USE_SERVER = true;
|
const USE_SERVER = true;
|
||||||
|
|
||||||
@@ -20,21 +21,22 @@ export class NonPartantService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PUT /api/v1/courses/{courseId}/non-partants - Replace the list of non-partants for a course
|
// PUT /api/v1/courses/{courseId}/non-partants - Replace the list of non-partants for a course
|
||||||
replaceNonPartants(courseId: string, nonPartants: string[]): Observable<string[]> {
|
replaceNonPartants(courseId: string, nonPartants: NonApiRequest): Observable<CourseApiResponse | undefined> {
|
||||||
if (USE_SERVER) {
|
const courseApiUrl = environment.apiBaseUrl + '/api/courses';
|
||||||
const courseApiUrl = environment.apiBaseUrl + '/api/v1/courses';
|
|
||||||
return this.http
|
return this.http
|
||||||
.put<string[]>(`${courseApiUrl}/${courseId}/non-partants`, nonPartants, {
|
.patch<CourseApiResponse>(`${courseApiUrl}/${courseId}/non-partants`, nonPartants, {
|
||||||
headers: this.getNgrokHeaders(),
|
headers: this.getNgrokHeaders(),
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((list) => list.map((np) => String(np))),
|
map((res) => {
|
||||||
|
|
||||||
|
return res
|
||||||
|
}),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
|
console.log(err);
|
||||||
console.error(`Error replacing non-partants for course ${courseId}:`, err);
|
console.error(`Error replacing non-partants for course ${courseId}:`, err);
|
||||||
return of([]);
|
return of(undefined);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/app/core/services/points-vente.spec.ts
Normal file
16
src/app/core/services/points-vente.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PointsVente } from './points-vente';
|
||||||
|
|
||||||
|
describe('PointsVente', () => {
|
||||||
|
let service: PointsVente;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(PointsVente);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
92
src/app/core/services/points-vente.ts
Normal file
92
src/app/core/services/points-vente.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { map, catchError } from 'rxjs/operators';
|
||||||
|
import { PointVente, PointVenteStatut } from 'src/app/core/interfaces/points-ventes';
|
||||||
|
import { environment } from 'src/environments/environment.development';
|
||||||
|
import { ServicesUtils } from './services-utils';
|
||||||
|
|
||||||
|
const API_BASE = '/api/points-de-vente';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class PointsVenteService {
|
||||||
|
private apiUrl = environment.apiBaseUrl + API_BASE;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient, private servicesUtils: ServicesUtils) {}
|
||||||
|
|
||||||
|
private getNgrokHeaders(): Record<string, string> {
|
||||||
|
const isNgrok =
|
||||||
|
environment.apiBaseUrl.includes('ngrok-free.app') ||
|
||||||
|
environment.apiBaseUrl.includes('ngrok.io') ||
|
||||||
|
environment.apiBaseUrl.includes('ngrok');
|
||||||
|
return isNgrok ? { 'ngrok-skip-browser-warning': 'true' } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
list(params: ListParams): Observable<PagedResult<PointVente>> {
|
||||||
|
return this.http
|
||||||
|
.get<PagedResult<PointVente>>(this.apiUrl, {
|
||||||
|
params: this.servicesUtils.getParamsFromModel(params),
|
||||||
|
headers: this.getNgrokHeaders(),
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
console.error('Error fetching points de vente:', err);
|
||||||
|
return of({
|
||||||
|
content: [],
|
||||||
|
pageable: { pageNumber: 0, pageSize: params.size || 10, total: 0 },
|
||||||
|
totalPages: 0,
|
||||||
|
totalElements: 0,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(id: string): Observable<PointVente | undefined> {
|
||||||
|
return this.http
|
||||||
|
.get<PointVente>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() })
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
console.error(`Error fetching point de vente ${id}:`, err);
|
||||||
|
return of(undefined);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(pointVente: Omit<PointVente, 'id'>): Observable<PointVente> {
|
||||||
|
return this.http
|
||||||
|
.post<PointVente>(this.apiUrl, pointVente, { headers: this.getNgrokHeaders() })
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
console.log(err)
|
||||||
|
console.error('Error creating point de vente:', err);
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, payload: Partial<PointVente>): Observable<PointVente | undefined> {
|
||||||
|
return this.http
|
||||||
|
.put<PointVente>(`${this.apiUrl}/${id}`, payload, { headers: this.getNgrokHeaders() })
|
||||||
|
.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
console.error(`Error updating point de vente ${id}:`, err);
|
||||||
|
return of(undefined);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id: string): Observable<boolean> {
|
||||||
|
return this.http
|
||||||
|
.delete<void>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() })
|
||||||
|
.pipe(
|
||||||
|
map(() => true),
|
||||||
|
catchError((err) => {
|
||||||
|
console.error(`Error deleting point de vente ${id}:`, err);
|
||||||
|
return of(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -137,28 +137,43 @@ export class ResultatService {
|
|||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((raw) => {
|
switchMap((raw) => {
|
||||||
// Some courses don't have a resultat yet.
|
// Debug raw response shape to help detect API changes
|
||||||
// In that case the API returns 200 with a body like:
|
console.debug(`ResultatService.getByCourseId(${courseId}) raw:`, raw);
|
||||||
// { "message": "Aucun résultat disponible pour cette course" }
|
// Handle common variants of server responses:
|
||||||
// We interpret this as "no resultat" and return undefined.
|
// - { message: '...' } -> no resultat
|
||||||
if (
|
// - { id: ..., ordreArrivee: '...' } -> single resultat
|
||||||
raw &&
|
// - [ { ... } ] -> array of resultats (pick first)
|
||||||
typeof raw === 'object' &&
|
// - { content: [...] } -> take first content
|
||||||
'message' in raw &&
|
|
||||||
!('id' in raw) &&
|
if (!raw) return of<Resultat | undefined>(undefined);
|
||||||
!('ordreArrivee' in raw)
|
|
||||||
) {
|
// message-only response -> no resultat
|
||||||
|
if (typeof raw === 'object' && 'message' in raw && !('id' in raw) && !('ordreArrivee' in raw)) {
|
||||||
return of<Resultat | undefined>(undefined);
|
return of<Resultat | undefined>(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiResultat = raw as ResultatApiResponse;
|
let apiResultat: ResultatApiResponse | undefined;
|
||||||
|
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
apiResultat = raw.length > 0 ? (raw[0] as ResultatApiResponse) : undefined;
|
||||||
|
} else if (raw && typeof raw === 'object') {
|
||||||
|
if ('id' in raw || 'ordreArrivee' in raw) {
|
||||||
|
apiResultat = raw as ResultatApiResponse;
|
||||||
|
} else if ('content' in raw && Array.isArray(raw.content) && raw.content.length > 0) {
|
||||||
|
apiResultat = raw.content[0] as ResultatApiResponse;
|
||||||
|
} else if ('data' in raw && raw.data && typeof raw.data === 'object') {
|
||||||
|
apiResultat = raw.data as ResultatApiResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiResultat) return of<Resultat | undefined>(undefined);
|
||||||
|
|
||||||
return this.courseService.getById(courseId).pipe(
|
return this.courseService.getById(courseId).pipe(
|
||||||
map((course) => {
|
map((course) => {
|
||||||
if (!course) {
|
if (!course) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return this.transformApiResponse(apiResultat, course);
|
return this.transformApiResponse(apiResultat!, course);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -264,6 +279,7 @@ export class ResultatService {
|
|||||||
course,
|
course,
|
||||||
// API now returns 'ordreArrivee' as CSV/string; normalize to number[]
|
// API now returns 'ordreArrivee' as CSV/string; normalize to number[]
|
||||||
ordreArrivee: apiResultat.ordreArrivee,
|
ordreArrivee: apiResultat.ordreArrivee,
|
||||||
|
statut: apiResultat.statut,
|
||||||
// dead-heat not provided by new API shape — default to empty
|
// dead-heat not provided by new API shape — default to empty
|
||||||
chevauxDeadHeat: [],
|
chevauxDeadHeat: [],
|
||||||
// Financial fields may not be present in new API; default to 0
|
// Financial fields may not be present in new API; default to 0
|
||||||
|
|||||||
@@ -9,64 +9,72 @@ import { normalizePage } from '@shared/paging/normalize-page';
|
|||||||
import { ListParams, PagedResult } from '@shared/paging/paging';
|
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||||
|
|
||||||
const USE_SERVER = true;
|
const USE_SERVER = true;
|
||||||
const API_BASE = '/api/v1/tpes';
|
const API_BASE = '/api/terminaux';
|
||||||
|
|
||||||
// Interface to match the API response structure for Agent (nested in TPE)
|
// Interface to match the API response structure for Agent (nested in TPE)
|
||||||
interface AgentApiResponse {
|
interface AgentApiResponse {
|
||||||
id: number;
|
id: number,
|
||||||
code: string;
|
code: String,
|
||||||
profile: string;
|
profil: String,
|
||||||
principalCode?: string;
|
principalCode: String,
|
||||||
caisseProfile?: string;
|
caisseProfile: String,
|
||||||
statut: string;
|
statut: AgentStatus,
|
||||||
zone?: string;
|
zone: String,
|
||||||
kiosk?: string;
|
kiosk: String,
|
||||||
fonction?: string;
|
fonction: String,
|
||||||
dateEmbauche?: string;
|
dateEmbauche: String,
|
||||||
nom: string;
|
nom: String,
|
||||||
prenom: string;
|
"prenom": String,
|
||||||
autresNoms?: string;
|
"autresNoms": String,
|
||||||
dateNaissance?: string;
|
"dateNaissance": String,
|
||||||
lieuNaissance?: string;
|
"lieuNaissance": String,
|
||||||
ville?: string;
|
"ville": String,
|
||||||
adresse?: string;
|
"adresse": String,
|
||||||
autoriserAides?: boolean;
|
"autoriserAides": boolean,
|
||||||
phone: string;
|
"phone": String,
|
||||||
pin?: string;
|
"limiteInferieure": number,
|
||||||
limiteInferieure?: number;
|
"limiteSuperieure": number,
|
||||||
limiteSuperieure?: number;
|
"limiteParTransaction": number,
|
||||||
limiteParTransaction?: number;
|
"limiteMinAirtime": number,
|
||||||
limiteMinAirtime?: number;
|
"limiteMaxAirtime": number,
|
||||||
limiteMaxAirtime?: number;
|
"maxPeripheriques": number,
|
||||||
maxPeripheriques?: number;
|
"limitId": String,
|
||||||
limitId?: number;
|
"nationalite": String,
|
||||||
nationalite?: string;
|
"cni": String,
|
||||||
cni?: string;
|
"cniDelivreeLe": String,
|
||||||
cniDelivreeLe?: string;
|
"cniDelivreeA": String,
|
||||||
cniDelivreeA?: string;
|
"residence": String,
|
||||||
residence?: string;
|
"autreAdresse1": String,
|
||||||
autreAdresse1?: string;
|
"statutMarital": String,
|
||||||
statutMarital?: string;
|
"epoux": String,
|
||||||
epoux?: string;
|
"autreTelephone": String,
|
||||||
autreTelephone?: string;
|
"createdAt": String,
|
||||||
createdAt?: string;
|
"updatedAt": String,
|
||||||
updatedAt?: string;
|
"createdBy": String,
|
||||||
createdBy?: string;
|
"terminauxIds": [
|
||||||
|
number[]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface to match the API response structure
|
// Interface to match the API response structure
|
||||||
interface TpeApiResponse {
|
interface TpeApiResponse {
|
||||||
id: number;
|
id: number,
|
||||||
imei: string;
|
numeroSerie: String,
|
||||||
serial: string;
|
pointDeVenteId: number,
|
||||||
type: string;
|
statut: TpeStatus,
|
||||||
marque: string;
|
derniereConnexion: String,
|
||||||
modele: string;
|
versionLogicielle: String,
|
||||||
statut: string; // API uses uppercase: VALIDE, INVALIDE, EN_PANNE, BLOQUE
|
typeTerminal: String,
|
||||||
agent?: AgentApiResponse;
|
plateforme: String,
|
||||||
assigne: boolean;
|
modeleAppareil: String,
|
||||||
createdAt?: string;
|
systemeExploitation: String,
|
||||||
updatedAt?: string;
|
versionOs: String,
|
||||||
|
adresseIp: String,
|
||||||
|
adresseMac: String,
|
||||||
|
agentConnecteId: number,
|
||||||
|
derniereConnexionAgent: String,
|
||||||
|
derniereDeconnexionAgent: String,
|
||||||
|
journalSession: String
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats interfaces
|
// Stats interfaces
|
||||||
@@ -96,17 +104,10 @@ export class TpeService {
|
|||||||
private transformStatut(apiStatut: string): TpeStatus {
|
private transformStatut(apiStatut: string): TpeStatus {
|
||||||
const upperStatut = apiStatut.toUpperCase() as TpeStatus;
|
const upperStatut = apiStatut.toUpperCase() as TpeStatus;
|
||||||
const validStatuses: TpeStatus[] = [
|
const validStatuses: TpeStatus[] = [
|
||||||
'VALIDE',
|
'ACTIF',
|
||||||
'INVALIDE',
|
'HORS_SERVICE'
|
||||||
'EN_PANNE',
|
|
||||||
'BLOQUE',
|
|
||||||
'DISPONIBLE',
|
|
||||||
'AFFECTE',
|
|
||||||
'EN_MAINTENANCE',
|
|
||||||
'HORS_SERVICE',
|
|
||||||
'VOLE',
|
|
||||||
];
|
];
|
||||||
return validStatuses.includes(upperStatut) ? upperStatut : 'INVALIDE';
|
return validStatuses.includes(upperStatut) ? upperStatut : 'HORS_SERVICE';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform interface statut to API statut (both use uppercase now, so direct return)
|
// Transform interface statut to API statut (both use uppercase now, so direct return)
|
||||||
@@ -114,29 +115,29 @@ export class TpeService {
|
|||||||
return statut; // Already uppercase, no transformation needed
|
return statut; // Already uppercase, no transformation needed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform API Agent response to Agent
|
// Transform API Agent response to Agent (lightweight mapping)
|
||||||
private transformAgent(apiAgent: AgentApiResponse): Agent {
|
private transformAgent(apiAgent: AgentApiResponse): Agent {
|
||||||
return {
|
return {
|
||||||
id: String(apiAgent.id),
|
id: String(apiAgent.id),
|
||||||
code: apiAgent.code,
|
code: String((apiAgent as any).code || ''),
|
||||||
profile: apiAgent.profile,
|
profil: String((apiAgent as any).profil || ''),
|
||||||
principalCode: apiAgent.principalCode,
|
principalCode: (apiAgent as any).principalCode ? String((apiAgent as any).principalCode) : undefined,
|
||||||
caisseProfile: apiAgent.caisseProfile,
|
caisseProfile: (apiAgent as any).caisseProfile ? String((apiAgent as any).caisseProfile) : undefined,
|
||||||
statut: apiAgent.statut as AgentStatus,
|
statut: apiAgent.statut as AgentStatus,
|
||||||
zone: apiAgent.zone,
|
zone: (apiAgent as any).zone ? String((apiAgent as any).zone) : undefined,
|
||||||
kiosk: apiAgent.kiosk,
|
kiosk: (apiAgent as any).kiosk ? String((apiAgent as any).kiosk) : undefined,
|
||||||
fonction: apiAgent.fonction,
|
fonction: (apiAgent as any).fonction ? String((apiAgent as any).fonction) : undefined,
|
||||||
dateEmbauche: apiAgent.dateEmbauche,
|
dateEmbauche: apiAgent.dateEmbauche ? String(apiAgent.dateEmbauche) : undefined,
|
||||||
nom: apiAgent.nom,
|
nom: String(apiAgent.nom || ''),
|
||||||
prenom: apiAgent.prenom,
|
prenom: String(apiAgent.prenom || ''),
|
||||||
autresNoms: apiAgent.autresNoms,
|
autresNoms: apiAgent.autresNoms ? String(apiAgent.autresNoms) : undefined,
|
||||||
dateNaissance: apiAgent.dateNaissance,
|
dateNaissance: apiAgent.dateNaissance ? String(apiAgent.dateNaissance) : undefined,
|
||||||
lieuNaissance: apiAgent.lieuNaissance,
|
lieuNaissance: apiAgent.lieuNaissance ? String(apiAgent.lieuNaissance) : undefined,
|
||||||
ville: apiAgent.ville,
|
ville: apiAgent.ville ? String(apiAgent.ville) : undefined,
|
||||||
adresse: apiAgent.adresse,
|
adresse: apiAgent.adresse ? String(apiAgent.adresse) : undefined,
|
||||||
autoriserAides: apiAgent.autoriserAides,
|
autoriserAides: Boolean(apiAgent.autoriserAides),
|
||||||
phone: apiAgent.phone,
|
phone: String(apiAgent.phone || ''),
|
||||||
pin: apiAgent.pin,
|
pin: (apiAgent as any).pin ? String((apiAgent as any).pin) : undefined,
|
||||||
limiteInferieure: apiAgent.limiteInferieure,
|
limiteInferieure: apiAgent.limiteInferieure,
|
||||||
limiteSuperieure: apiAgent.limiteSuperieure,
|
limiteSuperieure: apiAgent.limiteSuperieure,
|
||||||
limiteParTransaction: apiAgent.limiteParTransaction,
|
limiteParTransaction: apiAgent.limiteParTransaction,
|
||||||
@@ -144,58 +145,71 @@ export class TpeService {
|
|||||||
limiteMaxAirtime: apiAgent.limiteMaxAirtime,
|
limiteMaxAirtime: apiAgent.limiteMaxAirtime,
|
||||||
maxPeripheriques: apiAgent.maxPeripheriques,
|
maxPeripheriques: apiAgent.maxPeripheriques,
|
||||||
limitId: apiAgent.limitId ? String(apiAgent.limitId) : undefined,
|
limitId: apiAgent.limitId ? String(apiAgent.limitId) : undefined,
|
||||||
nationalite: apiAgent.nationalite,
|
nationalite: apiAgent.nationalite ? String(apiAgent.nationalite) : undefined,
|
||||||
cni: apiAgent.cni,
|
cni: apiAgent.cni ? String(apiAgent.cni) : undefined,
|
||||||
cniDelivreeLe: apiAgent.cniDelivreeLe,
|
cniDelivreeLe: apiAgent.cniDelivreeLe ? String(apiAgent.cniDelivreeLe) : undefined,
|
||||||
cniDelivreeA: apiAgent.cniDelivreeA,
|
cniDelivreeA: apiAgent.cniDelivreeA ? String(apiAgent.cniDelivreeA) : undefined,
|
||||||
residence: apiAgent.residence,
|
residence: apiAgent.residence ? String(apiAgent.residence) : undefined,
|
||||||
autreAdresse1: apiAgent.autreAdresse1,
|
autreAdresse1: apiAgent.autreAdresse1 ? String(apiAgent.autreAdresse1) : undefined,
|
||||||
statutMarital: apiAgent.statutMarital,
|
statutMarital: apiAgent.statutMarital ? String(apiAgent.statutMarital) : undefined,
|
||||||
epoux: apiAgent.epoux,
|
epoux: apiAgent.epoux ? String(apiAgent.epoux) : undefined,
|
||||||
autreTelephone: apiAgent.autreTelephone,
|
autreTelephone: apiAgent.autreTelephone ? String(apiAgent.autreTelephone) : undefined,
|
||||||
createdAt: apiAgent.createdAt,
|
createdAt: apiAgent.createdAt ? String(apiAgent.createdAt) : undefined,
|
||||||
updatedAt: apiAgent.updatedAt,
|
updatedAt: apiAgent.updatedAt ? String(apiAgent.updatedAt) : undefined,
|
||||||
createdBy: apiAgent.createdBy,
|
createdBy: apiAgent.createdBy ? String(apiAgent.createdBy) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform API response to TpeDevice
|
// Transform API response to TpeDevice
|
||||||
private transformTpe(apiTpe: TpeApiResponse): TpeDevice {
|
// private transformTpe(apiTpe: TpeApiResponse): TpeDevice {
|
||||||
return {
|
// // Map API-specific names to our generic interface where possible
|
||||||
id: String(apiTpe.id),
|
// const serial = (apiTpe as any).numeroSerie || (apiTpe as any).serial || '';
|
||||||
imei: apiTpe.imei,
|
// const imei = (apiTpe as any).imei || serial || '';
|
||||||
serial: apiTpe.serial,
|
// const typeRaw = String((apiTpe as any).typeTerminal || '').toUpperCase();
|
||||||
type: apiTpe.type as TpeType,
|
// const type = typeRaw.includes('POS') ? ('POS' as TpeType) : ('OTHER' as TpeType);
|
||||||
marque: apiTpe.marque,
|
// const marque = (apiTpe as any).plateforme || (apiTpe as any).marque || '';
|
||||||
modele: apiTpe.modele,
|
// const modele = (apiTpe as any).modeleAppareil || (apiTpe as any).modele || '';
|
||||||
statut: this.transformStatut(apiTpe.statut),
|
// const statut = this.transformStatut(String(apiTpe.statut || 'INVALIDE'));
|
||||||
agent: apiTpe.agent ? this.transformAgent(apiTpe.agent) : undefined,
|
// // Agent mapping: sometimes API returns an agent object or only an id
|
||||||
assigne: apiTpe.assigne,
|
// let agent: Agent | undefined = undefined;
|
||||||
createdAt: apiTpe.createdAt,
|
// if ((apiTpe as any).agent && typeof (apiTpe as any).agent === 'object' && (apiTpe as any).agent.id) {
|
||||||
updatedAt: apiTpe.updatedAt,
|
// agent = this.transformAgent((apiTpe as any).agent as AgentApiResponse);
|
||||||
};
|
// }
|
||||||
}
|
// const assigne = Boolean((apiTpe as any).agentConnecteId || (apiTpe as any).assigne);
|
||||||
|
|
||||||
// Transform TpeDevice to API payload
|
// return {
|
||||||
private transformToApiPayload(tpe: Partial<TpeDevice>): any {
|
// id: String(apiTpe.id),
|
||||||
const payload: any = {};
|
// imei: String(imei),
|
||||||
if (tpe.imei !== undefined) payload.imei = tpe.imei;
|
// serial: String(serial),
|
||||||
if (tpe.serial !== undefined) payload.serial = tpe.serial;
|
// type,
|
||||||
if (tpe.type !== undefined) payload.type = tpe.type;
|
// marque: String(marque),
|
||||||
if (tpe.marque !== undefined) payload.marque = tpe.marque;
|
// modele: String(modele),
|
||||||
if (tpe.modele !== undefined) payload.modele = tpe.modele;
|
// statut,
|
||||||
if (tpe.statut !== undefined) payload.statut = this.transformStatutToApi(tpe.statut);
|
// agent,
|
||||||
if (tpe.assigne !== undefined) payload.assigne = tpe.assigne;
|
// assigne,
|
||||||
return payload;
|
// createdAt: (apiTpe as any).createdAt,
|
||||||
}
|
// updatedAt: (apiTpe as any).updatedAt,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Transform TpeDevice to API payload (best-effort)
|
||||||
|
// private transformToApiPayload(tpe: Partial<TpeDevice>): any {
|
||||||
|
// const payload: any = {};
|
||||||
|
// if (tpe.imei !== undefined) payload.numeroSerie = tpe.imei;
|
||||||
|
// if (tpe.serial !== undefined) payload.numeroSerie = tpe.serial;
|
||||||
|
// if (tpe.type !== undefined) payload.typeTerminal = tpe.type;
|
||||||
|
// if (tpe.marque !== undefined) payload.plateforme = tpe.marque;
|
||||||
|
// if (tpe.modele !== undefined) payload.modeleAppareil = tpe.modele;
|
||||||
|
// if (tpe.statut !== undefined) payload.statut = this.transformStatutToApi(tpe.statut);
|
||||||
|
// if (tpe.assigne !== undefined) payload.assigne = tpe.assigne;
|
||||||
|
// return payload;
|
||||||
|
// }
|
||||||
|
|
||||||
// GET /api/v1/tpes/{id} - Get by ID
|
// GET /api/v1/tpes/{id} - Get by ID
|
||||||
getById(id: string): Observable<TpeDevice | undefined> {
|
getById(id: string): Observable<TpeDevice | undefined> {
|
||||||
if (USE_SERVER) {
|
if (USE_SERVER) {
|
||||||
return this.http
|
return this.http.get<TpeDevice>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }).pipe(
|
||||||
.get<TpeApiResponse>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() })
|
map((api) => api),
|
||||||
.pipe(
|
|
||||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error(`Error fetching TPE ${id}:`, err);
|
console.error(`Error fetching TPE ${id}:`, err);
|
||||||
return of(undefined);
|
return of(undefined);
|
||||||
@@ -205,9 +219,9 @@ export class TpeService {
|
|||||||
return of(undefined);
|
return of(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// GET /api/v1/tpes - List all
|
// GET /api/v1/tpes - List all
|
||||||
list(params?: ListParams): Observable<PagedResult<TpeDevice>> {
|
list(params?: ListParams): Observable<PagedResult<TpeDevice>> {
|
||||||
if (USE_SERVER) {
|
|
||||||
let httpParams = new HttpParams();
|
let httpParams = new HttpParams();
|
||||||
if (params) {
|
if (params) {
|
||||||
if (params.page) httpParams = httpParams.set('page', params.page.toString());
|
if (params.page) httpParams = httpParams.set('page', params.page.toString());
|
||||||
@@ -218,72 +232,49 @@ export class TpeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.http
|
return this.http
|
||||||
.get<TpeApiResponse[]>(this.apiUrl, {
|
.get<PagedResult<TpeDevice>>(this.apiUrl, {
|
||||||
params: httpParams,
|
params: httpParams,
|
||||||
headers: this.getNgrokHeaders(),
|
headers: this.getNgrokHeaders(),
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((list) => {
|
map((list) => {
|
||||||
const tpes = list.map((apiTpe) => this.transformTpe(apiTpe));
|
const content = (list.content || []).map((api) => api);
|
||||||
// If pagination params provided, return paginated result
|
return { ...list, content } as PagedResult<TpeDevice>;
|
||||||
if (params) {
|
|
||||||
return normalizePage<TpeDevice>(
|
|
||||||
{ data: tpes, meta: { total: tpes.length } },
|
|
||||||
params.page || 1,
|
|
||||||
params.size || 10
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Otherwise return all as single page
|
|
||||||
return normalizePage<TpeDevice>(
|
|
||||||
{ data: tpes, meta: { total: tpes.length } },
|
|
||||||
1,
|
|
||||||
tpes.length
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error('Error fetching TPEs:', err);
|
console.error('Error fetching TPEs:', err);
|
||||||
return of(normalizePage<TpeDevice>({ data: [], meta: { total: 0 } }, 1, 10));
|
return of(normalizePage<TpeDevice>({ content: [], meta: { total: 0 } }, 1, 10));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return of(normalizePage<TpeDevice>({ data: [], meta: { total: 0 } }, 1, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/v1/tpes - Create
|
// POST /api/v1/tpes - Create
|
||||||
create(payload: Omit<TpeDevice, 'id' | 'createdAt' | 'updatedAt'>): Observable<TpeDevice> {
|
create(payload: Partial<TpeDevice>): Observable<TpeDevice> {
|
||||||
if (USE_SERVER) {
|
|
||||||
const apiPayload = this.transformToApiPayload(payload);
|
|
||||||
return this.http
|
return this.http
|
||||||
.post<TpeApiResponse>(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() })
|
.post<TpeDevice>(this.apiUrl, payload, { headers: this.getNgrokHeaders() })
|
||||||
.pipe(
|
.pipe(
|
||||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
map((apiTpe) => apiTpe),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error('Error creating TPE:', err);
|
console.error('Error creating TPE:', err);
|
||||||
throw err;
|
throw err;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new Error('Server mode is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT /api/v1/tpes/{id} - Update
|
// PUT /api/v1/tpes/{id} - Update
|
||||||
update(id: string, payload: Partial<TpeDevice>): Observable<TpeDevice | undefined> {
|
update(id: string, payload: Partial<TpeDevice>): Observable<TpeDevice | undefined> {
|
||||||
if (USE_SERVER) {
|
|
||||||
const apiPayload = this.transformToApiPayload(payload);
|
|
||||||
return this.http
|
return this.http
|
||||||
.put<TpeApiResponse>(`${this.apiUrl}/${id}`, apiPayload, {
|
.put<TpeDevice>(`${this.apiUrl}/${id}`, payload, {
|
||||||
headers: this.getNgrokHeaders(),
|
headers: this.getNgrokHeaders(),
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
map((apiTpe) => apiTpe),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error(`Error updating TPE ${id}:`, err);
|
console.error(`Error updating TPE ${id}:`, err);
|
||||||
return of(undefined);
|
return of(undefined);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return of(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/v1/tpes/{id} - Delete
|
// DELETE /api/v1/tpes/{id} - Delete
|
||||||
delete(id: string): Observable<boolean> {
|
delete(id: string): Observable<boolean> {
|
||||||
@@ -305,13 +296,13 @@ export class TpeService {
|
|||||||
updateStatut(id: string, statut: TpeStatus): Observable<TpeDevice | undefined> {
|
updateStatut(id: string, statut: TpeStatus): Observable<TpeDevice | undefined> {
|
||||||
if (USE_SERVER) {
|
if (USE_SERVER) {
|
||||||
return this.http
|
return this.http
|
||||||
.patch<TpeApiResponse>(
|
.patch<TpeDevice>(
|
||||||
`${this.apiUrl}/${id}/statut`,
|
`${this.apiUrl}/${id}/statut`,
|
||||||
{ statut: this.transformStatutToApi(statut) },
|
{ statut: this.transformStatutToApi(statut) },
|
||||||
{ headers: this.getNgrokHeaders() }
|
{ headers: this.getNgrokHeaders() }
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
map((apiTpe) => apiTpe),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error(`Error updating TPE statut ${id}:`, err);
|
console.error(`Error updating TPE statut ${id}:`, err);
|
||||||
return of(undefined);
|
return of(undefined);
|
||||||
@@ -323,7 +314,6 @@ export class TpeService {
|
|||||||
|
|
||||||
// PATCH /api/v1/tpes/{id}/liberer - Liberate TPE (updates whole TPE, sets assigne to false and statut to DISPONIBLE)
|
// PATCH /api/v1/tpes/{id}/liberer - Liberate TPE (updates whole TPE, sets assigne to false and statut to DISPONIBLE)
|
||||||
liberer(id: string): Observable<TpeDevice | undefined> {
|
liberer(id: string): Observable<TpeDevice | undefined> {
|
||||||
if (USE_SERVER) {
|
|
||||||
// First get the current TPE data
|
// First get the current TPE data
|
||||||
return this.getById(id).pipe(
|
return this.getById(id).pipe(
|
||||||
switchMap((tpe) => {
|
switchMap((tpe) => {
|
||||||
@@ -331,14 +321,16 @@ export class TpeService {
|
|||||||
return of(undefined);
|
return of(undefined);
|
||||||
}
|
}
|
||||||
// Update the whole TPE with assigne set to false and statut to DISPONIBLE
|
// Update the whole TPE with assigne set to false and statut to DISPONIBLE
|
||||||
const updatedTpe = { ...tpe, assigne: false, statut: 'DISPONIBLE' as TpeStatus };
|
const updatedTpe = {
|
||||||
const apiPayload = this.transformToApiPayload(updatedTpe);
|
...tpe,
|
||||||
|
statut: 'ACTIF'
|
||||||
|
};
|
||||||
return this.http
|
return this.http
|
||||||
.patch<TpeApiResponse>(`${this.apiUrl}/liberer/${id}`, apiPayload, {
|
.patch<TpeDevice>(`${this.apiUrl}/liberer/${id}`, updatedTpe, {
|
||||||
headers: this.getNgrokHeaders(),
|
headers: this.getNgrokHeaders(),
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
map((apiTpe) => apiTpe),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error(`Error liberating TPE ${id}:`, err);
|
console.error(`Error liberating TPE ${id}:`, err);
|
||||||
return of(undefined);
|
return of(undefined);
|
||||||
@@ -350,51 +342,44 @@ export class TpeService {
|
|||||||
return of(undefined);
|
return of(undefined);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return of(undefined);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH /api/v1/tpes/assigner - Assign TPE
|
// PATCH /api/v1/tpes/assigner - Assign TPE
|
||||||
// Payload: { tpeId: number, agentId: number }
|
// Payload: { tpeId: number, agentId: number }
|
||||||
assigner(id: string, agentId: string): Observable<TpeDevice | undefined> {
|
assigner(id: string, agentId: string): Observable<TpeDevice | undefined> {
|
||||||
if (USE_SERVER) {
|
|
||||||
const payload = {
|
const payload = {
|
||||||
tpeId: Number(id),
|
tpeId: Number(id),
|
||||||
agentId: Number(agentId),
|
agentId: Number(agentId),
|
||||||
};
|
};
|
||||||
return this.http
|
return this.http
|
||||||
.patch<TpeApiResponse>(`${this.apiUrl}/assigner`, payload, {
|
.patch<TpeDevice>(`${this.apiUrl}/assigner`, payload, {
|
||||||
headers: this.getNgrokHeaders(),
|
headers: this.getNgrokHeaders(),
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
map((apiTpe) => apiTpe),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error(`Error assigning TPE ${id}:`, err);
|
console.error(`Error assigning TPE ${id}:`, err);
|
||||||
return of(undefined);
|
return of(undefined);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return of(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/v1/tpes/statut/{statut} - List by statut
|
// GET /api/v1/tpes/statut/{statut} - List by statut
|
||||||
getByStatut(statut: TpeStatus): Observable<TpeDevice[]> {
|
getByStatut(statut: TpeStatus): Observable<TpeDevice[]> {
|
||||||
if (USE_SERVER) {
|
|
||||||
const apiStatut = this.transformStatutToApi(statut);
|
const apiStatut = this.transformStatutToApi(statut);
|
||||||
return this.http
|
return this.http
|
||||||
.get<TpeApiResponse[]>(`${this.apiUrl}/statut/${apiStatut}`, {
|
.get<TpeDevice[]>(`${this.apiUrl}/statut/${apiStatut}`, {
|
||||||
headers: this.getNgrokHeaders(),
|
headers: this.getNgrokHeaders(),
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((list) => list.map((apiTpe) => this.transformTpe(apiTpe))),
|
map((list) => list.map((apiTpe) => apiTpe)),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error(`Error fetching TPEs by statut ${statut}:`, err);
|
console.error(`Error fetching TPEs by statut ${statut}:`, err);
|
||||||
return of([]);
|
return of([]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/v1/tpes/stats/count-by-statut - Get count by statut
|
// GET /api/v1/tpes/stats/count-by-statut - Get count by statut
|
||||||
getCountByStatut(): Observable<CountByStatutResponse> {
|
getCountByStatut(): Observable<CountByStatutResponse> {
|
||||||
@@ -432,36 +417,30 @@ export class TpeService {
|
|||||||
|
|
||||||
// GET /api/v1/tpes/search - Search
|
// GET /api/v1/tpes/search - Search
|
||||||
search(query: string): Observable<TpeDevice[]> {
|
search(query: string): Observable<TpeDevice[]> {
|
||||||
if (USE_SERVER) {
|
|
||||||
return this.http
|
return this.http
|
||||||
.get<TpeApiResponse[]>(`${this.apiUrl}/search`, {
|
.get<TpeDevice[]>(`${this.apiUrl}/search`, {
|
||||||
params: { q: query.trim() },
|
params: { q: query.trim() },
|
||||||
headers: this.getNgrokHeaders(),
|
headers: this.getNgrokHeaders(),
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((list) => list.map((apiTpe) => this.transformTpe(apiTpe))),
|
map((list) => list.map((apiTpe) => apiTpe)),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error(`Error searching TPEs with query ${query}:`, err);
|
console.error(`Error searching TPEs with query ${query}:`, err);
|
||||||
return of([]);
|
return of([]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/v1/tpes/disponibles - List available TPEs
|
// GET /api/v1/tpes/disponibles - List available TPEs
|
||||||
getDisponibles(): Observable<TpeDevice[]> {
|
getDisponibles(): Observable<TpeDevice[]> {
|
||||||
if (USE_SERVER) {
|
|
||||||
return this.http
|
return this.http
|
||||||
.get<TpeApiResponse[]>(`${this.apiUrl}/disponibles`, { headers: this.getNgrokHeaders() })
|
.get<TpeDevice[]>(`${this.apiUrl}/disponibles`, { headers: this.getNgrokHeaders() })
|
||||||
.pipe(
|
.pipe(
|
||||||
map((list) => list.map((apiTpe) => this.transformTpe(apiTpe))),
|
map((list) => list.map((apiTpe) => apiTpe)),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
console.error('Error fetching available TPEs:', err);
|
console.error('Error fetching available TPEs:', err);
|
||||||
return of([]);
|
return of([]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,17 @@ const routes: Routes = [
|
|||||||
loadComponent: () => import('./pages/reunion/reunion').then((m) => m.ReunionList),
|
loadComponent: () => import('./pages/reunion/reunion').then((m) => m.ReunionList),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'rapport',
|
path: 'resultat',
|
||||||
loadComponent: () => import('./pages/rapport/rapport').then((m) => m.Rapport),
|
loadComponent: () => import('./pages/rapport/rapport').then((m) => m.Rapport),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'gains',
|
||||||
|
loadComponent: () => import('./pages/gains/gains').then((m) => m.Gains),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'gains/:id',
|
||||||
|
loadComponent: () => import('./pages/gain-details/gain-details').then((m) => m.GainDetails),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'users',
|
path: 'users',
|
||||||
loadComponent: () => import('./pages/users/users').then((m) => m.UsersPage),
|
loadComponent: () => import('./pages/users/users').then((m) => m.UsersPage),
|
||||||
@@ -49,6 +57,10 @@ const routes: Routes = [
|
|||||||
path: 'limits',
|
path: 'limits',
|
||||||
loadComponent: () => import('./pages/limits/limits').then((m) => m.LimitsPage),
|
loadComponent: () => import('./pages/limits/limits').then((m) => m.LimitsPage),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'points-vente',
|
||||||
|
loadComponent: () => import('./pages/point-vente/point-vente').then((m) => m.PointVentePage),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ export class Layout {
|
|||||||
mainMenuItems: MenuItem[] = [
|
mainMenuItems: MenuItem[] = [
|
||||||
{ icon: '🏠', label: 'Tableau de bord', link: '/', exact: true },
|
{ icon: '🏠', label: 'Tableau de bord', link: '/', exact: true },
|
||||||
{ icon: '🏟️', label: 'Hippodromes', link: '/hippodromes' },
|
{ icon: '🏟️', label: 'Hippodromes', link: '/hippodromes' },
|
||||||
{ icon: '📅', label: 'Reunions', link: '/reunions' },
|
|
||||||
{ icon: '🏇', label: 'Courses', link: '/courses' },
|
{ icon: '🏇', label: 'Courses', link: '/courses' },
|
||||||
{ icon: 'icon-chart-bar', label: 'Rapport des courses', link: '/rapport' },
|
{ icon: 'icon-chart-bar', label: 'Résultats des courses', link: '/resultat' },
|
||||||
|
{ icon: '💰', label: 'Gains (cagnotte)', link: '/gains' },
|
||||||
];
|
];
|
||||||
|
|
||||||
workspaceMenuItems: MenuItem[] = [
|
workspaceMenuItems: MenuItem[] = [
|
||||||
@@ -59,6 +59,7 @@ export class Layout {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ icon: 'icon-monitor', label: 'Gestion des TPE', link: '/tpes' },
|
{ icon: 'icon-monitor', label: 'Gestion des TPE', link: '/tpes' },
|
||||||
|
{ icon: 'icon-map-pin', label: 'Points de vente', link: '/points-vente' },
|
||||||
{
|
{
|
||||||
icon: 'icon-users',
|
icon: 'icon-users',
|
||||||
label: 'Utilisateurs',
|
label: 'Utilisateurs',
|
||||||
|
|||||||
@@ -29,21 +29,17 @@
|
|||||||
[total]="total()"
|
[total]="total()"
|
||||||
[page]="page()"
|
[page]="page()"
|
||||||
[perPage]="size()"
|
[perPage]="size()"
|
||||||
(pageChange)="page.set($event)"
|
(pageChange)="page.set($event - 1)"
|
||||||
(perPageChange)="size.set($event)"
|
(perPageChange)="size.set($event)"
|
||||||
></app-paginator>
|
></app-paginator>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xxl">
|
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xxl">
|
||||||
<app-agent-full-form
|
<app-agent-form
|
||||||
[value]="editingItem() ?? undefined"
|
[value]="editingItem() ?? undefined"
|
||||||
(save)="onFormSave($event)"
|
(save)="onFormSave($event)"
|
||||||
(cancel)="closeModal()"
|
(cancel)="closeModal()"
|
||||||
/>
|
/>
|
||||||
<div modal-actions class="flex justify-end gap-2">
|
|
||||||
<z-button zType="destructive" (click)="closeModal()">Annuler</z-button>
|
|
||||||
<z-button (click)="submitChildForm()">Enregistrer</z-button>
|
|
||||||
</div>
|
|
||||||
</app-modal>
|
</app-modal>
|
||||||
|
|
||||||
<!-- Detail Modal -->
|
<!-- Detail Modal -->
|
||||||
@@ -61,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground mb-1">Profil</div>
|
<div class="text-xs text-muted-foreground mb-1">Profil</div>
|
||||||
<div class="font-medium">{{ agent.profile }}</div>
|
<div class="font-medium">{{ agent.profil }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground mb-1">Statut</div>
|
<div class="text-xs text-muted-foreground mb-1">Statut</div>
|
||||||
@@ -311,42 +307,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- TPE Assignés -->
|
<!-- TPE Assignés -->
|
||||||
@if (getAgentTpes(agent.id).length > 0) {
|
@if (tpeArray()) {
|
||||||
<z-card class="p-4">
|
@for (tpe of tpeDevices(); track tpe.id) {
|
||||||
<div class="text-lg font-semibold mb-4">TPE Assignés ({{ getAgentTpes(agent.id).length }})</div>
|
<ng-container
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
*ngTemplateOutlet="tpeCard; context: { $implicit: tpe }"
|
||||||
@for (tpe of getAgentTpes(agent.id); track tpe.id) {
|
/>
|
||||||
<div class="px-3 py-2.5 rounded bg-primary/10 border border-primary/20">
|
|
||||||
<div class="flex items-start justify-between mb-2">
|
|
||||||
<div class="font-medium text-sm">{{ tpe.imei }}</div>
|
|
||||||
@if (tpe.statut) {
|
|
||||||
<span class="text-xs px-2 py-0.5 rounded bg-surface text-muted-foreground">
|
|
||||||
{{ formatTpeStatut(tpe.statut) }}
|
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
<div class="space-y-1 text-xs text-muted-foreground">
|
|
||||||
@if (tpe.marque || tpe.modele) {
|
|
||||||
<div>
|
|
||||||
<span class="font-medium">Modèle:</span> {{ tpe.marque }} {{ tpe.modele }}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
@if (tpe.serial) {
|
@else {
|
||||||
<div>
|
<ng-container
|
||||||
<span class="font-medium">Série:</span> {{ tpe.serial }}
|
*ngTemplateOutlet="tpeCard; context: { $implicit: tpeDevice() }"
|
||||||
</div>
|
/>
|
||||||
}
|
|
||||||
@if (tpe.type) {
|
|
||||||
<div>
|
|
||||||
<span class="font-medium">Type:</span> {{ tpe.type }}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</z-card>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div modal-actions class="flex justify-end gap-2">
|
<div modal-actions class="flex justify-end gap-2">
|
||||||
@@ -361,20 +334,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- TPE Assignment Modal -->
|
<!-- TPE Assignment Modal -->
|
||||||
@if (assigningAgent()) {
|
@if (assigningAgent() !== undefined) {
|
||||||
<app-modal
|
<app-modal
|
||||||
[open]="assignTpeModalOpen()"
|
[open]="assignTpeModalOpen()"
|
||||||
[title]="'Assigner un TPE à ' + (assigningAgent()?.nom || '') + ' ' + (assigningAgent()?.prenom || '')"
|
[title]="'Assigner un TPE à ' + (assigningAgent()?.nom || '') + ' ' + (assigningAgent()?.prenom || '')"
|
||||||
(close)="closeAssignTpeModal()"
|
(close)="closeAssignTpeModal()"
|
||||||
size="md"
|
size="xxl"
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@if (tpesLoading()) {
|
@if (tpesLoading()) {
|
||||||
<div class="text-center py-4">Chargement des TPE disponibles...</div>
|
<div class="text-center py-4">Chargement des TPE disponibles...</div>
|
||||||
} @else if (availableTpes().length === 0) {
|
|
||||||
<div class="text-center py-4 text-muted-foreground">Aucun TPE disponible</div>
|
|
||||||
} @else {
|
} @else {
|
||||||
<z-form-field>
|
<app-tpe-select (selectionChange)="selectedTpeId.set($event)" [agent]="assigningAgent()" [selected]="getSelectedTpeIds()" ></app-tpe-select>
|
||||||
|
|
||||||
|
<!-- <z-form-field>
|
||||||
<label z-form-label>Sélectionner un TPE</label>
|
<label z-form-label>Sélectionner un TPE</label>
|
||||||
<div z-form-control>
|
<div z-form-control>
|
||||||
<z-select
|
<z-select
|
||||||
@@ -384,22 +357,51 @@
|
|||||||
>
|
>
|
||||||
@for (tpe of availableTpes(); track tpe.id) {
|
@for (tpe of availableTpes(); track tpe.id) {
|
||||||
<z-select-item [zValue]="tpe.id">
|
<z-select-item [zValue]="tpe.id">
|
||||||
{{ tpe.imei }} - {{ tpe.marque }} {{ tpe.modele }}
|
{{ tpe.numeroSerie }} - {{ tpe.modeleAppareil }}
|
||||||
@if (tpe.statut === 'VALIDE') {
|
@if (tpe.statut === 'ACTIF') {
|
||||||
<span class="text-xs text-green-600 dark:text-green-400 ml-2">(Valide)</span>
|
<span class="text-xs text-green-600 dark:text-green-400 ml-2">(Actif)</span>
|
||||||
}
|
}
|
||||||
</z-select-item>
|
</z-select-item>
|
||||||
}
|
}
|
||||||
</z-select>
|
</z-select>
|
||||||
</div>
|
</div>
|
||||||
</z-form-field>
|
</z-form-field> -->
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div modal-actions class="flex justify-end gap-2">
|
<div modal-actions class="flex justify-end gap-2">
|
||||||
<z-button zType="destructive" (click)="closeAssignTpeModal()">Annuler</z-button>
|
<z-button zType="destructive" (click)="closeAssignTpeModal()">Annuler</z-button>
|
||||||
<button z-button [disabled]="!selectedTpeId() || tpesLoading()" (click)="confirmAssignTpe()">
|
<button z-button [disabled]="tpesLoading()" (click)="confirmAssignTpe()">
|
||||||
Assigner
|
Assigner
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</app-modal>
|
</app-modal>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template #tpeCard let-tpe>
|
||||||
|
<div class="px-3 py-2.5 rounded bg-primary/10 border border-primary/20">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<div class="font-medium text-sm">
|
||||||
|
{{ tpe.numeroSerie }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (tpe.statut) {
|
||||||
|
<span class="text-xs px-2 py-0.5 rounded bg-surface text-muted-foreground">
|
||||||
|
{{ formatTpeStatut(tpe.statut) }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1 text-xs text-muted-foreground">
|
||||||
|
@if (tpe.modeleAppareil) {
|
||||||
|
<div><span class="font-medium">Modèle:</span> {{ tpe.modeleAppareil }}</div>
|
||||||
|
}
|
||||||
|
@if (tpe.numeroSerie) {
|
||||||
|
<div><span class="font-medium">Série:</span> {{ tpe.numeroSerie }}</div>
|
||||||
|
}
|
||||||
|
@if (tpe.typeTerminal) {
|
||||||
|
<div><span class="font-medium">Type:</span> {{ tpe.typeTerminal }}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import { TpeDevice, TpeStatus } from 'src/app/core/interfaces/tpe';
|
|||||||
import { AgentFullForm } from '@shared/forms/agent-full-form/agent-full-form';
|
import { AgentFullForm } from '@shared/forms/agent-full-form/agent-full-form';
|
||||||
import { forkJoin, of } from 'rxjs';
|
import { forkJoin, of } from 'rxjs';
|
||||||
import { switchMap, catchError } from 'rxjs/operators';
|
import { switchMap, catchError } from 'rxjs/operators';
|
||||||
|
import { toast } from 'ngx-sonner';
|
||||||
|
import { AgentForm } from '@shared/forms/agent-form/agent-form';
|
||||||
|
import { TpeSelect } from "../tpe-select/tpe-select";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -39,10 +42,9 @@ import { switchMap, catchError } from 'rxjs/operators';
|
|||||||
Modal,
|
Modal,
|
||||||
ZardButtonComponent,
|
ZardButtonComponent,
|
||||||
ZardCardComponent,
|
ZardCardComponent,
|
||||||
ZardSelectComponent,
|
|
||||||
ZardSelectItemComponent,
|
|
||||||
ZardFormModule,
|
ZardFormModule,
|
||||||
AgentFullForm,
|
AgentForm,
|
||||||
|
TpeSelect
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AgentsPage {
|
export class AgentsPage {
|
||||||
@@ -50,7 +52,7 @@ export class AgentsPage {
|
|||||||
total = signal(0);
|
total = signal(0);
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
|
|
||||||
page = signal(1);
|
page = signal(0);
|
||||||
size = signal(10);
|
size = signal(10);
|
||||||
search = signal('');
|
search = signal('');
|
||||||
sort = signal<SortState>({ key: 'code', dir: 'asc' });
|
sort = signal<SortState>({ key: 'code', dir: 'asc' });
|
||||||
@@ -65,11 +67,17 @@ export class AgentsPage {
|
|||||||
|
|
||||||
// TPE Assignment modal
|
// TPE Assignment modal
|
||||||
assignTpeModalOpen = signal(false);
|
assignTpeModalOpen = signal(false);
|
||||||
assigningAgent = signal<Agent | null>(null);
|
assigningAgent = signal<Agent | undefined>(undefined);
|
||||||
availableTpes = signal<TpeDevice[]>([]);
|
availableTpes = signal<TpeDevice[]>([]);
|
||||||
selectedTpeId = signal<string>('');
|
selectedTpeId = signal<string[]>([]);
|
||||||
tpesLoading = signal(false);
|
tpesLoading = signal(false);
|
||||||
|
|
||||||
|
tpeDevice = signal<TpeDevice | undefined>(undefined);
|
||||||
|
tpeDevices = signal<TpeDevice[]>([]);
|
||||||
|
|
||||||
|
tpeArray = signal<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
@ViewChild(AgentFullForm) formComp?: AgentFullForm;
|
@ViewChild(AgentFullForm) formComp?: AgentFullForm;
|
||||||
|
|
||||||
formatTpeStatut(statut: TpeStatus): string {
|
formatTpeStatut(statut: TpeStatus): string {
|
||||||
@@ -88,71 +96,24 @@ export class AgentsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cols: TableColumn<Agent>[] = [
|
cols: TableColumn<Agent>[] = [
|
||||||
{ key: 'code', label: 'Code', sortable: true },
|
{ key: 'code', label: 'Code', sortable: true, defaultVisible: true },
|
||||||
{ key: 'nom', label: 'Nom', sortable: true },
|
{ key: 'nomPrenom', label: 'Nom complet', sortable: true, defaultVisible: true, cell: (a) => `${a.nom} ${a.prenom}` },
|
||||||
{ key: 'prenom', label: 'Prénom', sortable: true },
|
{ key: 'profil', label: 'Profil', sortable: true, defaultVisible: true },
|
||||||
{ key: 'phone', label: 'Téléphone', sortable: true },
|
{ key: 'statut', label: 'Statut', sortable: true, defaultVisible: true, cell: (a) => this.renderStatutBadge(a.statut) },
|
||||||
{
|
{ key: 'phone', label: 'Téléphone', sortable: true, defaultVisible: true },
|
||||||
key: 'tpes',
|
{ key: 'zone', label: 'Zone', sortable: true, defaultVisible: true },
|
||||||
label: 'TPE assignés',
|
|
||||||
cell: (a) => {
|
|
||||||
const tpes = this.agentTpesMap.get(a.id) || [];
|
|
||||||
if (tpes.length === 0) {
|
|
||||||
return '<span class="text-muted-foreground text-sm">Aucun</span>';
|
|
||||||
}
|
|
||||||
// Show up to 2 TPEs with full details, then count for the rest
|
|
||||||
const displayCount = Math.min(2, tpes.length);
|
|
||||||
const displayed = tpes.slice(0, displayCount);
|
|
||||||
const remaining = tpes.length - displayCount;
|
|
||||||
|
|
||||||
const tpeCards = displayed
|
|
||||||
.map((t) => {
|
|
||||||
const imei = `<div class="font-medium text-xs">${t.imei}</div>`;
|
|
||||||
const details = [
|
|
||||||
t.marque && t.modele ? `${t.marque} ${t.modele}` : t.marque || t.modele || '',
|
|
||||||
t.statut ? this.formatTpeStatut(t.statut) : '',
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' • ');
|
|
||||||
const detailsHtml = details
|
|
||||||
? `<div class="text-xs text-muted-foreground">${details}</div>`
|
|
||||||
: '';
|
|
||||||
return `<div class="px-2 py-1.5 rounded bg-primary/10 border border-primary/20 flex flex-col gap-0.5">${imei}${detailsHtml}</div>`;
|
|
||||||
})
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
const moreHtml =
|
|
||||||
remaining > 0
|
|
||||||
? `<div class="text-xs text-muted-foreground px-2 py-1.5">+${remaining} autre${
|
|
||||||
remaining > 1 ? 's' : ''
|
|
||||||
}</div>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return `<div class="flex flex-col gap-1">${tpeCards}${moreHtml}</div>`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ key: 'zone', label: 'Zone', sortable: true },
|
|
||||||
{ key: 'kiosk', label: 'Kiosque', sortable: true },
|
|
||||||
{ key: 'profile', label: 'Profil', sortable: true },
|
|
||||||
{ key: 'statut', label: 'Statut', sortable: true },
|
|
||||||
{ key: 'limiteSuperieure', label: 'Limite sup.', sortable: true },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
tpeMap = new Map<string, TpeDevice>();
|
tpeMap = new Map<string, TpeDevice>();
|
||||||
agentTpesMap = new Map<string, TpeDevice[]>();
|
agentTpesMap = new Map<string, TpeDevice[]>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private api: AgentService,
|
private api: AgentService,
|
||||||
private tpeSvc: TpeService,
|
private tpeSvc: TpeService,
|
||||||
private familyMemberService: AgentFamilyMemberService
|
private familyMemberService: AgentFamilyMemberService
|
||||||
) {
|
) {
|
||||||
// Preload TPE maps for display
|
|
||||||
this.tpeSvc
|
|
||||||
.list({ page: 1, size: 200, search: '', sortKey: 'imei', sortDir: 'asc' } as any)
|
|
||||||
.subscribe((res) => {
|
|
||||||
const tpes = res.content as TpeDevice[];
|
|
||||||
this.rebuildTpeMaps(tpes);
|
|
||||||
});
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const params = {
|
const params = {
|
||||||
page: this.page(),
|
page: this.page(),
|
||||||
@@ -178,8 +139,6 @@ export class AgentsPage {
|
|||||||
this.rows.set(res.content);
|
this.rows.set(res.content);
|
||||||
this.total.set(res.pageable.total);
|
this.total.set(res.pageable.total);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
// Refresh TPE map to ensure we have latest data
|
|
||||||
this.refreshTpeMap();
|
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.rows.set([]);
|
this.rows.set([]);
|
||||||
@@ -189,31 +148,25 @@ export class AgentsPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private refreshTpeMap() {
|
|
||||||
this.tpeSvc
|
renderStatutBadge(statut: Agent['statut'] | string | undefined): string {
|
||||||
.list({ page: 1, size: 200, search: '', sortKey: 'imei', sortDir: 'asc' } as any)
|
if (!statut) return '';
|
||||||
.subscribe((res) => {
|
const s = String(statut).toUpperCase();
|
||||||
const tpes = res.content as TpeDevice[];
|
if (s === 'ACTIF') {
|
||||||
this.rebuildTpeMaps(tpes);
|
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-green-500/10 text-green-600 dark:text-green-400 text-xs font-medium"><i class="icon-check"></i> Actif</span>`;
|
||||||
});
|
}
|
||||||
|
if (s === 'INACTIF') {
|
||||||
|
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-gray-500/10 text-gray-600 dark:text-gray-400 text-xs font-medium"><i class="icon-x"></i> Inactif</span>`;
|
||||||
|
}
|
||||||
|
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-orange-500/10 text-orange-600 dark:text-orange-400 text-xs font-medium"><i class="icon-alert-circle"></i> Suspendu</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private rebuildTpeMaps(tpes: TpeDevice[]) {
|
formatLimits(a: Agent): string {
|
||||||
this.tpeMap.clear();
|
const parts: string[] = [];
|
||||||
this.agentTpesMap.clear();
|
const nf = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 });
|
||||||
tpes.forEach((t) => {
|
if (a.limiteInferieure !== undefined) parts.push(nf.format(a.limiteInferieure));
|
||||||
this.tpeMap.set(t.id, t);
|
if (a.limiteSuperieure !== undefined) parts.push(nf.format(a.limiteSuperieure));
|
||||||
const agentId = t.agent?.id;
|
return parts.length ? parts.join(' — ') : '';
|
||||||
if (agentId) {
|
|
||||||
const list = this.agentTpesMap.get(agentId) || [];
|
|
||||||
list.push(t);
|
|
||||||
this.agentTpesMap.set(agentId, list);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getAgentTpes(agentId: string): TpeDevice[] {
|
|
||||||
return this.agentTpesMap.get(agentId) || [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(q: string) {
|
onSearch(q: string) {
|
||||||
@@ -233,6 +186,8 @@ export class AgentsPage {
|
|||||||
closeModal() {
|
closeModal() {
|
||||||
this.modalOpen.set(false);
|
this.modalOpen.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
openDetail(row: Agent) {
|
openDetail(row: Agent) {
|
||||||
// Fetch full agent details
|
// Fetch full agent details
|
||||||
this.api.getById(row.id).subscribe({
|
this.api.getById(row.id).subscribe({
|
||||||
@@ -248,6 +203,31 @@ export class AgentsPage {
|
|||||||
this.detailFamilyMembers.set([]);
|
this.detailFamilyMembers.set([]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const tpeIds = agent.terminauxIds;
|
||||||
|
if(Array.isArray(tpeIds)){
|
||||||
|
this.tpeArray.set(true);
|
||||||
|
forkJoin(
|
||||||
|
tpeIds.map(id=>this.tpeSvc.getById(String(id)))
|
||||||
|
).subscribe({
|
||||||
|
next:(tpes)=>{
|
||||||
|
this.tpeDevices.set(tpes.filter(tpe=>tpe!==undefined))
|
||||||
|
},
|
||||||
|
error:(err)=>{
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
this.tpeArray.set(false);
|
||||||
|
this.tpeSvc.getById(String(tpeIds)).subscribe({
|
||||||
|
next:(tpe)=>{
|
||||||
|
if(tpe && tpe !== undefined)
|
||||||
|
this.tpeDevice.set(tpe);
|
||||||
|
},
|
||||||
|
error:(err)=>{
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
this.detailModalOpen.set(true);
|
this.detailModalOpen.set(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -278,6 +258,7 @@ export class AgentsPage {
|
|||||||
|
|
||||||
onFormSave(payload: Partial<Agent>) {
|
onFormSave(payload: Partial<Agent>) {
|
||||||
const current = this.editingItem();
|
const current = this.editingItem();
|
||||||
|
const isCreating = !current?.id; // Mode création (compact)
|
||||||
const familyMembersData = this.formComp?.getFamilyMembersData() || [];
|
const familyMembersData = this.formComp?.getFamilyMembersData() || [];
|
||||||
|
|
||||||
// Save agent first
|
// Save agent first
|
||||||
@@ -298,7 +279,12 @@ export class AgentsPage {
|
|||||||
throw new Error("Impossible d'obtenir l'ID de l'agent sauvegardé");
|
throw new Error("Impossible d'obtenir l'ID de l'agent sauvegardé");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get existing family members for this agent
|
// In creation mode (compact), skip family members management
|
||||||
|
if (isCreating || familyMembersData.length === 0) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get existing family members for this agent (only for updates)
|
||||||
return this.familyMemberService.getByAgentId(savedAgentId).pipe(
|
return this.familyMemberService.getByAgentId(savedAgentId).pipe(
|
||||||
switchMap((existingMembers) => {
|
switchMap((existingMembers) => {
|
||||||
const existingIds = new Set(existingMembers.map((m) => m.id));
|
const existingIds = new Set(existingMembers.map((m) => m.id));
|
||||||
@@ -372,12 +358,13 @@ export class AgentsPage {
|
|||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
toast.success('Agent sauvegardé avec succès');
|
||||||
|
// Close modal first
|
||||||
|
this.closeModal();
|
||||||
// Reset form after successful save
|
// Reset form after successful save
|
||||||
this.formComp?.resetForm();
|
this.formComp?.resetForm();
|
||||||
// Clear editing item
|
// Clear editing item
|
||||||
this.editingItem.set(null);
|
this.editingItem.set(null);
|
||||||
// Close modal
|
|
||||||
this.closeModal();
|
|
||||||
// Refresh data
|
// Refresh data
|
||||||
this.fetch({
|
this.fetch({
|
||||||
page: this.page(),
|
page: this.page(),
|
||||||
@@ -394,6 +381,23 @@ export class AgentsPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getSelectedTpeIds = (): string[] => {
|
||||||
|
const ids = this.assigningAgent()?.terminauxIds;
|
||||||
|
|
||||||
|
if (!ids) return []; // undefined ou null → tableau vide
|
||||||
|
|
||||||
|
// Si c'est un tableau, on map en string
|
||||||
|
if (Array.isArray(ids)) {
|
||||||
|
return ids.map(id => id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si c'est un seul nombre, on retourne un tableau avec un élément
|
||||||
|
return [ids.toString()];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
remove(row: Agent) {
|
remove(row: Agent) {
|
||||||
if (!confirm(`Supprimer l\'agent ${row.code} ?`)) return;
|
if (!confirm(`Supprimer l\'agent ${row.code} ?`)) return;
|
||||||
this.api.delete(row.id).subscribe(() =>
|
this.api.delete(row.id).subscribe(() =>
|
||||||
@@ -409,75 +413,111 @@ export class AgentsPage {
|
|||||||
|
|
||||||
openAssignTpe(agent: Agent) {
|
openAssignTpe(agent: Agent) {
|
||||||
this.assigningAgent.set(agent);
|
this.assigningAgent.set(agent);
|
||||||
this.selectedTpeId.set('');
|
this.selectedTpeId.set([]);
|
||||||
this.loadAvailableTpes();
|
|
||||||
this.assignTpeModalOpen.set(true);
|
this.assignTpeModalOpen.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAvailableTpes() {
|
// loadAvailableTpes() {
|
||||||
this.tpesLoading.set(true);
|
// this.tpesLoading.set(true);
|
||||||
const agent = this.assigningAgent();
|
// const agent = this.assigningAgent();
|
||||||
if (!agent) {
|
// if (!agent) {
|
||||||
this.availableTpes.set([]);
|
// this.availableTpes.set([]);
|
||||||
this.tpesLoading.set(false);
|
// this.tpesLoading.set(false);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const currentAgentTpes = this.agentTpesMap.get(agent.id) || [];
|
// const currentAgentTpes = this.agentTpesMap.get(agent.id) || [];
|
||||||
const agentTpeIds = new Set(currentAgentTpes.map((t) => t.id));
|
// const agentTpeIds = new Set(currentAgentTpes.map((t) => t.id));
|
||||||
|
|
||||||
// Load available TPEs (DISPONIBLE or VALIDE status)
|
// // Load available TPEs (DISPONIBLE or VALIDE status)
|
||||||
forkJoin([this.tpeSvc.getByStatut('DISPONIBLE'), this.tpeSvc.getByStatut('VALIDE')]).subscribe({
|
// forkJoin([this.tpeSvc.getByStatut('ACTIF'), this.tpeSvc.getByStatut('ACTIF')]).subscribe({
|
||||||
next: ([disponibleTpes, valideTpes]) => {
|
// next: ([disponibleTpes, valideTpes]) => {
|
||||||
// Combine and filter: only show TPEs that are not assigned to any agent AND not already assigned to this agent
|
// // Combine and filter: only show TPEs that are not assigned to any agent AND not already assigned to this agent
|
||||||
const allTpes = [...disponibleTpes, ...valideTpes];
|
// const allTpes = [...disponibleTpes, ...valideTpes];
|
||||||
const available = allTpes.filter(
|
// const available = allTpes.filter(
|
||||||
(t) =>
|
// (t) =>
|
||||||
!t.assigne &&
|
// !t.agentConnecteId &&
|
||||||
(t.statut === 'DISPONIBLE' || t.statut === 'VALIDE') &&
|
// (t.statut === 'ACTIF') &&
|
||||||
!agentTpeIds.has(t.id)
|
// !agentTpeIds.has(t.id)
|
||||||
);
|
// );
|
||||||
// Remove duplicates
|
// // Remove duplicates
|
||||||
const uniqueTpes = Array.from(new Map(available.map((t) => [t.id, t])).values());
|
// const uniqueTpes = Array.from(new Map(available.map((t) => [t.id, t])).values());
|
||||||
this.availableTpes.set(uniqueTpes);
|
// this.availableTpes.set(uniqueTpes);
|
||||||
this.tpesLoading.set(false);
|
// this.tpesLoading.set(false);
|
||||||
},
|
// },
|
||||||
error: () => {
|
// error: () => {
|
||||||
this.availableTpes.set([]);
|
// this.availableTpes.set([]);
|
||||||
this.tpesLoading.set(false);
|
// this.tpesLoading.set(false);
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// testAssigne(tpeIds: string[]){
|
||||||
|
// console.log(tpeIds);
|
||||||
|
// }
|
||||||
|
|
||||||
confirmAssignTpe() {
|
confirmAssignTpe() {
|
||||||
const agent = this.assigningAgent();
|
const agent = this.assigningAgent();
|
||||||
const tpeId = this.selectedTpeId();
|
const tpeId = this.selectedTpeId().map(id=> id);
|
||||||
if (!agent || !tpeId) {
|
if (!agent) {
|
||||||
alert('Veuillez sélectionner un TPE');
|
toast.error('Veuillez sélectionner un TPE');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign TPE to agent
|
this.api.assigner(tpeId, agent.id).subscribe({
|
||||||
this.tpeSvc.assigner(tpeId, agent.id).subscribe({
|
next:()=>{
|
||||||
next: (tpe) => {
|
|
||||||
if (tpe) {
|
|
||||||
// Fermer le modal et recharger complètement la page
|
|
||||||
this.assignTpeModalOpen.set(false);
|
this.assignTpeModalOpen.set(false);
|
||||||
this.assigningAgent.set(null);
|
this.assigningAgent.set(undefined);
|
||||||
this.selectedTpeId.set('');
|
this.selectedTpeId.set([]);
|
||||||
// Rechargement complet pour s'assurer que la liste des agents / TPE est à jour
|
toast.success("Termiaux affectés avec succès!");
|
||||||
window.location.reload();
|
const params = {
|
||||||
}
|
page: this.page(),
|
||||||
|
size: this.size(),
|
||||||
|
search: this.search(),
|
||||||
|
sortKey: this.sort().key,
|
||||||
|
sortDir: this.sort().dir as SortDir,
|
||||||
|
};
|
||||||
|
this.fetch(params);
|
||||||
},
|
},
|
||||||
error:()=>{
|
error:()=>{
|
||||||
alert("Erreur lors de l'assignation du TPE");
|
console.error("Une érreur s'est produite")
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
|
// forkJoin(this.selectedTpeId().map(id=> this.api.assigner(id, agent.id))).subscribe(
|
||||||
|
// {
|
||||||
|
// next:()=>{
|
||||||
|
// this.assignTpeModalOpen.set(false);
|
||||||
|
// this.assigningAgent.set(undefined);
|
||||||
|
// this.selectedTpeId.set([]);
|
||||||
|
// toast.success(`Tpe affecté à l'agent avec succès1`)
|
||||||
|
// },
|
||||||
|
// error: (err)=>{
|
||||||
|
// console.error(err);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// // Assign TPE to agent
|
||||||
|
// this.tpeSvc.assigner(tpeId, agent.id).subscribe({
|
||||||
|
// next: (tpe) => {
|
||||||
|
// if (tpe) {
|
||||||
|
// // Fermer le modal et recharger complètement la page
|
||||||
|
|
||||||
|
// this.selectedTpeId.set('');
|
||||||
|
// // Rechargement complet pour s'assurer que la liste des agents / TPE est à jour
|
||||||
|
// window.location.reload();
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// error: () => {
|
||||||
|
// alert("Erreur lors de l'assignation du TPE");
|
||||||
|
// },
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAssignTpeModal() {
|
closeAssignTpeModal() {
|
||||||
this.assignTpeModalOpen.set(false);
|
this.assignTpeModalOpen.set(false);
|
||||||
this.assigningAgent.set(null);
|
this.assigningAgent.set(undefined);
|
||||||
this.selectedTpeId.set('');
|
this.selectedTpeId.set([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</z-card>
|
</z-card>
|
||||||
|
|
||||||
<z-card class="text-center py-4">
|
<z-card class="text-center py-4">
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">En cours</div>
|
<div class="text-sm text-gray-500 dark:text-gray-400">Ouverts</div>
|
||||||
<div class="text-3xl font-bold text-amber-600 dark:text-amber-400 mt-1">
|
<div class="text-3xl font-bold text-amber-600 dark:text-amber-400 mt-1">
|
||||||
{{ runningCourses() }}
|
{{ runningCourses() }}
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</z-card>
|
</z-card>
|
||||||
|
|
||||||
<z-card class="text-center py-4">
|
<z-card class="text-center py-4">
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">Par type</div>
|
<div class="text-sm text-gray-500 dark:text-gray-400">Nombre de statuts</div>
|
||||||
<div class="text-sm mt-2 text-gray-900 dark:text-gray-100 space-y-1">
|
<div class="text-sm mt-2 text-gray-900 dark:text-gray-100 space-y-1">
|
||||||
@for (type of (byType() | keyvalue); track type.key) {
|
@for (type of (byType() | keyvalue); track type.key) {
|
||||||
<div class="flex justify-between px-3">
|
<div class="flex justify-between px-3">
|
||||||
@@ -116,19 +116,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" size="xl" (close)="closeModal()">
|
|
||||||
@if (modalOpen()) {
|
@if (modalOpen()) {
|
||||||
|
<app-modal [open]="modalOpen()" [title]="modalTitle()" size="xl" (close)="closeModal()">
|
||||||
<app-course-form
|
<app-course-form
|
||||||
[value]="editingItem() ?? undefined"
|
[value]="editingItem() ?? undefined"
|
||||||
(save)="onFormSave($event)"
|
(save)="onFormSave($event)"
|
||||||
(cancel)="closeModal()"
|
(cancel)="closeModal()"
|
||||||
></app-course-form>
|
></app-course-form>
|
||||||
}
|
|
||||||
<div modal-actions class="flex gap-2 justify-end">
|
<div modal-actions class="flex gap-2 justify-end">
|
||||||
<z-button zType="destructive" (click)="closeModal()">Annuler</z-button>
|
<z-button zType="destructive" (click)="closeModal()">Annuler</z-button>
|
||||||
<z-button zType="default" (click)="submitChildForm()">Enregistrer</z-button>
|
<z-button
|
||||||
|
zType="default"
|
||||||
|
(click)="submitChildForm()"
|
||||||
|
[attr.aria-disabled]="formComp?.form?.invalid ? 'true' : null"
|
||||||
|
[class.opacity-50]="formComp?.form?.invalid"
|
||||||
|
[class.pointer-events-none]="formComp?.form?.invalid"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</z-button>
|
||||||
</div>
|
</div>
|
||||||
</app-modal>
|
</app-modal>
|
||||||
|
}
|
||||||
|
|
||||||
@if(selectedCourse()) {
|
@if(selectedCourse()) {
|
||||||
<app-modal
|
<app-modal
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import { SearchBar } from '@shared/components/search-bar/search-bar';
|
|||||||
import { Modal } from '@shared/components/modal/modal';
|
import { Modal } from '@shared/components/modal/modal';
|
||||||
import { ZardCardComponent } from '@shared/components/card/card.component';
|
import { ZardCardComponent } from '@shared/components/card/card.component';
|
||||||
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||||
import { Course as CourseType } from 'src/app/core/interfaces/course';
|
import { CourseStatut, Course as CourseType } from 'src/app/core/interfaces/course';
|
||||||
import { SortDir } from '@shared/paging/paging';
|
import { SortDir } from '@shared/paging/paging';
|
||||||
import { CourseApiResponse, CourseService } from 'src/app/core/services/course';
|
import { CourseApiResponse, CourseService, NonApiRequest } from 'src/app/core/services/course';
|
||||||
import { ResultatService } from 'src/app/core/services/resultat';
|
import { ResultatService } from 'src/app/core/services/resultat';
|
||||||
import { Resultat, ResultatStatut } from 'src/app/core/interfaces/resultat';
|
import { Resultat, ResultatStatut } from 'src/app/core/interfaces/resultat';
|
||||||
import { A11yModule } from '@angular/cdk/a11y';
|
import { A11yModule } from '@angular/cdk/a11y';
|
||||||
@@ -210,9 +210,11 @@ export class Course {
|
|||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.rows.set(res.content);
|
this.rows.set(res.content);
|
||||||
this.total.set(res.totalElements);
|
this.total.set(res.totalElements);
|
||||||
this.totalRunning.set(0);
|
this.totalRunning.set(res.content.filter(c=> c.statut === String(CourseStatut.OUVERT)).length);
|
||||||
this.totalClosed.set(0);
|
this.totalClosed.set(res.content.filter(c=>c.statut === String(CourseStatut.FERME)).length);
|
||||||
this.totalByType.set({});
|
this.totalByType.set({
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch resultats for all courses in parallel
|
// Fetch resultats for all courses in parallel
|
||||||
const courseIds = res.content.map((c) => c.id);
|
const courseIds = res.content.map((c) => c.id);
|
||||||
@@ -345,8 +347,13 @@ export class Course {
|
|||||||
onNonPartantSave(payload: string[]) {
|
onNonPartantSave(payload: string[]) {
|
||||||
const course = this.selectedCourse();
|
const course = this.selectedCourse();
|
||||||
if (!course) return;
|
if (!course) return;
|
||||||
|
const reqPayload = {
|
||||||
|
nonPartants: [
|
||||||
|
...payload
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
this.api.setNonPartants(course.id, payload).subscribe({
|
this.api.setNonPartants(course.id, reqPayload).subscribe({
|
||||||
next: (updatedCourse) => {
|
next: (updatedCourse) => {
|
||||||
if (updatedCourse) {
|
if (updatedCourse) {
|
||||||
toast.success('Non-partants mis à jour avec succès');
|
toast.success('Non-partants mis à jour avec succès');
|
||||||
@@ -465,7 +472,10 @@ export class Course {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For now, validation is just an update. In the future, you might add a statut field
|
// For now, validation is just an update. In the future, you might add a statut field
|
||||||
this.resultatService.update(resultat.id, {}).subscribe({
|
this.resultatService.update(resultat.id, {
|
||||||
|
...resultat,
|
||||||
|
statut: ResultatStatut.PROVISOIRE
|
||||||
|
}).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.closeResultatModal();
|
this.closeResultatModal();
|
||||||
this.fetch({
|
this.fetch({
|
||||||
@@ -487,15 +497,18 @@ export class Course {
|
|||||||
onResultatConfirm() {
|
onResultatConfirm() {
|
||||||
const c = this.selectedCourseForResultat();
|
const c = this.selectedCourseForResultat();
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
|
|
||||||
const resultat = this.resultatsMap().get(c.id);
|
const resultat = this.resultatsMap().get(c.id);
|
||||||
|
|
||||||
if (!resultat) {
|
if (!resultat) {
|
||||||
toast.error('Aucun résultat à confirmer');
|
toast.error('Aucun résultat à confirmer');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now, confirmation is just an update. In the future, you might add a statut field
|
// For now, confirmation is just an update. In the future, you might add a statut field
|
||||||
this.resultatService.update(resultat.id, {}).subscribe({
|
this.resultatService.update(resultat.id, {
|
||||||
|
...resultat,
|
||||||
|
statut: ResultatStatut.OFFICIEL
|
||||||
|
}).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.closeResultatModal();
|
this.closeResultatModal();
|
||||||
this.fetch({
|
this.fetch({
|
||||||
|
|||||||
16
src/app/dashboard/pages/gain-details/gain-details.css
Normal file
16
src/app/dashboard/pages/gain-details/gain-details.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Cohesive styles for gain-details */
|
||||||
|
.space-y-4 > * + * { margin-top: 1rem; }
|
||||||
|
.text-muted { color: var(--muted-foreground, #6b7280); }
|
||||||
|
.bg-accent { background-color: var(--accent, #f3f4f6); }
|
||||||
|
.p-2 { padding: 0.5rem; }
|
||||||
|
.p-3 { padding: 0.75rem; }
|
||||||
|
.p-4 { padding: 1rem; }
|
||||||
|
.rounded-md { border-radius: 0.375rem; }
|
||||||
|
.border { border: 1px solid var(--border, #e5e7eb); }
|
||||||
|
.font-medium { font-weight: 600; }
|
||||||
|
|
||||||
|
/* Responsive tweaks */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
65
src/app/dashboard/pages/gain-details/gain-details.html
Normal file
65
src/app/dashboard/pages/gain-details/gain-details.html
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
@if(detail()){
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-2xl font-semibold">Détails du gain — Course n° {{ detail()!.course.numero }}</h2>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<a z-button routerLink="/gains" zType="ghost"><i class="icon-arrow-left mr-2"></i>Retour</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<z-card class="p-4">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Date:</span>
|
||||||
|
{{ detail()!.course.reunionDate | date : 'dd/MM/yyyy' }}
|
||||||
|
</div>
|
||||||
|
<div><span class="font-medium">Nom:</span> {{ detail()!.course.nom }}</div>
|
||||||
|
<div><span class="font-medium">Montant cagnotte:</span> {{ detail()!.montantCagnotte | number : '1.0-0' : 'fr-FR' }} CFA</div>
|
||||||
|
<div><span class="font-medium">Montant à rembourser:</span> {{ detail()!.montantARembourser | number : '1.0-0' : 'fr-FR' }} CFA</div>
|
||||||
|
</div>
|
||||||
|
</z-card>
|
||||||
|
|
||||||
|
@if(detail()!.formules && detail()!.formules.length > 0) {
|
||||||
|
<div class="space-y-4">
|
||||||
|
@for (f of detail()!.formules; track f.id || $index) {
|
||||||
|
<z-card class="p-3">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<div class="text-sm font-medium">Formule: {{ f.gains }} — {{ f.typeFormule }}</div>
|
||||||
|
<div class="text-sm text-muted">Type pari: {{ f.typePari }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-2 text-sm mb-3">
|
||||||
|
<div><span class="font-medium">Masse initiale:</span> {{ f.masseInitiale | number }}</div>
|
||||||
|
<div><span class="font-medium">Masse après prélèvements:</span> {{ f.masseApresPrelevements | number }}</div>
|
||||||
|
<div><span class="font-medium">Masse finale:</span> {{ f.masseFinale | number }}</div>
|
||||||
|
<div><span class="font-medium">Total gagnants:</span> {{ f.totalGagnants }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-md border overflow-hidden">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-accent">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left p-2">Libellé</th>
|
||||||
|
<th class="text-left p-2">Rapport</th>
|
||||||
|
<th class="text-left p-2">Nombre gagnants</th>
|
||||||
|
<th class="text-left p-2">Masse partagée</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of f.rapportsDetails; track r.id || $index) {
|
||||||
|
<tr class="border-t">
|
||||||
|
<td class="p-2">{{ r.libelle }}</td>
|
||||||
|
<td class="p-2">{{ r.rapport }}</td>
|
||||||
|
<td class="p-2">{{ r.nombreGagnants }}</td>
|
||||||
|
<td class="p-2">{{ r.massePartageeRang | number }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</z-card>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
23
src/app/dashboard/pages/gain-details/gain-details.spec.ts
Normal file
23
src/app/dashboard/pages/gain-details/gain-details.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { GainDetails } from './gain-details';
|
||||||
|
|
||||||
|
describe('GainDetails', () => {
|
||||||
|
let component: GainDetails;
|
||||||
|
let fixture: ComponentFixture<GainDetails>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [GainDetails]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(GainDetails);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
37
src/app/dashboard/pages/gain-details/gain-details.ts
Normal file
37
src/app/dashboard/pages/gain-details/gain-details.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||||
|
import { ActivatedRoute, RouterModule, Router } from '@angular/router';
|
||||||
|
import { ZardCardComponent } from '@shared/components/card/card.component';
|
||||||
|
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||||
|
import { Gain } from 'src/app/core/services/gain';
|
||||||
|
import { ResultatCagnotte } from 'src/app/core/interfaces/gain';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'app-gain-details',
|
||||||
|
templateUrl: './gain-details.html',
|
||||||
|
styleUrl: './gain-details.css',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [CommonModule, RouterModule, ZardCardComponent, ZardButtonComponent],
|
||||||
|
})
|
||||||
|
export class GainDetails {
|
||||||
|
detail = signal<ResultatCagnotte | undefined>(undefined);
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute, private api: Gain, private router: Router) {
|
||||||
|
const id = this.route.snapshot.params['id'];
|
||||||
|
if (!id) {
|
||||||
|
// nothing to show, go back
|
||||||
|
this.router.navigate(['/gains']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.api.getById(String(id)).subscribe((d) => {
|
||||||
|
console.log(d);
|
||||||
|
this.detail.set(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.router.navigate(['/gains']);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/app/dashboard/pages/gains/gains.css
Normal file
0
src/app/dashboard/pages/gains/gains.css
Normal file
34
src/app/dashboard/pages/gains/gains.html
Normal file
34
src/app/dashboard/pages/gains/gains.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-2xl font-semibold">Gains — Cagnotte</h2>
|
||||||
|
<z-button zType="default" (click)="fetchPage()">Récupérer les gains</z-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<app-data-table [data]="rows()" [columns]="cols" [loading]="loading()">
|
||||||
|
<ng-template #rowActions let-row>
|
||||||
|
<z-button zType="ghost" zSize="icon" aria-label="Voir le détail" (click)="openReport(row)">
|
||||||
|
<div class="icon-file-text"></div>
|
||||||
|
</z-button>
|
||||||
|
</ng-template>
|
||||||
|
</app-data-table>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-3">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="text-sm">Lignes par page</label>
|
||||||
|
<select class="border rounded px-2 py-1" [value]="perPage()" (change)="onPerPageChangeEvent($event)">
|
||||||
|
<option [value]="5">5</option>
|
||||||
|
<option [value]="10">10</option>
|
||||||
|
<option [value]="25">25</option>
|
||||||
|
<option [value]="50">50</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-muted">{{ totalElements() }} résultats</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<z-pagination [zPageIndex]="page()" [zTotal]="totalPages()" (zPageIndexChange)="onPageChange($event)"></z-pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
23
src/app/dashboard/pages/gains/gains.spec.ts
Normal file
23
src/app/dashboard/pages/gains/gains.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Gains } from './gains';
|
||||||
|
|
||||||
|
describe('Gains', () => {
|
||||||
|
let component: Gains;
|
||||||
|
let fixture: ComponentFixture<Gains>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Gains]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Gains);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
84
src/app/dashboard/pages/gains/gains.ts
Normal file
84
src/app/dashboard/pages/gains/gains.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { DataTable, TableColumn } from '@shared/components/data-table/data-table';
|
||||||
|
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||||
|
import { ZardPaginationModule } from '@shared/components/pagination/pagination.module';
|
||||||
|
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||||
|
import { Gain } from 'src/app/core/services/gain';
|
||||||
|
import { ResultatCagnotte } from 'src/app/core/interfaces/gain';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'app-gains',
|
||||||
|
templateUrl: './gains.html',
|
||||||
|
styleUrl: './gains.css',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [CommonModule, DataTable, ZardButtonComponent, ZardPaginationModule],
|
||||||
|
})
|
||||||
|
export class Gains {
|
||||||
|
rows = signal<ResultatCagnotte[]>([]);
|
||||||
|
loading = signal(false);
|
||||||
|
// Pagination
|
||||||
|
page = signal<number>(1);
|
||||||
|
perPage = signal<number>(10);
|
||||||
|
totalPages = signal<number>(1);
|
||||||
|
totalElements = signal<number>(0);
|
||||||
|
|
||||||
|
cols: TableColumn<ResultatCagnotte>[] = [
|
||||||
|
{ key: 'course.nom', label: 'Course' , cell: (r) => r.course?.nom ?? '—'},
|
||||||
|
{ key: 'montantCagnotte', label: 'Montant cagnotte', cell: (r) => `${r.montantCagnotte.toLocaleString('fr-FR')} CFA` },
|
||||||
|
{ key: 'montantARembourser', label: 'Montant à rembourser', cell: (r) => `${r.montantARembourser.toLocaleString('fr-FR')} CFA` },
|
||||||
|
{ key: 'dateCalcul', label: 'Date calcul', cell: (r) => r.dateCalcul ?? '—' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private gainService: Gain, private router: Router) {
|
||||||
|
this.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPage(params?: Partial<ListParams>) {
|
||||||
|
this.loading.set(true);
|
||||||
|
const p: ListParams = { page: this.page(), size: this.perPage(), ...(params || {}) };
|
||||||
|
this.gainService.list(p).subscribe({
|
||||||
|
next: (res: PagedResult<ResultatCagnotte>) => {
|
||||||
|
this.rows.set(res.content || []);
|
||||||
|
this.totalPages.set(res.totalPages ?? 1);
|
||||||
|
this.totalElements.set(res.totalElements ?? (res.content?.length ?? 0));
|
||||||
|
if ((res as any).pageable?.pageNumber) this.page.set((res as any).pageable.pageNumber);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Error fetching gains:', err);
|
||||||
|
this.rows.set([]);
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageChange(next: number) {
|
||||||
|
this.page.set(next);
|
||||||
|
this.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPerPageChange(size: number) {
|
||||||
|
this.perPage.set(size);
|
||||||
|
this.page.set(1);
|
||||||
|
this.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template-friendly wrapper to safely parse the select event value
|
||||||
|
onPerPageChangeEvent(e: Event) {
|
||||||
|
const v = (e.target as HTMLSelectElement)?.value;
|
||||||
|
const size = Number(v) || 10;
|
||||||
|
this.onPerPageChange(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
openReport(row: ResultatCagnotte) {
|
||||||
|
try {
|
||||||
|
// Navigate to detail page
|
||||||
|
this.router.navigate(['/gains', String(row.course.id)]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to open gain details for', row, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/app/dashboard/pages/point-vente/point-vente.css
Normal file
0
src/app/dashboard/pages/point-vente/point-vente.css
Normal file
41
src/app/dashboard/pages/point-vente/point-vente.html
Normal file
41
src/app/dashboard/pages/point-vente/point-vente.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<div class="flex flex-col gap-2 min-h-screen">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-2xl font-semibold">Gestion des Points de Vente</h2>
|
||||||
|
<z-button (click)="openCreate()">Nouveau point de vente</z-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-search-bar (search)="onSearch($event)"></app-search-bar>
|
||||||
|
|
||||||
|
<app-data-table [data]="rows()" [columns]="cols" [sort]="sort()" (sortChange)="sort.set($event)">
|
||||||
|
<ng-template #rowActions let-row>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button z-button zType="ghost" (click)="openEdit(row)" title="Modifier">
|
||||||
|
<i class="icon-pen"></i>
|
||||||
|
</button>
|
||||||
|
<button z-button zType="destructive" (click)="remove(row)" title="Supprimer">
|
||||||
|
<i class="icon-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-data-table>
|
||||||
|
|
||||||
|
<app-paginator
|
||||||
|
[total]="total()"
|
||||||
|
[page]="page()"
|
||||||
|
[perPage]="size()"
|
||||||
|
(pageChange)="page.set($event - 1)"
|
||||||
|
(perPageChange)="size.set($event)"
|
||||||
|
></app-paginator>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xl">
|
||||||
|
<app-points-vente-form
|
||||||
|
[value]="editingItem() ?? undefined"
|
||||||
|
(save)="onFormSave($event)"
|
||||||
|
(cancel)="closeModal()"
|
||||||
|
/>
|
||||||
|
<div modal-actions class="flex justify-end gap-2">
|
||||||
|
<z-button zType="destructive" (click)="closeModal()">Annuler</z-button>
|
||||||
|
<z-button (click)="submitChildForm()">Enregistrer</z-button>
|
||||||
|
</div>
|
||||||
|
</app-modal>
|
||||||
23
src/app/dashboard/pages/point-vente/point-vente.spec.ts
Normal file
23
src/app/dashboard/pages/point-vente/point-vente.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PointVente } from './point-vente';
|
||||||
|
|
||||||
|
describe('PointVente', () => {
|
||||||
|
let component: PointVente;
|
||||||
|
let fixture: ComponentFixture<PointVente>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PointVente]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PointVente);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
209
src/app/dashboard/pages/point-vente/point-vente.ts
Normal file
209
src/app/dashboard/pages/point-vente/point-vente.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
ViewChild,
|
||||||
|
effect,
|
||||||
|
signal,
|
||||||
|
untracked,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { DataTable, SortState, TableColumn } from '@shared/components/data-table/data-table';
|
||||||
|
import { Paginator } from '@shared/components/paginator/paginator';
|
||||||
|
import { SearchBar } from '@shared/components/search-bar/search-bar';
|
||||||
|
import { Modal } from '@shared/components/modal/modal';
|
||||||
|
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||||
|
import { SortDir } from '@shared/paging/paging';
|
||||||
|
import { PointVente, PointVenteStatut } from 'src/app/core/interfaces/points-ventes';
|
||||||
|
import { PointsVenteService } from 'src/app/core/services/points-vente';
|
||||||
|
import { PointsVenteForm } from '@shared/forms/points-vente-form/points-vente-form';
|
||||||
|
import { toast } from 'ngx-sonner';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: 'app-point-vente',
|
||||||
|
templateUrl: './point-vente.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
DataTable,
|
||||||
|
Paginator,
|
||||||
|
SearchBar,
|
||||||
|
Modal,
|
||||||
|
ZardButtonComponent,
|
||||||
|
PointsVenteForm,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PointVentePage {
|
||||||
|
rows = signal<PointVente[]>([]);
|
||||||
|
total = signal(0);
|
||||||
|
loading = signal(false);
|
||||||
|
|
||||||
|
page = signal(0);
|
||||||
|
size = signal(10);
|
||||||
|
search = signal('');
|
||||||
|
sort = signal<SortState>({ key: 'nom', dir: 'asc' });
|
||||||
|
|
||||||
|
modalOpen = signal(false);
|
||||||
|
modalTitle = signal('Nouveau point de vente');
|
||||||
|
editingItem = signal<PointVente | null>(null);
|
||||||
|
|
||||||
|
@ViewChild(PointsVenteForm) formComp?: PointsVenteForm;
|
||||||
|
|
||||||
|
cols: TableColumn<PointVente>[] = [
|
||||||
|
{ key: 'nom', label: 'Nom', sortable: true, defaultVisible: true },
|
||||||
|
{ key: 'adresse', label: 'Adresse', sortable: true, defaultVisible: true },
|
||||||
|
{ key: 'ville', label: 'Ville', sortable: true, defaultVisible: true },
|
||||||
|
{
|
||||||
|
key: 'statut',
|
||||||
|
label: 'Statut',
|
||||||
|
sortable: true,
|
||||||
|
defaultVisible: true,
|
||||||
|
cell: (pv) => this.renderStatutBadge(pv.statut),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'latitude',
|
||||||
|
label: 'Latitude',
|
||||||
|
cell: (pv) => pv.latitude?.toFixed(6) || '—',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'longitude',
|
||||||
|
label: 'Longitude',
|
||||||
|
cell: (pv) => pv.longitude?.toFixed(6) || '—',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private api: PointsVenteService) {
|
||||||
|
effect(() => {
|
||||||
|
const params = {
|
||||||
|
page: this.page(),
|
||||||
|
size: this.size(),
|
||||||
|
search: this.search(),
|
||||||
|
sortKey: this.sort().key,
|
||||||
|
sortDir: this.sort().dir as SortDir,
|
||||||
|
};
|
||||||
|
untracked(() => this.fetch(params));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetch(params: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
search: string;
|
||||||
|
sortKey: string;
|
||||||
|
sortDir: SortDir;
|
||||||
|
}) {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.api.list(params).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
this.rows.set(res.content);
|
||||||
|
this.total.set(res.pageable.total);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.rows.set([]);
|
||||||
|
this.total.set(0);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderStatutBadge(statut: PointVenteStatut | string | undefined): string {
|
||||||
|
if (!statut) return '';
|
||||||
|
const s = String(statut).toUpperCase();
|
||||||
|
if (s === 'ACTIF') {
|
||||||
|
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-green-500/10 text-green-600 dark:text-green-400 text-xs font-medium"><i class="icon-check"></i> Actif</span>`;
|
||||||
|
}
|
||||||
|
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-gray-500/10 text-gray-600 dark:text-gray-400 text-xs font-medium"><i class="icon-x"></i> Inactif</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(q: string) {
|
||||||
|
this.search.set(q);
|
||||||
|
this.page.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
openCreate() {
|
||||||
|
this.modalTitle.set('Nouveau point de vente');
|
||||||
|
this.editingItem.set(null);
|
||||||
|
queueMicrotask(() => this.modalOpen.set(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
openEdit(row: PointVente) {
|
||||||
|
this.modalTitle.set("Modifier le point de vente");
|
||||||
|
this.editingItem.set(row);
|
||||||
|
queueMicrotask(() => this.modalOpen.set(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.modalOpen.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitChildForm() {
|
||||||
|
this.formComp?.onSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFormSave(payload: Partial<PointVente>) {
|
||||||
|
const current = this.editingItem();
|
||||||
|
const isCreating = !current?.id;
|
||||||
|
|
||||||
|
const req$ = current?.id
|
||||||
|
? this.api.update(current.id, payload)
|
||||||
|
: this.api.create(payload as Omit<PointVente, 'id'>);
|
||||||
|
|
||||||
|
req$.subscribe({
|
||||||
|
next: (result) => {
|
||||||
|
if (result) {
|
||||||
|
toast.success(
|
||||||
|
isCreating
|
||||||
|
? 'Point de vente créé avec succès'
|
||||||
|
: 'Point de vente modifié avec succès'
|
||||||
|
);
|
||||||
|
// Close modal first
|
||||||
|
this.closeModal();
|
||||||
|
// Reset form
|
||||||
|
this.formComp?.resetForm();
|
||||||
|
// Clear editing item
|
||||||
|
this.editingItem.set(null);
|
||||||
|
// Refresh data
|
||||||
|
this.fetch({
|
||||||
|
page: this.page(),
|
||||||
|
size: this.size(),
|
||||||
|
search: this.search(),
|
||||||
|
sortKey: this.sort().key,
|
||||||
|
sortDir: this.sort().dir as SortDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Error saving point de vente:', err);
|
||||||
|
alert(
|
||||||
|
isCreating
|
||||||
|
? "Erreur lors de la création du point de vente"
|
||||||
|
: "Erreur lors de la modification du point de vente"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(row: PointVente) {
|
||||||
|
if (!confirm(`Supprimer le point de vente "${row.nom}" ?`)) return;
|
||||||
|
this.api.delete(row.id).subscribe({
|
||||||
|
next: (success) => {
|
||||||
|
if (success) {
|
||||||
|
toast.success('Point de vente supprimé avec succès');
|
||||||
|
this.fetch({
|
||||||
|
page: this.page(),
|
||||||
|
size: this.size(),
|
||||||
|
search: this.search(),
|
||||||
|
sortKey: this.sort().key,
|
||||||
|
sortDir: this.sort().dir as SortDir,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert("Erreur lors de la suppression du point de vente");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
alert("Erreur lors de la suppression du point de vente");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-2xl font-semibold">Rapport — Courses avec résultats</h2>
|
<h2 class="text-2xl font-semibold">Résultats — Courses</h2>
|
||||||
<z-button zType="default" (click)="fetch()">Récupérer le rapport</z-button>
|
<z-button zType="default" (click)="fetch()">Récupérer les résultats</z-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<app-data-table [data]="rows()" [columns]="cols" [loading]="loading()">
|
<app-data-table [data]="rows()" [columns]="cols" [loading]="loading()">
|
||||||
<ng-template #rowActions let-row>
|
<ng-template #rowActions let-row>
|
||||||
<z-button zType="ghost" zSize="icon" aria-label="Voir le rapport" (click)="openReport(row)">
|
<z-button
|
||||||
|
(click)="sendToDepouillement(row)"
|
||||||
|
[zLoading]="isSending(row.id)"
|
||||||
|
zType="ghost"
|
||||||
|
zSize="icon"
|
||||||
|
aria-label="Voir le résultat" >
|
||||||
<div class="icon-file-text"></div>
|
<div class="icon-file-text"></div>
|
||||||
</z-button>
|
</z-button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import { DataTable, TableColumn } from '@shared/components/data-table/data-table
|
|||||||
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||||
import { ZardPaginationModule } from '@shared/components/pagination/pagination.module';
|
import { ZardPaginationModule } from '@shared/components/pagination/pagination.module';
|
||||||
import { ListParams, PagedResult } from '@shared/paging/paging';
|
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||||
import { ResultatApiResponse } from 'src/app/core/interfaces/resultat';
|
import { ResultatApiResponse, ResultatStatut } from 'src/app/core/interfaces/resultat';
|
||||||
import { ResultatService } from 'src/app/core/services/resultat';
|
import { ResultatService } from 'src/app/core/services/resultat';
|
||||||
|
import { Depouillement, ResultatCourse } from 'src/app/core/services/depouillement';
|
||||||
|
import { Course } from 'src/app/core/interfaces/course';
|
||||||
|
import { toast } from 'ngx-sonner';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -17,6 +20,7 @@ import { ResultatService } from 'src/app/core/services/resultat';
|
|||||||
export class Rapport {
|
export class Rapport {
|
||||||
rows = signal<ResultatApiResponse[]>([]);
|
rows = signal<ResultatApiResponse[]>([]);
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
|
sending = signal<Map<string, boolean>>(new Map());
|
||||||
// Pagination state
|
// Pagination state
|
||||||
page = signal<number>(1);
|
page = signal<number>(1);
|
||||||
perPage = signal<number>(10);
|
perPage = signal<number>(10);
|
||||||
@@ -34,7 +38,7 @@ export class Rapport {
|
|||||||
{ key: 'dateValidation', label: 'Date validation' },
|
{ key: 'dateValidation', label: 'Date validation' },
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(private api: ResultatService) {
|
constructor(private api: ResultatService, private depouillement: Depouillement) {
|
||||||
// initial load
|
// initial load
|
||||||
this.fetch();
|
this.fetch();
|
||||||
}
|
}
|
||||||
@@ -85,10 +89,66 @@ export class Rapport {
|
|||||||
openReport(row: ResultatApiResponse) {
|
openReport(row: ResultatApiResponse) {
|
||||||
try {
|
try {
|
||||||
// Open a per-result report URL in a new tab. Adjust path if your server uses another route.
|
// Open a per-result report URL in a new tab. Adjust path if your server uses another route.
|
||||||
const url = `/rapport/${row.id}`;
|
const url = `/resultat/${row.id}`;
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to open report for', row, err);
|
console.error('Failed to open report for', row, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSending(id: string | number) {
|
||||||
|
return !!this.sending().get(String(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private setSending(id: string | number, v: boolean) {
|
||||||
|
const map = new Map(this.sending());
|
||||||
|
map.set(String(id), v);
|
||||||
|
this.sending.set(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToDepouillement(row: ResultatApiResponse) {
|
||||||
|
if (!row || !row.id) return;
|
||||||
|
const id = String(row.id);
|
||||||
|
if (this.isSending(id)) return; // already sending
|
||||||
|
|
||||||
|
this.setSending(id, true);
|
||||||
|
|
||||||
|
// Build a minimal ResultatCourse payload using available fields.
|
||||||
|
const course = {
|
||||||
|
id: String((row as any).courseId ?? '')
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload: Omit<ResultatCourse, "id"> = {
|
||||||
|
course,
|
||||||
|
statut: (row.statut as any) ?? (0 as any),
|
||||||
|
ordreArrivee: String(row.ordreArrivee ?? ''),
|
||||||
|
datePublication: row.datePublication ?? row.createdAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.depouillement.sendResultat(payload).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
// After successful depouillement, update the resultat statut to PROVISOIRE
|
||||||
|
const updateId = String((res && (res as any).id) ?? row.id);
|
||||||
|
this.api.update(updateId, { statut: ResultatStatut.PROVISOIRE }).subscribe({
|
||||||
|
next: (updated) => {
|
||||||
|
// Update the local rows to reflect the new statut
|
||||||
|
this.rows.set(
|
||||||
|
this.rows().map((r) => (String(r.id) === String(updateId) ? { ...r, statut: ResultatStatut.PROVISOIRE } : r))
|
||||||
|
);
|
||||||
|
toast.success('Résultat envoyé au dépouillement et statut mis à jour.');
|
||||||
|
this.setSending(id, false);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Error updating resultat statut after depouillement:', err);
|
||||||
|
toast.error('Échec de la mise à jour du statut du résultat.');
|
||||||
|
this.setSending(id, false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Error sending to depouillement:', err);
|
||||||
|
this.setSending(id, false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (modalOpen()) {
|
||||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" size="md" (close)="closeModal()">
|
<app-modal [open]="modalOpen()" [title]="modalTitle()" size="md" (close)="closeModal()">
|
||||||
<app-reunion-form
|
<app-reunion-form
|
||||||
[value]="editingItem()"
|
[value]="editingItem()"
|
||||||
@@ -84,4 +85,5 @@
|
|||||||
<z-button zType="default" (click)="submitChildForm()">Enregistrer</z-button>
|
<z-button zType="default" (click)="submitChildForm()">Enregistrer</z-button>
|
||||||
</div>
|
</div>
|
||||||
</app-modal>
|
</app-modal>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
0
src/app/dashboard/pages/tpe-select/tpe-select.css
Normal file
0
src/app/dashboard/pages/tpe-select/tpe-select.css
Normal file
35
src/app/dashboard/pages/tpe-select/tpe-select.html
Normal file
35
src/app/dashboard/pages/tpe-select/tpe-select.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="p-4 border rounded-lg shadow bg-white dark:bg-gray-900">
|
||||||
|
@if (loading()) {
|
||||||
|
<div class="flex justify-center py-4">
|
||||||
|
<div class="w-6 h-6 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if(!loading()){
|
||||||
|
@if(tpes().length > 0){
|
||||||
|
<div>
|
||||||
|
<app-data-table
|
||||||
|
[data]="tpes()"
|
||||||
|
[columns]="columns">
|
||||||
|
<ng-template #rowActions let-row>
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<input type="checkbox" (click)="toggleTpe(row)" [checked] = "isChecked(row)" />
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-data-table>
|
||||||
|
<app-paginator
|
||||||
|
[total]="total()"
|
||||||
|
[page]="page()"
|
||||||
|
[perPage]="perPage()"
|
||||||
|
(pageChange)="page.set($event)"
|
||||||
|
(perPageChange)="perPage.set($event)">
|
||||||
|
</app-paginator>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (tpes().length === 0) {
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
Aucun TPE disponible.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
23
src/app/dashboard/pages/tpe-select/tpe-select.spec.ts
Normal file
23
src/app/dashboard/pages/tpe-select/tpe-select.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TpeSelect } from './tpe-select';
|
||||||
|
|
||||||
|
describe('TpeSelect', () => {
|
||||||
|
let component: TpeSelect;
|
||||||
|
let fixture: ComponentFixture<TpeSelect>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TpeSelect]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(TpeSelect);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
108
src/app/dashboard/pages/tpe-select/tpe-select.ts
Normal file
108
src/app/dashboard/pages/tpe-select/tpe-select.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { Component, effect, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||||
|
import { SortState, TableColumn, DataTable } from '@shared/components/data-table/data-table';
|
||||||
|
import { ListParams, SortDir } from '@shared/paging/paging';
|
||||||
|
import { Agent } from 'src/app/core/interfaces/agent';
|
||||||
|
import { TpeDevice } from 'src/app/core/interfaces/tpe';
|
||||||
|
import { TpeService } from 'src/app/core/services/tpe';
|
||||||
|
import { Paginator } from "@shared/components/paginator/paginator";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-tpe-select',
|
||||||
|
imports: [DataTable, Paginator],
|
||||||
|
templateUrl: './tpe-select.html',
|
||||||
|
styleUrl: './tpe-select.css',
|
||||||
|
})
|
||||||
|
export class TpeSelect {
|
||||||
|
@Input() agent?: Agent; // Agent pour filtrer les TPE assignés
|
||||||
|
@Input() selected: string[] = []; // Ids de TPE à cocher par défaut
|
||||||
|
|
||||||
|
@Output() selectionChange = new EventEmitter<string[]>(); // TPE sélectionnés
|
||||||
|
|
||||||
|
tpes = signal<TpeDevice[]>([]);
|
||||||
|
total = signal(0);
|
||||||
|
loading = signal<boolean>(true);
|
||||||
|
selectedIds = signal<Set<string>>(new Set());
|
||||||
|
page = signal(0);
|
||||||
|
perPage = signal(10);
|
||||||
|
search = signal('');
|
||||||
|
sort = signal<SortState>({ key: 'id', dir: 'asc' });
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
columns:TableColumn<TpeDevice>[] = [
|
||||||
|
{
|
||||||
|
key: 'numeroSerie',
|
||||||
|
label: "Numero de serie"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'versionLogicielle',
|
||||||
|
label: "Version du logiciel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'modeleAppareil',
|
||||||
|
label: "Model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'systemeExploitation',
|
||||||
|
label: 'Système d\'exploitation'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'versionOs',
|
||||||
|
label: 'Version Os'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor(private tpeService: TpeService) {
|
||||||
|
effect(()=>{
|
||||||
|
const params = {
|
||||||
|
page: this.page(),
|
||||||
|
size: this.perPage(),
|
||||||
|
sortKey: this.sort().key,
|
||||||
|
sortDir: this.sort().dir as SortDir,
|
||||||
|
}
|
||||||
|
this.loadTpes(params);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Initialiser les TPE sélectionnés si fournis
|
||||||
|
this.selectedIds.set(new Set(this.selected));
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadTpes(params:ListParams) {
|
||||||
|
this.loading.set(true);
|
||||||
|
this.tpeService.list(params).subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
// Filtrer : TPE non assignés ou déjà assignés à cet agent
|
||||||
|
//console.log(res.content);
|
||||||
|
const available = res.content.filter(
|
||||||
|
(t) =>
|
||||||
|
(t.assigned && this.selectedIds().has(String(t.id))) || !t.assigned
|
||||||
|
);
|
||||||
|
this.tpes.set(available);
|
||||||
|
this.total.set(res.pageable.total);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error(err);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTpe(tpe: TpeDevice) {
|
||||||
|
const current = new Set(this.selectedIds());
|
||||||
|
if (current.has(String(tpe.id))) {
|
||||||
|
current.delete(String(tpe.id));
|
||||||
|
} else {
|
||||||
|
current.add(String(tpe.id));
|
||||||
|
}
|
||||||
|
this.selectedIds.set(current);
|
||||||
|
this.selectionChange.emit(Array.from(current));
|
||||||
|
}
|
||||||
|
|
||||||
|
isChecked(tpe: TpeDevice) {
|
||||||
|
return this.selectedIds().has(String(tpe.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -137,17 +137,19 @@
|
|||||||
[total]="total()"
|
[total]="total()"
|
||||||
[page]="page()"
|
[page]="page()"
|
||||||
[perPage]="perPage()"
|
[perPage]="perPage()"
|
||||||
(pageChange)="page.set($event)"
|
(pageChange)="page.set($event - 1)"
|
||||||
(perPageChange)="perPage.set($event)"
|
(perPageChange)="perPage.set($event)"
|
||||||
></app-paginator>
|
></app-paginator>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xl">
|
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xl">
|
||||||
|
@if (modalOpen()) {
|
||||||
<app-tpe-form
|
<app-tpe-form
|
||||||
[value]="editingItem() ?? undefined"
|
[value]="editingItem() ?? undefined"
|
||||||
(save)="onFormSave($event)"
|
(save)="onFormSave($event)"
|
||||||
(cancel)="closeModal()"
|
(cancel)="closeModal()"
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
<div modal-actions class="flex justify-end gap-2">
|
<div modal-actions class="flex justify-end gap-2">
|
||||||
<z-button zType="destructive" (click)="closeModal()">Annuler</z-button>
|
<z-button zType="destructive" (click)="closeModal()">Annuler</z-button>
|
||||||
<z-button (click)="submitChildForm()">Enregistrer</z-button>
|
<z-button (click)="submitChildForm()">Enregistrer</z-button>
|
||||||
@@ -157,7 +159,7 @@
|
|||||||
<!-- Agent Assignment Modal -->
|
<!-- Agent Assignment Modal -->
|
||||||
<app-modal
|
<app-modal
|
||||||
[open]="assignModalOpen()"
|
[open]="assignModalOpen()"
|
||||||
[title]="'Assigner le TPE ' + (assigningTpe()?.imei || '')"
|
[title]="'Assigner le TPE ' + (assigningTpe()?.numeroSerie || '')"
|
||||||
(close)="closeAssignModal()"
|
(close)="closeAssignModal()"
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ import { ZardSelectItemComponent } from '@shared/components/select/select-item.c
|
|||||||
import { ZardFormModule } from '@shared/components/form/form.module';
|
import { ZardFormModule } from '@shared/components/form/form.module';
|
||||||
import { forkJoin, Subject } from 'rxjs';
|
import { forkJoin, Subject } from 'rxjs';
|
||||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||||
|
import { toast } from 'ngx-sonner';
|
||||||
|
import { PointsVenteService } from 'src/app/core/services/points-vente';
|
||||||
|
import { PointVente } from 'src/app/core/interfaces/points-ventes';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -52,10 +55,10 @@ export class TpePage implements OnInit {
|
|||||||
total = signal(0);
|
total = signal(0);
|
||||||
loading = signal(false);
|
loading = signal(false);
|
||||||
|
|
||||||
page = signal(1);
|
page = signal(0);
|
||||||
perPage = signal(10);
|
perPage = signal(10);
|
||||||
search = signal('');
|
search = signal('');
|
||||||
sort = signal<SortState>({ key: 'imei', dir: 'asc' });
|
sort = signal<SortState>({ key: 'id', dir: 'asc' });
|
||||||
selectedStatut = signal<TpeStatus | null>(null);
|
selectedStatut = signal<TpeStatus | null>(null);
|
||||||
|
|
||||||
modalOpen = signal(false);
|
modalOpen = signal(false);
|
||||||
@@ -74,6 +77,8 @@ export class TpePage implements OnInit {
|
|||||||
assignmentStats = signal({ total: 0, assignes: 0, disponibles: 0 });
|
assignmentStats = signal({ total: 0, assignes: 0, disponibles: 0 });
|
||||||
statsLoading = signal(false);
|
statsLoading = signal(false);
|
||||||
|
|
||||||
|
pointDeVenteMap = signal<Map<string, PointVente>>(new Map());
|
||||||
|
|
||||||
// Live search
|
// Live search
|
||||||
private searchSubject = new Subject<string>();
|
private searchSubject = new Subject<string>();
|
||||||
|
|
||||||
@@ -95,21 +100,31 @@ export class TpePage implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cols: TableColumn<TpeDevice>[] = [
|
cols: TableColumn<TpeDevice>[] = [
|
||||||
{ key: 'imei', label: 'IMEI', sortable: true },
|
{ key: 'numeroSerie', label: 'Numéro de série', sortable: true },
|
||||||
{ key: 'serial', label: 'N° de Série', sortable: true },
|
{
|
||||||
{ key: 'type', label: 'Type', sortable: true },
|
key: 'pointDeVenteId',
|
||||||
{ key: 'marque', label: 'Marque', sortable: true },
|
label: 'Point de vente',
|
||||||
{ key: 'modele', label: 'Modèle', sortable: true },
|
sortable: true,
|
||||||
|
cell: (t) => {
|
||||||
|
const pdv = this.pointDeVenteMap().get(String(t.pointDeVenteId));
|
||||||
|
if (!pdv) return 'Pas de point de vente';
|
||||||
|
return `${pdv.ville}/${pdv.adresse}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'versionLogicielle', label: 'Version', sortable: true },
|
||||||
|
{ key: 'typeTerminal', label: 'Type', sortable: true },
|
||||||
|
{ key: 'systemeExploitation', label: 'Sytème', sortable: true },
|
||||||
{ key: 'statut', label: 'Statut', sortable: true, cell: (d) => this.formatStatut(d.statut) },
|
{ key: 'statut', label: 'Statut', sortable: true, cell: (d) => this.formatStatut(d.statut) },
|
||||||
{
|
{
|
||||||
key: 'assigne',
|
key: 'assigne',
|
||||||
label: 'Assigné à',
|
label: 'Assigné à',
|
||||||
cell: (d) => {
|
cell: (d) => {
|
||||||
if (!d.assigne || !d.agent) {
|
if (!d.assigned) {
|
||||||
return '<span class="text-muted-foreground text-sm">Non assigné</span>';
|
return '<span class="text-muted-foreground text-sm">Non assigné</span>';
|
||||||
}
|
}
|
||||||
const agent = d.agent;
|
// a rectifier apres avec les données des agents!
|
||||||
const code = agent.code
|
const agent = {} as any;
|
||||||
|
const code = agent?.code
|
||||||
? `<span class="inline-flex items-center px-2 py-1 rounded bg-primary/10 text-primary text-xs font-medium">${agent.code}</span>`
|
? `<span class="inline-flex items-center px-2 py-1 rounded bg-primary/10 text-primary text-xs font-medium">${agent.code}</span>`
|
||||||
: '';
|
: '';
|
||||||
const name =
|
const name =
|
||||||
@@ -134,19 +149,13 @@ export class TpePage implements OnInit {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
allStatuses: TpeStatus[] = [
|
allStatuses: TpeStatus[] = ['ACTIF', 'HORS_SERVICE'];
|
||||||
'VALIDE',
|
|
||||||
'INVALIDE',
|
|
||||||
'EN_PANNE',
|
|
||||||
'BLOQUE',
|
|
||||||
'DISPONIBLE',
|
|
||||||
'AFFECTE',
|
|
||||||
'EN_MAINTENANCE',
|
|
||||||
'HORS_SERVICE',
|
|
||||||
'VOLE',
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(private api: TpeService, private agentService: AgentService) {
|
constructor(
|
||||||
|
private api: TpeService,
|
||||||
|
private agentService: AgentService,
|
||||||
|
private pointVenteService: PointsVenteService
|
||||||
|
) {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
// Only trigger fetch when page, perPage, or sort changes (not search - handled by searchSubject)
|
// Only trigger fetch when page, perPage, or sort changes (not search - handled by searchSubject)
|
||||||
const searchValue = this.search();
|
const searchValue = this.search();
|
||||||
@@ -279,10 +288,32 @@ export class TpePage implements OnInit {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Normal list with pagination
|
// Normal list with pagination
|
||||||
|
const map: Map<string, PointVente> = new Map();
|
||||||
this.api.list(params).subscribe({
|
this.api.list(params).subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.rows.set(res.content);
|
this.rows.set(res.content);
|
||||||
this.total.set(res.pageable.total);
|
this.total.set(res.pageable.total);
|
||||||
|
const requests = res.content
|
||||||
|
.filter((t) => t.pointDeVenteId)
|
||||||
|
.map((t) => this.pointVenteService.getById(t.pointDeVenteId!));
|
||||||
|
forkJoin(requests).subscribe({
|
||||||
|
next: (pdvs) => {
|
||||||
|
res.content.forEach((t) => {
|
||||||
|
if (!t.pointDeVenteId) return;
|
||||||
|
|
||||||
|
const pdv = pdvs.find((p) => p?.id === t.pointDeVenteId);
|
||||||
|
if (!pdv) return;
|
||||||
|
|
||||||
|
map.set(String(t.pointDeVenteId), pdv);
|
||||||
|
});
|
||||||
|
// ✅ SET ICI seulement
|
||||||
|
this.pointDeVenteMap.set(map);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: () => this.loading.set(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pointDeVenteMap.set(map);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
@@ -326,7 +357,8 @@ export class TpePage implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUpdateStatut(row: TpeDevice, newStatut: TpeStatus) {
|
onUpdateStatut(row: TpeDevice, newStatut: TpeStatus) {
|
||||||
if (!confirm(`Changer le statut de ${row.imei} vers ${this.formatStatut(newStatut)} ?`)) return;
|
if (!confirm(`Changer le statut de ${row.numeroSerie} vers ${this.formatStatut(newStatut)} ?`))
|
||||||
|
return;
|
||||||
this.api.updateStatut(row.id, newStatut).subscribe({
|
this.api.updateStatut(row.id, newStatut).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.fetch({
|
this.fetch({
|
||||||
@@ -342,7 +374,7 @@ export class TpePage implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onLiberer(row: TpeDevice) {
|
onLiberer(row: TpeDevice) {
|
||||||
if (!confirm(`Libérer le TPE ${row.imei} ?`)) return;
|
if (!confirm(`Libérer le TPE ${row.numeroSerie} ?`)) return;
|
||||||
this.api.liberer(row.id).subscribe({
|
this.api.liberer(row.id).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.fetch({
|
this.fetch({
|
||||||
@@ -440,6 +472,7 @@ export class TpePage implements OnInit {
|
|||||||
: this.api.create(payload as Omit<TpeDevice, 'id'>);
|
: this.api.create(payload as Omit<TpeDevice, 'id'>);
|
||||||
req$.subscribe({
|
req$.subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
|
toast.success('Tpe créé avec succès!');
|
||||||
// For update, check if result is valid (update can return undefined on error)
|
// For update, check if result is valid (update can return undefined on error)
|
||||||
if (current?.id && !result) {
|
if (current?.id && !result) {
|
||||||
console.error('Update failed - result is undefined');
|
console.error('Update failed - result is undefined');
|
||||||
@@ -472,7 +505,7 @@ export class TpePage implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove(row: TpeDevice) {
|
remove(row: TpeDevice) {
|
||||||
if (!confirm(`Supprimer l\'équipement IMEI ${row.imei} ?`)) return;
|
if (!confirm(`Supprimer l\'équipement IMEI ${row.numeroSerie} ?`)) return;
|
||||||
this.api.delete(row.id).subscribe(() => {
|
this.api.delete(row.id).subscribe(() => {
|
||||||
this.fetch({
|
this.fetch({
|
||||||
page: this.page(),
|
page: this.page(),
|
||||||
|
|||||||
@@ -1,91 +1,293 @@
|
|||||||
<form class="space-y-4" (ngSubmit)="onSubmit()" [formGroup]="form">
|
<form class="space-y-6" [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||||
|
|
||||||
|
<!-- SECTION IDENTIFICATION -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow space-y-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Identification</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
||||||
<z-form-field>
|
<z-form-field>
|
||||||
<label z-form-label>Code</label>
|
<label z-form-label>Code</label>
|
||||||
<div z-form-control [errorMessage]="error('code') || ''"><input z-input formControlName="code" /></div>
|
<div z-form-control [errorMessage]="errorMessage('code') || ''">
|
||||||
</z-form-field>
|
<input z-input formControlName="code" placeholder="Code de l'agent" />
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Profil</label>
|
|
||||||
<div z-form-control [errorMessage]="error('profile') || ''"><input z-input formControlName="profile" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Statut</label>
|
|
||||||
<div z-form-control [errorMessage]="error('statut') || ''">
|
|
||||||
<z-select formControlName="statut" [zPlaceholder]="'Sélectionner...'">
|
|
||||||
<z-select-item zValue="ACTIF">Actif</z-select-item>
|
|
||||||
<z-select-item zValue="INACTIF">Inactif</z-select-item>
|
|
||||||
<z-select-item zValue="SUSPENDU">Suspendu</z-select-item>
|
|
||||||
</z-select>
|
|
||||||
</div>
|
</div>
|
||||||
</z-form-field>
|
</z-form-field>
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Zone</label>
|
|
||||||
<div z-form-control><input z-input formControlName="zone" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Kiosque</label>
|
|
||||||
<div z-form-control><input z-input formControlName="kiosk" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Fonction</label>
|
|
||||||
<div z-form-control><input z-input formControlName="fonction" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
|
|
||||||
<z-form-field>
|
<z-form-field>
|
||||||
<label z-form-label>Nom</label>
|
<label z-form-label>Profil</label>
|
||||||
<div z-form-control [errorMessage]="error('nom') || ''"><input z-input formControlName="nom" /></div>
|
<div z-form-control [errorMessage]="errorMessage('profil') || ''">
|
||||||
</z-form-field>
|
<z-select formControlName="profil">
|
||||||
<z-form-field>
|
@for (p of profils; track p) {
|
||||||
<label z-form-label>Prénom</label>
|
<z-select-item [zValue]="p.value">{{ p.label }}</z-select-item>
|
||||||
<div z-form-control [errorMessage]="error('prenom') || ''"><input z-input formControlName="prenom" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Téléphone</label>
|
|
||||||
<div z-form-control [errorMessage]="error('phone') || ''"><input z-input formControlName="phone" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Limite inférieure</label>
|
|
||||||
<div z-form-control><input z-input type="number" formControlName="limiteInferieure" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Limite supérieure</label>
|
|
||||||
<div z-form-control><input z-input type="number" formControlName="limiteSuperieure" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Limite / transaction</label>
|
|
||||||
<div z-form-control><input z-input type="number" formControlName="limiteParTransaction" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Min Airtime</label>
|
|
||||||
<div z-form-control><input z-input type="number" formControlName="limiteMinAirtime" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Max Airtime</label>
|
|
||||||
<div z-form-control><input z-input type="number" formControlName="limiteMaxAirtime" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Nbre max. périphériques</label>
|
|
||||||
<div z-form-control><input z-input type="number" formControlName="maxPeripheriques" /></div>
|
|
||||||
</z-form-field>
|
|
||||||
|
|
||||||
<z-form-field>
|
|
||||||
<label z-form-label>Groupe de limites</label>
|
|
||||||
<div z-form-control [errorMessage]="error('limitId') || ''">
|
|
||||||
<z-select formControlName="limitId" [zPlaceholder]="'Sélectionner...'">
|
|
||||||
@for (l of limits; track l.id) {
|
|
||||||
<z-select-item [zValue]="l.id">{{ l.nom }}</z-select-item>
|
|
||||||
}
|
}
|
||||||
</z-select>
|
</z-select>
|
||||||
</div>
|
</div>
|
||||||
</z-form-field>
|
</z-form-field>
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Profil caisse</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('profil') || ''">
|
||||||
|
<input z-input formControlName="caisseProfile"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Princial</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('principalCode') || ''">
|
||||||
|
<input z-input formControlName="principalCode" placeholder="12242" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Nom</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('nom') || ''">
|
||||||
|
<input z-input formControlName="nom" placeholder="Nom" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Prénom</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('prenom') || ''">
|
||||||
|
<input z-input formControlName="prenom" placeholder="Prénom" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Autres noms</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="autresNoms" placeholder="Autres noms" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Statut</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('statut') || ''">
|
||||||
|
<z-select formControlName="statut">
|
||||||
|
@for (s of statutOptions; track s) {
|
||||||
|
<z-select-item [zValue]="s.value">{{ s.label }}</z-select-item>
|
||||||
|
}
|
||||||
|
</z-select>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Date d'embauche</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="date" formControlName="dateEmbauche"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Fonction</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="fonction" placeholder="Fonction" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end gap-2 pt-4">
|
<!-- SECTION CONTACT -->
|
||||||
<z-button zType="destructive" type="button" (click)="cancel.emit()">Annuler</z-button>
|
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow space-y-4 border border-gray-200 dark:border-gray-700">
|
||||||
<z-button type="submit">Enregistrer</z-button>
|
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Contact & Identité</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Phone</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('phone') || ''">
|
||||||
|
<input z-input formControlName="phone" placeholder="Numéro de téléphone" />
|
||||||
</div>
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>PIN</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="password" formControlName="pin" placeholder="PIN" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Adresse</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="adresse" placeholder="Adresse" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Ville</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="ville" placeholder="Ville" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Lieu de naissance</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="lieuNaissance" placeholder="Lieu de naissance" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Date de naissance</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="date" formControlName="dateNaissance"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SECTION LIMITE & PERIPHERIQUES -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow space-y-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Limites & Périphériques</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Zone</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="zone"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Kiosk</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="kiosk"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Limite inférieure</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="number" formControlName="limiteInferieure"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Limite supérieure</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="number" formControlName="limiteSuperieure"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Limite par transaction</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="number" formControlName="limiteParTransaction"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Limite Airtime (min)</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="number" formControlName="limiteMinAirtime"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Limite Airtime (max)</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="number" formControlName="limiteMaxAirtime"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Nombre max périphériques</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="number" formControlName="maxPeripheriques"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SECTION INFORMATIONS LEGALES -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow space-y-4 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Informations légales</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Nationalité</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="nationalite" placeholder="Nationalité" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>CNI</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="cni" placeholder="Numéro CNI" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>CNI délivrée le</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input type="date" formControlName="cniDelivreeLe"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>CNI délivrée à</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="cniDelivreeA" placeholder="Lieu de délivrance"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Résidence</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="residence" placeholder="Résidence"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Autre adresse</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="autreAdresse1" placeholder="Résidence"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Statut marital</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="statutMarital" placeholder="Statut marital"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Époux / Épouse</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="epoux" placeholder="Nom époux / épouse"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Autre téléphone</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input z-input formControlName="autreTelephone" placeholder="Autre numéro"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Autoriser aides</label>
|
||||||
|
<div z-form-control>
|
||||||
|
<input type="checkbox" formControlName="autoriserAides"/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BOUTONS SUBMIT -->
|
||||||
|
<div class="flex justify-end space-x-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 rounded-lg bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-400 dark:hover:bg-gray-600"
|
||||||
|
(click)="onClose()"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import { ZardButtonComponent } from '@shared/components/button/button.component'
|
|||||||
import { Agent, AgentStatus } from 'src/app/core/interfaces/agent';
|
import { Agent, AgentStatus } from 'src/app/core/interfaces/agent';
|
||||||
import { AgentLimit } from 'src/app/core/interfaces/agent-limit';
|
import { AgentLimit } from 'src/app/core/interfaces/agent-limit';
|
||||||
import { AgentLimitService } from 'src/app/core/services/agent-limit';
|
import { AgentLimitService } from 'src/app/core/services/agent-limit';
|
||||||
|
import { ZardCheckboxComponent } from "@shared/components/checkbox/checkbox.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-agent-form',
|
selector: 'app-agent-form',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './agent-form.html',
|
templateUrl: './agent-form.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule, ReactiveFormsModule, ZardFormModule, ZardInputDirective, ZardSelectComponent, ZardSelectItemComponent, ZardButtonComponent],
|
imports: [CommonModule, ReactiveFormsModule, ZardFormModule, ZardInputDirective, ZardSelectComponent, ZardSelectItemComponent],
|
||||||
})
|
})
|
||||||
export class AgentForm {
|
export class AgentForm {
|
||||||
@Output() save = new EventEmitter<Agent>();
|
@Output() save = new EventEmitter<Agent>();
|
||||||
@@ -23,6 +24,33 @@ export class AgentForm {
|
|||||||
|
|
||||||
limits: AgentLimit[] = [];
|
limits: AgentLimit[] = [];
|
||||||
|
|
||||||
|
profils = [
|
||||||
|
{
|
||||||
|
label: "Caissier",
|
||||||
|
value: "CAISSIER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Agent",
|
||||||
|
value: "AGENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Superviseur",
|
||||||
|
value: "SUPERVISEUR"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
statutOptions = [
|
||||||
|
{
|
||||||
|
label: "Actif",
|
||||||
|
value: "ACTIF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Inactif",
|
||||||
|
value: "INACTIF"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
private _value?: Agent;
|
private _value?: Agent;
|
||||||
@Input() set value(v: Agent | undefined) { this._value = v; this.hydrateFromValue(v); }
|
@Input() set value(v: Agent | undefined) { this._value = v; this.hydrateFromValue(v); }
|
||||||
get value(): Agent | undefined { return this._value; }
|
get value(): Agent | undefined { return this._value; }
|
||||||
@@ -33,25 +61,46 @@ export class AgentForm {
|
|||||||
constructor(private fb: FormBuilder, private limitService: AgentLimitService) {
|
constructor(private fb: FormBuilder, private limitService: AgentLimitService) {
|
||||||
this.form = this.fb.group({
|
this.form = this.fb.group({
|
||||||
code: ['', Validators.required],
|
code: ['', Validators.required],
|
||||||
profile: ['', Validators.required],
|
profil: ['CAISSIER', Validators.required],
|
||||||
statut: ['ACTIF' as AgentStatus, Validators.required],
|
principalCode: ['', Validators.required],
|
||||||
|
caisseProfile: ['', Validators.required],
|
||||||
|
statut: ['ACTIF', Validators.required],
|
||||||
zone: [''],
|
zone: [''],
|
||||||
kiosk: [''],
|
kiosk: [''],
|
||||||
fonction: [''],
|
fonction: [''],
|
||||||
dateEmbauche: [''],
|
dateEmbauche: [''],
|
||||||
|
|
||||||
nom: ['', Validators.required],
|
nom: ['', Validators.required],
|
||||||
prenom: ['', Validators.required],
|
prenom: ['', Validators.required],
|
||||||
phone: ['', [Validators.required, Validators.minLength(6)]],
|
autresNoms: [''],
|
||||||
|
dateNaissance: [''],
|
||||||
|
lieuNaissance: [''],
|
||||||
|
ville: [''],
|
||||||
|
adresse: [''],
|
||||||
|
autoriserAides: [false],
|
||||||
|
|
||||||
limiteInferieure: [0, [Validators.min(0)]],
|
phone: ['', Validators.required],
|
||||||
limiteSuperieure: [0, [Validators.min(0)]],
|
pin: [''],
|
||||||
limiteParTransaction: [0, [Validators.min(0)]],
|
|
||||||
limiteMinAirtime: [0, [Validators.min(0)]],
|
limiteInferieure: [''],
|
||||||
limiteMaxAirtime: [0, [Validators.min(0)]],
|
limiteSuperieure: [''],
|
||||||
maxPeripheriques: [0, [Validators.min(0)]],
|
limiteParTransaction: [''],
|
||||||
|
limiteMinAirtime: [''],
|
||||||
|
limiteMaxAirtime: [''],
|
||||||
|
|
||||||
|
maxPeripheriques: [''],
|
||||||
|
|
||||||
|
limitId: ['1'],
|
||||||
|
|
||||||
|
nationalite: [''],
|
||||||
|
cni: [''],
|
||||||
|
cniDelivreeLe: [''],
|
||||||
|
cniDelivreeA: [''],
|
||||||
|
residence: [''],
|
||||||
|
autreAdresse1: [''],
|
||||||
|
statutMarital: [''],
|
||||||
|
epoux: [''],
|
||||||
|
autreTelephone: [''],
|
||||||
|
|
||||||
limitId: ['', Validators.required],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.limitService
|
this.limitService
|
||||||
@@ -59,29 +108,123 @@ export class AgentForm {
|
|||||||
.subscribe((res) => (this.limits = res.content));
|
.subscribe((res) => (this.limits = res.content));
|
||||||
}
|
}
|
||||||
|
|
||||||
error(control: string): string {
|
errorMessage(control: string): string {
|
||||||
const e = this.form.get(control)?.errors; if (!e) return ''; if (e['required']) return 'Requis'; return '';
|
const e = this.form.get(control)?.errors; if (!e) return ''; if (e['required']) return 'Requis'; return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private hydrateFromValue(v?: Agent) {
|
private hydrateFromValue(v?: Agent) {
|
||||||
if (!v) {
|
if (!v) {
|
||||||
this.form.reset({
|
this.form.reset({
|
||||||
code: '', profile: '', statut: 'ACTIF', zone: '', kiosk: '', fonction: '', dateEmbauche: '', nom: '', prenom: '', phone: '', limiteInferieure: 0, limiteSuperieure: 0, limiteParTransaction: 0, limiteMinAirtime: 0, limiteMaxAirtime: 0, maxPeripheriques: 0, limitId: '',
|
statut: 'ACTIF',
|
||||||
|
autoriserAides: false,
|
||||||
});
|
});
|
||||||
|
this.submitted = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log(v);
|
||||||
this.form.reset({
|
this.form.reset({
|
||||||
code: v.code, profile: v.profile, statut: v.statut, zone: v.zone ?? '', kiosk: v.kiosk ?? '', fonction: v.fonction ?? '', dateEmbauche: v.dateEmbauche ?? '', nom: v.nom, prenom: v.prenom, phone: v.phone, limiteInferieure: v.limiteInferieure ?? 0, limiteSuperieure: v.limiteSuperieure ?? 0, limiteParTransaction: v.limiteParTransaction ?? 0, limiteMinAirtime: v.limiteMinAirtime ?? 0, limiteMaxAirtime: v.limiteMaxAirtime ?? 0, maxPeripheriques: v.maxPeripheriques ?? 0, limitId: v.limitId ?? '',
|
code: v.code || '',
|
||||||
|
profil: v.profil || '',
|
||||||
|
principalCode: v.principalCode || '',
|
||||||
|
caisseProfile: v.caisseProfile || '',
|
||||||
|
statut: v.statut || 'ACTIF',
|
||||||
|
zone: v.zone || '',
|
||||||
|
kiosk: v.kiosk || '',
|
||||||
|
fonction: v.fonction || '',
|
||||||
|
dateEmbauche: v.dateEmbauche || '',
|
||||||
|
|
||||||
|
nom: v.nom || '',
|
||||||
|
prenom: v.prenom || '',
|
||||||
|
autresNoms: v.autresNoms || '',
|
||||||
|
dateNaissance: v.dateNaissance || '',
|
||||||
|
lieuNaissance: v.lieuNaissance || '',
|
||||||
|
ville: v.ville || '',
|
||||||
|
adresse: v.adresse || '',
|
||||||
|
autoriserAides: v.autoriserAides ?? false,
|
||||||
|
|
||||||
|
phone: v.phone || '',
|
||||||
|
pin: v.pin || '',
|
||||||
|
|
||||||
|
limiteInferieure: v.limiteInferieure || '',
|
||||||
|
limiteSuperieure: v.limiteSuperieure || '',
|
||||||
|
limiteParTransaction: v.limiteParTransaction || '',
|
||||||
|
limiteMinAirtime: v.limiteMinAirtime || '',
|
||||||
|
limiteMaxAirtime: v.limiteMaxAirtime || '',
|
||||||
|
|
||||||
|
maxPeripheriques: v.maxPeripheriques || '',
|
||||||
|
|
||||||
|
limitId: v.limitId || '',
|
||||||
|
|
||||||
|
nationalite: v.nationalite || '',
|
||||||
|
cni: v.cni || '',
|
||||||
|
cniDelivreeLe: v.cniDelivreeLe || '',
|
||||||
|
cniDelivreeA: v.cniDelivreeA || '',
|
||||||
|
residence: v.residence || '',
|
||||||
|
autreAdresse1: v.autreAdresse1 || '',
|
||||||
|
statutMarital: v.statutMarital || '',
|
||||||
|
epoux: v.epoux || '',
|
||||||
|
autreTelephone: v.autreTelephone || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.submitted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.submitted = true;
|
this.submitted = true;
|
||||||
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
|
if (this.form.invalid) {
|
||||||
|
this.form.markAllAsTouched(); return;
|
||||||
|
}
|
||||||
const raw = this.form.getRawValue() as any;
|
const raw = this.form.getRawValue() as any;
|
||||||
const payload: Agent = { id: this.value?.id ?? '', code: raw.code, profile: raw.profile, statut: raw.statut, zone: raw.zone, kiosk: raw.kiosk, fonction: raw.fonction, dateEmbauche: raw.dateEmbauche, nom: raw.nom, prenom: raw.prenom, phone: raw.phone, limiteInferieure: +raw.limiteInferieure, limiteSuperieure: +raw.limiteSuperieure, limiteParTransaction: +raw.limiteParTransaction, limiteMinAirtime: +raw.limiteMinAirtime, limiteMaxAirtime: +raw.limiteMaxAirtime, maxPeripheriques: +raw.maxPeripheriques, limitId: raw.limitId };
|
const payload: Agent = {
|
||||||
|
id: this.value?.id ?? '',
|
||||||
|
code: raw.code,
|
||||||
|
profil: raw.profil,
|
||||||
|
statut: raw.statut,
|
||||||
|
principalCode: raw.principalCode || '',
|
||||||
|
caisseProfile: raw.caisseProfile || '',
|
||||||
|
zone: raw.zone || '',
|
||||||
|
kiosk: raw.kiosk || '',
|
||||||
|
fonction: raw.fonction || '',
|
||||||
|
dateEmbauche: raw.dateEmbauche || '',
|
||||||
|
|
||||||
|
nom: raw.nom,
|
||||||
|
prenom: raw.prenom,
|
||||||
|
autresNoms: raw.autresNoms || '',
|
||||||
|
dateNaissance: raw.dateNaissance || '',
|
||||||
|
lieuNaissance: raw.lieuNaissance || '',
|
||||||
|
ville: raw.ville || '',
|
||||||
|
adresse: raw.adresse || '',
|
||||||
|
autoriserAides: raw.autoriserAides ?? false,
|
||||||
|
|
||||||
|
phone: raw.phone,
|
||||||
|
pin: raw.pin || '',
|
||||||
|
|
||||||
|
limiteInferieure: +raw.limiteInferieure || 0,
|
||||||
|
limiteSuperieure: +raw.limiteSuperieure || 0,
|
||||||
|
limiteParTransaction: +raw.limiteParTransaction || 0,
|
||||||
|
limiteMinAirtime: +raw.limiteMinAirtime || 0,
|
||||||
|
limiteMaxAirtime: +raw.limiteMaxAirtime || 0,
|
||||||
|
|
||||||
|
maxPeripheriques: +raw.maxPeripheriques || 0,
|
||||||
|
limitId: raw.limitId || '1',
|
||||||
|
|
||||||
|
// Légales
|
||||||
|
nationalite: raw.nationalite || '',
|
||||||
|
cni: raw.cni || '',
|
||||||
|
cniDelivreeLe: raw.cniDelivreeLe || '',
|
||||||
|
cniDelivreeA: raw.cniDelivreeA || '',
|
||||||
|
residence: raw.residence || '',
|
||||||
|
autreAdresse1: raw.autreAdresse1 || '',
|
||||||
|
statutMarital: raw.statutMarital || '',
|
||||||
|
epoux: raw.epoux || '',
|
||||||
|
autreTelephone: raw.autreTelephone || ''
|
||||||
|
};
|
||||||
this.save.emit(payload);
|
this.save.emit(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClose(){
|
||||||
|
this.cancel.emit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ export class AgentFullForm {
|
|||||||
// Famille
|
// Famille
|
||||||
famille: this.fb.array([]),
|
famille: this.fb.array([]),
|
||||||
// TPE (stored as IDs in form, converted to full objects on submit)
|
// TPE (stored as IDs in form, converted to full objects on submit)
|
||||||
tpeIds: this.fb.array<string>([]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.limitService.list({ page: 1, perPage: 100, search: '', sortKey: 'nom', sortDir: 'asc' } as any).subscribe((res) => {
|
this.limitService.list({ page: 1, perPage: 100, search: '', sortKey: 'nom', sortDir: 'asc' } as any).subscribe((res) => {
|
||||||
@@ -226,8 +225,7 @@ export class AgentFullForm {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load assigned TPE IDs from tpes array
|
|
||||||
(v.tpes ?? []).forEach((tpe) => this.tpeArray.push(this.fb.control(tpe.id)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleTpe(id: string, checked: boolean) {
|
onToggleTpe(id: string, checked: boolean) {
|
||||||
@@ -257,15 +255,15 @@ export class AgentFullForm {
|
|||||||
// Only show VALIDE TPEs that are either not assigned or assigned to this agent
|
// Only show VALIDE TPEs that are either not assigned or assigned to this agent
|
||||||
const currentAgentId = this.value?.id;
|
const currentAgentId = this.value?.id;
|
||||||
this.tpeRows = res.content.filter((t) => {
|
this.tpeRows = res.content.filter((t) => {
|
||||||
if (t.statut !== 'VALIDE') return false;
|
if (t.statut !== 'ACTIF') return false;
|
||||||
// If TPE is assigned but to this agent, show it
|
// If TPE is assigned but to this agent, show it
|
||||||
if (t.assigne && currentAgentId) {
|
if (t.agentConnecteId && currentAgentId) {
|
||||||
// We need to check if this TPE is assigned to this agent
|
// We need to check if this TPE is assigned to this agent
|
||||||
// For now, show all VALIDE TPEs - the backend should handle assignment logic
|
// For now, show all VALIDE TPEs - the backend should handle assignment logic
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Show unassigned TPEs
|
// Show unassigned TPEs
|
||||||
return !t.assigne;
|
return !t.agentConnecteId;
|
||||||
});
|
});
|
||||||
this.tpeTotal = this.tpeRows.length;
|
this.tpeTotal = this.tpeRows.length;
|
||||||
});
|
});
|
||||||
@@ -275,7 +273,7 @@ export class AgentFullForm {
|
|||||||
const list = [...(this.tpeRows ?? []), ...(this.tpes ?? [])];
|
const list = [...(this.tpeRows ?? []), ...(this.tpes ?? [])];
|
||||||
const found = list.find((t) => t.id === id);
|
const found = list.find((t) => t.id === id);
|
||||||
if (found) {
|
if (found) {
|
||||||
return `${found.imei} (${found.marque} ${found.modele})`;
|
return `${found.numeroSerie}`;
|
||||||
}
|
}
|
||||||
// Try to load from service if not in current list
|
// Try to load from service if not in current list
|
||||||
return id;
|
return id;
|
||||||
@@ -314,21 +312,8 @@ export class AgentFullForm {
|
|||||||
}
|
}
|
||||||
const raw = this.form.getRawValue() as any;
|
const raw = this.form.getRawValue() as any;
|
||||||
|
|
||||||
// Convert TPE IDs to full TPE objects
|
// Convert TPE IDs to full TPE object
|
||||||
const tpeIds = [...this.tpeArray.value] as string[];
|
const tpes: TpeDevice[] = [];
|
||||||
const tpes: TpeDevice[] = tpeIds
|
|
||||||
.map((id) => {
|
|
||||||
// Try to find in tpeRows first (current modal list)
|
|
||||||
const found = this.tpeRows.find((t) => t.id === id);
|
|
||||||
if (found) return found;
|
|
||||||
// Try to find in existing tpes (from value)
|
|
||||||
const existing = this.value?.tpes?.find((t) => t.id === id);
|
|
||||||
if (existing) return existing;
|
|
||||||
// If not found, create a minimal TPE object (backend will fill in details)
|
|
||||||
// This shouldn't happen in normal flow, but handle gracefully
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((t): t is TpeDevice => t !== null);
|
|
||||||
|
|
||||||
// Prepare agent payload (without famille - family members are handled separately)
|
// Prepare agent payload (without famille - family members are handled separately)
|
||||||
const payload: Agent = {
|
const payload: Agent = {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
>
|
>
|
||||||
<z-select
|
<z-select
|
||||||
id="hippodromeId"
|
id="hippodromeId"
|
||||||
|
[zDisabled]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
placeholder="Rechercher une réunion..."
|
placeholder="Rechercher une réunion..."
|
||||||
formControlName="hippodromeId"
|
formControlName="hippodromeId"
|
||||||
[zLabel]="selectedHippodromeLabel() || ''"
|
[zLabel]="selectedHippodromeLabel() || ''"
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<input
|
<input
|
||||||
|
[readOnly]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
z-input
|
z-input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -63,6 +65,7 @@
|
|||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<input
|
<input
|
||||||
|
[readOnly]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
z-input
|
z-input
|
||||||
type="date"
|
type="date"
|
||||||
placeholder="Ex: 10/07/2025"
|
placeholder="Ex: 10/07/2025"
|
||||||
@@ -82,6 +85,7 @@
|
|||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<input
|
<input
|
||||||
|
[readOnly]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
z-input
|
z-input
|
||||||
placeholder="Ex: Plat"
|
placeholder="Ex: Plat"
|
||||||
formControlName="discipline"
|
formControlName="discipline"
|
||||||
@@ -101,6 +105,7 @@
|
|||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<input
|
<input
|
||||||
|
[readOnly]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
z-input
|
z-input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Ex: Grand Prix du Sahel"
|
placeholder="Ex: Grand Prix du Sahel"
|
||||||
@@ -145,6 +150,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center">
|
||||||
<input
|
<input
|
||||||
|
[readOnly]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
z-input
|
z-input
|
||||||
type="time"
|
type="time"
|
||||||
formControlName="heureDepartPrevue"
|
formControlName="heureDepartPrevue"
|
||||||
@@ -184,6 +190,7 @@
|
|||||||
@for (t of courseTypes; track t.value) {
|
@for (t of courseTypes; track t.value) {
|
||||||
<label class="flex items-center gap-2 text-sm">
|
<label class="flex items-center gap-2 text-sm">
|
||||||
<input
|
<input
|
||||||
|
[disabled]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
[value]="t.value"
|
[value]="t.value"
|
||||||
(change)="onToggleType($event)"
|
(change)="onToggleType($event)"
|
||||||
@@ -208,6 +215,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
[readOnly]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
z-input
|
z-input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -223,6 +231,7 @@
|
|||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<input
|
<input
|
||||||
|
[readOnly]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
z-input
|
z-input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -238,6 +247,7 @@
|
|||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<input
|
<input
|
||||||
|
[readOnly]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
z-input
|
z-input
|
||||||
placeholder="Ex: A"
|
placeholder="Ex: A"
|
||||||
formControlName="categorie"
|
formControlName="categorie"
|
||||||
@@ -267,7 +277,7 @@
|
|||||||
>Statut</label
|
>Statut</label
|
||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<z-select formControlName="statut" class="w-full">
|
<z-select [zDisabled]="updateDisabled()" formControlName="statut" class="w-full">
|
||||||
@for (s of courseStatus; track s.value) {
|
@for (s of courseStatus; track s.value) {
|
||||||
<z-select-item [zValue]="s.value">{{ s.label }}</z-select-item>
|
<z-select-item [zValue]="s.value">{{ s.label }}</z-select-item>
|
||||||
}
|
}
|
||||||
@@ -281,6 +291,7 @@
|
|||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<input
|
<input
|
||||||
|
[readOnly]="getCourseStatut()!==undefined && getCourseStatut() !=='BROUILLON'"
|
||||||
z-input
|
z-input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -314,6 +325,7 @@
|
|||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<input
|
<input
|
||||||
|
readOnly
|
||||||
z-input
|
z-input
|
||||||
formControlName="createdBy"
|
formControlName="createdBy"
|
||||||
readonly
|
readonly
|
||||||
@@ -328,6 +340,7 @@
|
|||||||
>
|
>
|
||||||
<z-form-control>
|
<z-form-control>
|
||||||
<input
|
<input
|
||||||
|
readOnly
|
||||||
z-input
|
z-input
|
||||||
formControlName="validatedBy"
|
formControlName="validatedBy"
|
||||||
readonly
|
readonly
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ export class CourseForm implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCourseStatut():string|undefined{
|
||||||
|
return this._value?.statut
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisabled():boolean{
|
||||||
|
const statut = this._value?.statut;
|
||||||
|
if(statut !== undefined){
|
||||||
|
if(statut === 'OUVERT' || statut === 'FERME'){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
form: FormGroup;
|
form: FormGroup;
|
||||||
submitted = false;
|
submitted = false;
|
||||||
|
|
||||||
|
|||||||
60
src/app/shared/forms/point-vente-form/point-vente-form.html
Normal file
60
src/app/shared/forms/point-vente-form/point-vente-form.html
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<form class="space-y-4" (ngSubmit)="onSubmit()" [formGroup]="form">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Nom</label>
|
||||||
|
<div z-form-control [errorMessage]="error('nom') || ''">
|
||||||
|
<input z-input formControlName="nom" placeholder="Nom du point de vente" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Ville</label>
|
||||||
|
<div z-form-control [errorMessage]="error('ville') || ''">
|
||||||
|
<input z-input formControlName="ville" placeholder="Ville" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field class="md:col-span-2">
|
||||||
|
<label z-form-label>Adresse</label>
|
||||||
|
<div z-form-control [errorMessage]="error('adresse') || ''">
|
||||||
|
<input z-input formControlName="adresse" placeholder="Adresse complète" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Latitude</label>
|
||||||
|
<div z-form-control [errorMessage]="error('latitude') || ''">
|
||||||
|
<input
|
||||||
|
z-input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
formControlName="latitude"
|
||||||
|
placeholder="0.000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Longitude</label>
|
||||||
|
<div z-form-control [errorMessage]="error('longitude') || ''">
|
||||||
|
<input
|
||||||
|
z-input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
formControlName="longitude"
|
||||||
|
placeholder="0.000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Statut</label>
|
||||||
|
<div z-form-control [errorMessage]="error('statut') || ''">
|
||||||
|
<z-select formControlName="statut">
|
||||||
|
<z-select-item zValue="ACTIVE">Actif</z-select-item>
|
||||||
|
<z-select-item zValue="INACTIVE">Inactif</z-select-item>
|
||||||
|
</z-select>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
115
src/app/shared/forms/point-vente-form/point-vente-form.ts
Normal file
115
src/app/shared/forms/point-vente-form/point-vente-form.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { ZardFormModule } from '@shared/components/form/form.module';
|
||||||
|
import { ZardInputDirective } from '@shared/components/input/input.directive';
|
||||||
|
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
||||||
|
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
||||||
|
import { PointVente, PointVenteStatut } from 'src/app/core/interfaces/points-ventes';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-point-vente-form',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './point-vente-form.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ZardFormModule,
|
||||||
|
ZardInputDirective,
|
||||||
|
ZardSelectComponent,
|
||||||
|
ZardSelectItemComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PointVenteForm {
|
||||||
|
@Output() save = new EventEmitter<Partial<PointVente>>();
|
||||||
|
@Output() cancel = new EventEmitter<void>();
|
||||||
|
|
||||||
|
private _value?: PointVente;
|
||||||
|
@Input() set value(v: PointVente | undefined) {
|
||||||
|
this._value = v;
|
||||||
|
this.hydrate(v);
|
||||||
|
}
|
||||||
|
get value(): PointVente | undefined {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
form: FormGroup;
|
||||||
|
submitted = false;
|
||||||
|
|
||||||
|
constructor(private fb: FormBuilder) {
|
||||||
|
this.form = this.fb.group({
|
||||||
|
nom: ['', Validators.required],
|
||||||
|
adresse: ['', Validators.required],
|
||||||
|
ville: ['', Validators.required],
|
||||||
|
latitude: [0, [Validators.required, Validators.min(-90), Validators.max(90)]],
|
||||||
|
longitude: [0, [Validators.required, Validators.min(-180), Validators.max(180)]],
|
||||||
|
statut: ['ACTIVE', Validators.required],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
error(control: string): string {
|
||||||
|
const c = this.form.get(control);
|
||||||
|
if (!c) return '';
|
||||||
|
const invalid = c.invalid && (c.touched || this.submitted);
|
||||||
|
if (!invalid) return '';
|
||||||
|
const e = c.errors || {};
|
||||||
|
if (e['required']) return 'Ce champ est obligatoire';
|
||||||
|
if (e['min']) return `Valeur minimale: ${e['min'].min}`;
|
||||||
|
if (e['max']) return `Valeur maximale: ${e['max'].max}`;
|
||||||
|
return 'Valeur invalide';
|
||||||
|
}
|
||||||
|
|
||||||
|
private hydrate(v?: PointVente) {
|
||||||
|
if (!v) {
|
||||||
|
this.form.reset({
|
||||||
|
nom: '',
|
||||||
|
adresse: '',
|
||||||
|
ville: '',
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
statut: 'ACTIVE',
|
||||||
|
});
|
||||||
|
this.submitted = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.reset({
|
||||||
|
nom: v.nom,
|
||||||
|
adresse: v.adresse,
|
||||||
|
ville: v.ville,
|
||||||
|
latitude: v.latitude ?? 0,
|
||||||
|
longitude: v.longitude ?? 0,
|
||||||
|
statut: v.statut ?? 'ACTIVE',
|
||||||
|
});
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
this.submitted = true;
|
||||||
|
if (this.form.invalid) {
|
||||||
|
this.form.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = this.form.getRawValue() as any;
|
||||||
|
const payload: Partial<PointVente> = {
|
||||||
|
...raw,
|
||||||
|
latitude: Number(raw.latitude),
|
||||||
|
longitude: Number(raw.longitude),
|
||||||
|
...(this.value?.id ? { id: this.value.id } : {}),
|
||||||
|
};
|
||||||
|
this.save.emit(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this._value = undefined;
|
||||||
|
this.form.reset({
|
||||||
|
nom: '',
|
||||||
|
adresse: '',
|
||||||
|
ville: '',
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
statut: 'ACTIVE',
|
||||||
|
});
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<form class="space-y-4" (ngSubmit)="onSubmit()" [formGroup]="form">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Nom</label>
|
||||||
|
<div z-form-control [errorMessage]="error('nom') || ''">
|
||||||
|
<input z-input formControlName="nom" placeholder="Nom du point de vente" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Ville</label>
|
||||||
|
<div z-form-control [errorMessage]="error('ville') || ''">
|
||||||
|
<input z-input formControlName="ville" placeholder="Ville" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field >
|
||||||
|
<label z-form-label>Adresse</label>
|
||||||
|
<div z-form-control [errorMessage]="error('adresse') || ''">
|
||||||
|
<input z-input formControlName="adresse" placeholder="Adresse complète" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Code</label>
|
||||||
|
<div z-form-control [errorMessage]="error('code') || ''">
|
||||||
|
<input z-input formControlName="code" placeholder="Code du point de vente" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Latitude</label>
|
||||||
|
<div z-form-control [errorMessage]="error('latitude') || ''">
|
||||||
|
<input
|
||||||
|
z-input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
formControlName="latitude"
|
||||||
|
placeholder="0.000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Longitude</label>
|
||||||
|
<div z-form-control [errorMessage]="error('longitude') || ''">
|
||||||
|
<input
|
||||||
|
z-input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
formControlName="longitude"
|
||||||
|
placeholder="0.000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Statut</label>
|
||||||
|
<div z-form-control [errorMessage]="error('statut') || ''">
|
||||||
|
<z-select formControlName="statut">
|
||||||
|
<z-select-item *ngFor="let statut of pointsVenteStatuts" [zValue]="statut.value">{{ statut.label }}</z-select-item>
|
||||||
|
</z-select>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PointsVenteForm } from './points-vente-form';
|
||||||
|
|
||||||
|
describe('PointsVenteForm', () => {
|
||||||
|
let component: PointsVenteForm;
|
||||||
|
let fixture: ComponentFixture<PointsVenteForm>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PointsVenteForm]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PointsVenteForm);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
126
src/app/shared/forms/points-vente-form/points-vente-form.ts
Normal file
126
src/app/shared/forms/points-vente-form/points-vente-form.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { ZardFormModule } from '@shared/components/form/form.module';
|
||||||
|
import { ZardInputDirective } from '@shared/components/input/input.directive';
|
||||||
|
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
||||||
|
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
||||||
|
import { PointVente, PointVenteStatut } from 'src/app/core/interfaces/points-ventes';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-points-vente-form',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './points-vente-form.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ZardFormModule,
|
||||||
|
ZardInputDirective,
|
||||||
|
ZardSelectComponent,
|
||||||
|
ZardSelectItemComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PointsVenteForm {
|
||||||
|
@Output() save = new EventEmitter<Partial<PointVente>>();
|
||||||
|
@Output() cancel = new EventEmitter<void>();
|
||||||
|
|
||||||
|
pointsVenteStatuts = [{
|
||||||
|
label: 'Actif',
|
||||||
|
value: 'ACTIF'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Inactif',
|
||||||
|
value: 'INACTIF'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
private _value?: PointVente;
|
||||||
|
@Input() set value(v: PointVente | undefined) {
|
||||||
|
this._value = v;
|
||||||
|
this.hydrate(v);
|
||||||
|
}
|
||||||
|
get value(): PointVente | undefined {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
form: FormGroup;
|
||||||
|
submitted = false;
|
||||||
|
|
||||||
|
constructor(private fb: FormBuilder) {
|
||||||
|
this.form = this.fb.group({
|
||||||
|
nom: ['', Validators.required],
|
||||||
|
adresse: ['', Validators.required],
|
||||||
|
code: ['', Validators.required],
|
||||||
|
ville: ['', Validators.required],
|
||||||
|
latitude: [0, [Validators.required, Validators.min(-90), Validators.max(90)]],
|
||||||
|
longitude: [0, [Validators.required, Validators.min(-180), Validators.max(180)]],
|
||||||
|
statut: ['', Validators.required],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
error(control: string): string {
|
||||||
|
const c = this.form.get(control);
|
||||||
|
if (!c) return '';
|
||||||
|
const invalid = c.invalid && (c.touched || this.submitted);
|
||||||
|
if (!invalid) return '';
|
||||||
|
const e = c.errors || {};
|
||||||
|
if (e['required']) return 'Ce champ est obligatoire';
|
||||||
|
if (e['min']) return `Valeur minimale: ${e['min'].min}`;
|
||||||
|
if (e['max']) return `Valeur maximale: ${e['max'].max}`;
|
||||||
|
return 'Valeur invalide';
|
||||||
|
}
|
||||||
|
|
||||||
|
private hydrate(v?: PointVente) {
|
||||||
|
if (!v) {
|
||||||
|
this.form.reset({
|
||||||
|
nom: '',
|
||||||
|
adresse: '',
|
||||||
|
ville: '',
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
statut: 'ACTIF',
|
||||||
|
});
|
||||||
|
this.submitted = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.form.reset({
|
||||||
|
nom: v.nom,
|
||||||
|
adresse: v.adresse,
|
||||||
|
ville: v.ville,
|
||||||
|
latitude: v.latitude ?? 0,
|
||||||
|
longitude: v.longitude ?? 0,
|
||||||
|
statut: v.statut ?? 'ACTIF',
|
||||||
|
});
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
this.submitted = true;
|
||||||
|
if (this.form.invalid) {
|
||||||
|
this.form.markAllAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = this.form.getRawValue() as any;
|
||||||
|
const payload: Partial<PointVente> = {
|
||||||
|
...raw,
|
||||||
|
latitude: Number(raw.latitude),
|
||||||
|
longitude: Number(raw.longitude),
|
||||||
|
...(this.value?.id ? { id: this.value.id } : {}),
|
||||||
|
};
|
||||||
|
this.save.emit(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this._value = undefined;
|
||||||
|
this.form.reset({
|
||||||
|
nom: '',
|
||||||
|
adresse: '',
|
||||||
|
ville: '',
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
statut: 'ACTIVE',
|
||||||
|
});
|
||||||
|
this.submitted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,19 +21,18 @@
|
|||||||
[disabled]="!canSave()">
|
[disabled]="!canSave()">
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</button>
|
</button>
|
||||||
<button
|
<!-- <button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex-1 px-3 py-2 text-sm rounded border border-blue-300 bg-blue-50 text-blue-700 hover:bg-blue-100 disabled:opacity-50"
|
class="flex-1 px-3 py-2 text-sm rounded border border-blue-300 bg-blue-50 text-blue-700 hover:bg-blue-100 disabled:opacity-50"
|
||||||
(click)="validate.emit()"
|
(click)="validate.emit()"
|
||||||
[disabled]="!canValidate()">
|
[disabled]="!canValidate()">
|
||||||
Valider
|
Valider
|
||||||
</button>
|
</button> -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex-1 px-3 py-2 text-sm rounded border border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50"
|
class="flex-1 px-3 py-2 text-sm rounded border border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 disabled:opacity-50"
|
||||||
(click)="confirm.emit()"
|
(click)="confirm.emit()"
|
||||||
[disabled]="!canConfirm()"
|
[disabled]="!canConfirm()">
|
||||||
>
|
|
||||||
Confirmer
|
Confirmer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ZardFormModule } from '@shared/components/form/form.module';
|
|||||||
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
||||||
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
||||||
import { Course, CourseType } from 'src/app/core/interfaces/course';
|
import { Course, CourseType } from 'src/app/core/interfaces/course';
|
||||||
import { Resultat } from 'src/app/core/interfaces/resultat';
|
import { Resultat, ResultatStatut } from 'src/app/core/interfaces/resultat';
|
||||||
|
|
||||||
type PlaceRow = { picks: FormArray<FormControl<number | null>> };
|
type PlaceRow = { picks: FormArray<FormControl<number | null>> };
|
||||||
type ResultatShape = { places: FormArray<FormGroup<PlaceRow>> };
|
type ResultatShape = { places: FormArray<FormGroup<PlaceRow>> };
|
||||||
@@ -65,12 +65,12 @@ export class ResultatForm {
|
|||||||
return this.resultat ? 'PROVISOIRE' : 'EN_ATTENTE';
|
return this.resultat ? 'PROVISOIRE' : 'EN_ATTENTE';
|
||||||
});
|
});
|
||||||
|
|
||||||
canValidate(): boolean {
|
// canValidate(): boolean {
|
||||||
return this.statut() === 'EN_ATTENTE';
|
// return this.statut() === 'PROVISOIRE';
|
||||||
}
|
// }
|
||||||
|
|
||||||
canConfirm(): boolean {
|
canConfirm(): boolean {
|
||||||
return this.statut() === 'PROVISOIRE';
|
return this.statut() === 'PROVISOIRE' || this.statut() === 'OFFICIEL';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods for template
|
// Helper methods for template
|
||||||
@@ -228,6 +228,7 @@ export class ResultatForm {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
|
||||||
this.seed();
|
this.seed();
|
||||||
// Watch for changes to auto-populate places when ex-aequo is detected
|
// Watch for changes to auto-populate places when ex-aequo is detected
|
||||||
this.setupAutoPopulate();
|
this.setupAutoPopulate();
|
||||||
@@ -549,6 +550,10 @@ export class ResultatForm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(this.statut()==='OFFICIEL'){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,109 @@
|
|||||||
<form class="space-y-4" (ngSubmit)="onSubmit()" [formGroup]="form">
|
<form class="space-y-4" [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|
||||||
<z-form-field>
|
<z-form-field>
|
||||||
<label z-form-label>IMEI</label>
|
<label z-form-label>Numéro de série</label>
|
||||||
<div z-form-control [errorMessage]="errorMessage('imei') || ''">
|
<div z-form-control [errorMessage]="errorMessage('numeroSerie') || ''">
|
||||||
<input z-input formControlName="imei" placeholder="IMEI" />
|
<input z-input formControlName="numeroSerie" placeholder="Numéro de série" />
|
||||||
</div>
|
</div>
|
||||||
</z-form-field>
|
</z-form-field>
|
||||||
|
|
||||||
<z-form-field>
|
<z-form-field>
|
||||||
<label z-form-label>N° de Série</label>
|
<label z-form-label>Type de terminal</label>
|
||||||
<div z-form-control [errorMessage]="errorMessage('serial') || ''">
|
<div z-form-control [errorMessage]="errorMessage('typeTerminal') || ''">
|
||||||
<input z-input formControlName="serial" placeholder="Numéro de série" />
|
<input z-input formControlName="typeTerminal" zPlaceholder="Sélectionner..."/>
|
||||||
</div>
|
</div>
|
||||||
</z-form-field>
|
</z-form-field>
|
||||||
|
|
||||||
<z-form-field>
|
<z-form-field>
|
||||||
<label z-form-label>Type</label>
|
<label z-form-label>Plateforme</label>
|
||||||
<div z-form-control [errorMessage]="errorMessage('type') || ''">
|
<div z-form-control [errorMessage]="errorMessage('plateforme') || ''">
|
||||||
<z-select formControlName="type" [zPlaceholder]="'Sélectionner...'">
|
<input z-input formControlName="plateforme"/>
|
||||||
@for (t of types; track t.value) {
|
</div>
|
||||||
<z-select-item [zValue]="t.value">{{ t.label }}</z-select-item>
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Modèle appareil</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('modeleAppareil') || ''">
|
||||||
|
<input z-input formControlName="modeleAppareil" placeholder="Modèle" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Système d’exploitation</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('systemeExploitation') || ''">
|
||||||
|
<z-select formControlName="systemeExploitation">
|
||||||
|
@for (os of osList; track os) {
|
||||||
|
<z-select-item [zValue]="os">{{ os }}</z-select-item>
|
||||||
}
|
}
|
||||||
</z-select>
|
</z-select>
|
||||||
</div>
|
</div>
|
||||||
</z-form-field>
|
</z-form-field>
|
||||||
|
|
||||||
<z-form-field>
|
<z-form-field>
|
||||||
<label z-form-label>Marque</label>
|
<label z-form-label>Version OS</label>
|
||||||
<div z-form-control [errorMessage]="errorMessage('marque') || ''">
|
<div z-form-control [errorMessage]="errorMessage('versionOs') || ''">
|
||||||
<input z-input formControlName="marque" placeholder="Marque" />
|
<input z-input formControlName="versionOs" placeholder="Ex: 12.0" />
|
||||||
</div>
|
</div>
|
||||||
</z-form-field>
|
</z-form-field>
|
||||||
|
|
||||||
<z-form-field>
|
<z-form-field>
|
||||||
<label z-form-label>Modèle</label>
|
<label z-form-label>Version logicielle</label>
|
||||||
<div z-form-control [errorMessage]="errorMessage('modele') || ''">
|
<div z-form-control [errorMessage]="errorMessage('versionLogicielle') || ''">
|
||||||
<input z-input formControlName="modele" placeholder="Modèle" />
|
<input z-input formControlName="versionLogicielle" placeholder="Ex: v3.2.1" />
|
||||||
</div>
|
</div>
|
||||||
</z-form-field>
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Adresse IP</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('adresseIp') || ''">
|
||||||
|
<input z-input formControlName="adresseIp" placeholder="192.168.1.10" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Adresse MAC</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('adresseMac') || ''">
|
||||||
|
<input z-input formControlName="adresseMac" placeholder="AA:BB:CC:DD:EE:FF" />
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Point de vente</label>
|
||||||
|
<input [value]="pointDeVenteText()" (blur)="onClose()" (input)="pointDeVenteTextChange($event)" z-input placeholder="Entrez le nom du point de vente"/>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('pointDeVenteId') || ''">
|
||||||
|
@if (pointDeVenteLoading()){
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
class="w-6 h-6 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
@if (pointsDevente.length > 0 && !pointDeVenteLoading()) {
|
||||||
|
<ul class="mt-1 border rounded-lg bg-white dark:bg-indigo-950 shadow max-h-60 overflow-auto">
|
||||||
|
@for (pdv of pointsDevente; track pdv.id) {
|
||||||
|
<li
|
||||||
|
class="px-3 py-2 cursor-pointer hover:bg-blue-500"
|
||||||
|
(mousedown)="selectPointDeVente(pdv)">
|
||||||
|
{{ pdv.nom }} / {{ pdv.code }}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
|
<z-form-field>
|
||||||
|
<label z-form-label>Statut</label>
|
||||||
|
<div z-form-control [errorMessage]="errorMessage('statut') || ''">
|
||||||
|
<z-select formControlName="statut">
|
||||||
|
@for (s of statuts; track s) {
|
||||||
|
<z-select-item [zValue]="s.value">{{ s.label }}</z-select-item>
|
||||||
|
}
|
||||||
|
</z-select>
|
||||||
|
</div>
|
||||||
|
</z-form-field>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, effect, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { ZardFormModule } from '@shared/components/form/form.module';
|
import { ZardFormModule } from '@shared/components/form/form.module';
|
||||||
import { ZardInputDirective } from '@shared/components/input/input.directive';
|
import { ZardInputDirective } from '@shared/components/input/input.directive';
|
||||||
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
||||||
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
||||||
import { TpeDevice, TpeType } from 'src/app/core/interfaces/tpe';
|
import { TpeDevice, TpeType } from 'src/app/core/interfaces/tpe';
|
||||||
|
import { PointsVenteService } from 'src/app/core/services/points-vente';
|
||||||
|
import { ListParams } from '@shared/paging/paging';
|
||||||
|
import { PointVente } from 'src/app/core/interfaces/points-ventes';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tpe-form',
|
selector: 'app-tpe-form',
|
||||||
@@ -27,6 +30,13 @@ export class TpeForm {
|
|||||||
|
|
||||||
private _value?: TpeDevice;
|
private _value?: TpeDevice;
|
||||||
private _skipHydration = false;
|
private _skipHydration = false;
|
||||||
|
|
||||||
|
|
||||||
|
pointDeVenteText = signal('');
|
||||||
|
|
||||||
|
pointDeVenteLoading = signal<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
@Input() set value(v: TpeDevice | undefined) {
|
@Input() set value(v: TpeDevice | undefined) {
|
||||||
this._value = v;
|
this._value = v;
|
||||||
if (!this._skipHydration) {
|
if (!this._skipHydration) {
|
||||||
@@ -37,19 +47,87 @@ export class TpeForm {
|
|||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
osList = ['Windows', 'Linux', 'Mac Os'];
|
||||||
|
|
||||||
|
pointsDevente:PointVente[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
statuts = [
|
||||||
|
{
|
||||||
|
label: 'Actif',
|
||||||
|
value: 'ACTIF'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hors service',
|
||||||
|
value: 'HORS_SERVICE'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
form: FormGroup;
|
form: FormGroup;
|
||||||
submitted = false;
|
submitted = false;
|
||||||
|
|
||||||
constructor(private fb: FormBuilder) {
|
constructor(private fb: FormBuilder, private pointsVenteService: PointsVenteService) {
|
||||||
this.form = this.fb.group({
|
this.form = this.fb.group({
|
||||||
imei: ['', [Validators.required, Validators.minLength(10)]],
|
numeroSerie: ['', [Validators.required, Validators.minLength(10)]],
|
||||||
serial: ['', Validators.required],
|
pointDeVenteId: ['', Validators.required],
|
||||||
type: ['POS' as TpeType, Validators.required],
|
statut: ['', Validators.required],
|
||||||
marque: ['', Validators.required],
|
versionLogicielle: ['', Validators.required],
|
||||||
modele: ['', Validators.required],
|
typeTerminal: ['', Validators.required],
|
||||||
|
plateforme: ['', Validators.required],
|
||||||
|
modeleAppareil: ['', Validators.required],
|
||||||
|
systemeExploitation: ['', Validators.required],
|
||||||
|
versionOs: ['', Validators.required],
|
||||||
|
adresseIp: ['', Validators.required],
|
||||||
|
adresseMac: ['', Validators.required],
|
||||||
|
agentConnecteId: ['1'],
|
||||||
|
journalSession: ['1'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pointDeVenteTextChange =(event: Event)=>{
|
||||||
|
const value = (event.target as HTMLInputElement).value;
|
||||||
|
this.pointDeVenteText.set(value);
|
||||||
|
const text = this.pointDeVenteText();
|
||||||
|
const params: ListParams = {
|
||||||
|
page: 0,
|
||||||
|
size: 10,
|
||||||
|
search: text
|
||||||
|
}
|
||||||
|
this.getPointDeventeFromText(text, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPointDeventeFromText=(text: string, params: ListParams)=>{
|
||||||
|
if(text.length < 2) return;
|
||||||
|
this.pointDeVenteLoading.set(true);
|
||||||
|
this.pointsVenteService.list(params).subscribe({
|
||||||
|
next: res =>{
|
||||||
|
this.pointsDevente = res.content;
|
||||||
|
this.pointDeVenteLoading.set(false);
|
||||||
|
},
|
||||||
|
error: err =>{
|
||||||
|
this.pointDeVenteLoading.set(false);
|
||||||
|
return console.error(err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
selectPointDeVente =(pvt:PointVente):void=>{
|
||||||
|
this.form.patchValue({
|
||||||
|
pointDeVenteId: pvt.id
|
||||||
|
})
|
||||||
|
this.pointsDevente = [];
|
||||||
|
this.pointDeVenteText.set(`${pvt.nom}/${pvt.code} `)
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose(){
|
||||||
|
this.pointsDevente = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
isInvalid(control: string): boolean {
|
isInvalid(control: string): boolean {
|
||||||
const c = this.form.get(control);
|
const c = this.form.get(control);
|
||||||
return !!(c && c.invalid && (c.touched || this.submitted));
|
return !!(c && c.invalid && (c.touched || this.submitted));
|
||||||
@@ -65,21 +143,37 @@ export class TpeForm {
|
|||||||
private hydrateFromValue(v?: TpeDevice) {
|
private hydrateFromValue(v?: TpeDevice) {
|
||||||
if (!v) {
|
if (!v) {
|
||||||
this.form.reset({
|
this.form.reset({
|
||||||
imei: '',
|
numeroSerie: '',
|
||||||
serial: '',
|
pointDeVenteId: 0,
|
||||||
type: 'POS',
|
statut: 'ACTIF',
|
||||||
marque: '',
|
versionLogicielle: '',
|
||||||
modele: '',
|
typeTerminal: '',
|
||||||
|
plateforme: '',
|
||||||
|
modeleAppareil: '',
|
||||||
|
systemeExploitation: '',
|
||||||
|
versionOs: '',
|
||||||
|
adresseIp: '',
|
||||||
|
adresseMac: '',
|
||||||
|
agentConnecteId: '',
|
||||||
|
journalSession: '',
|
||||||
});
|
});
|
||||||
this.submitted = false; // Reset submitted flag when form is cleared
|
this.submitted = false; // Reset submitted flag when form is cleared
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.form.reset({
|
this.form.reset({
|
||||||
imei: v.imei,
|
numeroSerie: v.numeroSerie,
|
||||||
serial: v.serial,
|
pointDeVenteId: v.pointDeVenteId,
|
||||||
type: v.type,
|
statut: 'ACTIF',
|
||||||
marque: v.marque,
|
versionLogicielle: v.versionLogicielle,
|
||||||
modele: v.modele,
|
typeTerminal: v.typeTerminal,
|
||||||
|
plateforme: v.plateforme,
|
||||||
|
modeleAppareil: v.modeleAppareil,
|
||||||
|
systemeExploitation: v.systemeExploitation,
|
||||||
|
versionOs: v.versionOs,
|
||||||
|
adresseIp: v.adresseIp,
|
||||||
|
adresseMac: v.adresseMac,
|
||||||
|
agentConnecteId: v.agentConnecteId,
|
||||||
|
journalSession: v.journalSession,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,18 +183,16 @@ export class TpeForm {
|
|||||||
this.form.markAllAsTouched();
|
this.form.markAllAsTouched();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const raw = this.form.getRawValue() as any;
|
const raw = this.form.getRawValue() as Partial<TpeDevice>;
|
||||||
const payload: Partial<TpeDevice> = {
|
const payload: Partial<TpeDevice> = {
|
||||||
imei: raw.imei,
|
...raw
|
||||||
serial: raw.serial,
|
|
||||||
type: raw.type,
|
|
||||||
marque: raw.marque,
|
|
||||||
modele: raw.modele,
|
|
||||||
};
|
};
|
||||||
// Preserve existing id, statut, and assigne if editing
|
// Preserve existing id, statut, and assigne if editing
|
||||||
if (this.value?.id) {
|
if (this.value?.id) {
|
||||||
payload.id = this.value.id;
|
payload.id = this.value.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.save.emit(payload as TpeDevice);
|
this.save.emit(payload as TpeDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,11 +200,19 @@ export class TpeForm {
|
|||||||
this._skipHydration = true; // Prevent hydration when clearing value
|
this._skipHydration = true; // Prevent hydration when clearing value
|
||||||
this._value = undefined;
|
this._value = undefined;
|
||||||
this.form.reset({
|
this.form.reset({
|
||||||
imei: '',
|
numeroSerie: '',
|
||||||
serial: '',
|
pointDeVenteId: 0,
|
||||||
type: 'POS',
|
statut: 'ACTIF',
|
||||||
marque: '',
|
versionLogicielle: '',
|
||||||
modele: '',
|
typeTerminal: '',
|
||||||
|
plateforme: '',
|
||||||
|
modeleAppareil: '',
|
||||||
|
systemeExploitation: '',
|
||||||
|
versionOs: '',
|
||||||
|
adresseIp: '',
|
||||||
|
adresseMac: '',
|
||||||
|
agentConnecteId: '',
|
||||||
|
journalSession: '',
|
||||||
});
|
});
|
||||||
this.submitted = false;
|
this.submitted = false;
|
||||||
this._skipHydration = false; // Re-enable hydration
|
this._skipHydration = false; // Re-enable hydration
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiBaseUrl: 'http://192.168.1.235:8280',
|
apiBaseUrl: 'http://192.168.1.235:8381',
|
||||||
|
depouillementBaseUrl: 'http://192.168.1.235:8383'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiBaseUrl: 'http://192.168.1.235:8280',
|
apiBaseUrl: 'https://cuddly-years-work.loca.lt',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user