From 0ae7fa316e0bcd042a2646cabcc4987d03c23194 Mon Sep 17 00:00:00 2001 From: OnlyPapy98 Date: Tue, 6 Jan 2026 14:07:09 +0100 Subject: [PATCH] agent and tpe save done --- src/app/core/interfaces/agent.ts | 2 +- src/app/core/interfaces/points-ventes.ts | 12 + src/app/core/interfaces/tpe.ts | 36 +- src/app/core/services/agent.ts | 72 ++-- src/app/core/services/points-vente.spec.ts | 16 + src/app/core/services/points-vente.ts | 92 +++++ src/app/core/services/tpe.ts | 271 ++++++------- src/app/dashboard/dashboard-routing-module.ts | 4 + src/app/dashboard/layout/layout.ts | 1 + src/app/dashboard/pages/agents/agents.html | 27 +- src/app/dashboard/pages/agents/agents.ts | 14 +- .../pages/point-vente/point-vente.css | 0 .../pages/point-vente/point-vente.html | 41 ++ .../pages/point-vente/point-vente.spec.ts | 23 ++ .../pages/point-vente/point-vente.ts | 209 ++++++++++ src/app/dashboard/pages/tpe/tpe.html | 2 +- src/app/dashboard/pages/tpe/tpe.ts | 26 +- .../shared/forms/agent-form/agent-form.html | 368 ++++++++++++++---- src/app/shared/forms/agent-form/agent-form.ts | 174 ++++++++- .../agent-full-form/agent-full-form.html | 195 +++++++--- .../forms/agent-full-form/agent-full-form.ts | 127 +++++- .../shared/forms/course-form/course-form.html | 4 +- .../shared/forms/course-form/course-form.ts | 14 +- .../point-vente-form/point-vente-form.html | 60 +++ .../point-vente-form/point-vente-form.ts | 115 ++++++ .../points-vente-form/points-vente-form.css | 0 .../points-vente-form/points-vente-form.html | 65 ++++ .../points-vente-form.spec.ts | 23 ++ .../points-vente-form/points-vente-form.ts | 126 ++++++ .../forms/resultat-form/resultat-form.ts | 12 +- src/app/shared/forms/tpe-form/tpe-form.html | 122 ++++-- src/app/shared/forms/tpe-form/tpe-form.ts | 180 ++++++--- src/environments/environment.development.ts | 2 +- 33 files changed, 1910 insertions(+), 525 deletions(-) create mode 100644 src/app/core/interfaces/points-ventes.ts create mode 100644 src/app/core/services/points-vente.spec.ts create mode 100644 src/app/core/services/points-vente.ts create mode 100644 src/app/dashboard/pages/point-vente/point-vente.css create mode 100644 src/app/dashboard/pages/point-vente/point-vente.html create mode 100644 src/app/dashboard/pages/point-vente/point-vente.spec.ts create mode 100644 src/app/dashboard/pages/point-vente/point-vente.ts create mode 100644 src/app/shared/forms/point-vente-form/point-vente-form.html create mode 100644 src/app/shared/forms/point-vente-form/point-vente-form.ts create mode 100644 src/app/shared/forms/points-vente-form/points-vente-form.css create mode 100644 src/app/shared/forms/points-vente-form/points-vente-form.html create mode 100644 src/app/shared/forms/points-vente-form/points-vente-form.spec.ts create mode 100644 src/app/shared/forms/points-vente-form/points-vente-form.ts diff --git a/src/app/core/interfaces/agent.ts b/src/app/core/interfaces/agent.ts index d3cf8ce..5e26308 100644 --- a/src/app/core/interfaces/agent.ts +++ b/src/app/core/interfaces/agent.ts @@ -48,7 +48,7 @@ export interface Agent { autreTelephone?: string; // TPE assignés (actifs seulement) - tpes?: TpeDevice[]; + tpes?: TpeDevice[] | TpeDevice; createdAt?: string; updatedAt?: string; diff --git a/src/app/core/interfaces/points-ventes.ts b/src/app/core/interfaces/points-ventes.ts new file mode 100644 index 0000000..1e61054 --- /dev/null +++ b/src/app/core/interfaces/points-ventes.ts @@ -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; +} \ No newline at end of file diff --git a/src/app/core/interfaces/tpe.ts b/src/app/core/interfaces/tpe.ts index b2a5bec..d0325b8 100644 --- a/src/app/core/interfaces/tpe.ts +++ b/src/app/core/interfaces/tpe.ts @@ -1,27 +1,23 @@ -import { Agent } from './agent'; - export type TpeStatus = - | 'VALIDE' - | 'INVALIDE' - | 'EN_PANNE' - | 'BLOQUE' - | 'DISPONIBLE' - | 'AFFECTE' - | 'EN_MAINTENANCE' - | 'HORS_SERVICE' - | 'VOLE'; + | 'ACTIF' + | 'HORS_SERVICE'; export type TpeType = 'POS' | 'OTHER'; export interface TpeDevice { id: string; - imei: string; - serial: string; - type: TpeType; - marque: string; - modele: string; + numeroSerie: string; + pointDeVenteId: string; statut: TpeStatus; - agent?: Agent; - assigne: boolean; - createdAt?: string; - updatedAt?: string; + versionLogicielle: string; + typeTerminal: string; + plateforme: string; + modeleAppareil: string; + systemeExploitation: string; + versionOs: string; + adresseIp: string; + adresseMac: string; + agentConnecteId: string; + derniereConnexionAgent: string; + derniereDeconnexionAgent: string; + journalSession: string; } diff --git a/src/app/core/services/agent.ts b/src/app/core/services/agent.ts index d80e8c6..628a455 100644 --- a/src/app/core/services/agent.ts +++ b/src/app/core/services/agent.ts @@ -66,7 +66,7 @@ interface AgentApiResponse { statutMarital?: string; epoux?: string; autreTelephone?: string; - tpes?: TpeApiResponse[]; + tpes?: TpeDevice; createdAt?: string; updatedAt?: string; createdBy?: string; @@ -88,44 +88,37 @@ export class AgentService { } // Transform API TPE response to TpeDevice - private transformTpe(apiTpe: TpeApiResponse): TpeDevice { - const transformStatut = (apiStatut: string): TpeStatus => { - const upperStatut = apiStatut.toUpperCase() as TpeStatus; - const validStatuses: TpeStatus[] = [ - 'VALIDE', - 'INVALIDE', - 'EN_PANNE', - 'BLOQUE', - 'DISPONIBLE', - 'AFFECTE', - 'EN_MAINTENANCE', - 'HORS_SERVICE', - 'VOLE', - ]; - return validStatuses.includes(upperStatut) ? upperStatut : 'INVALIDE'; - }; + // private transformTpe(apiTpe: TpeApiResponse): TpeDevice { + // const transformStatut = (apiStatut: string): TpeStatus => { + // const upperStatut = apiStatut.toUpperCase() as TpeStatus; + // const validStatuses: TpeStatus[] = [ + // 'ACTIF', + // 'HORS_SERVICE' + // ]; + // return validStatuses.includes(upperStatut) ? upperStatut : 'INVALIDE'; + // }; - // Transform agent if it's an object (not just a string reference) - let transformedAgent: Agent | undefined = undefined; - if (apiTpe.agent && typeof apiTpe.agent === 'object' && apiTpe.agent.id) { - // If agent is a full object, transform it - transformedAgent = this.transformAgent(apiTpe.agent as any); - } + // // Transform agent if it's an object (not just a string reference) + // let transformedAgent: Agent | undefined = undefined; + // if (apiTpe.agent && typeof apiTpe.agent === 'object' && apiTpe.agent.id) { + // // If agent is a full object, transform it + // transformedAgent = this.transformAgent(apiTpe.agent as any); + // } - return { - id: String(apiTpe.id), - imei: apiTpe.imei, - serial: apiTpe.serial, - type: apiTpe.type as TpeType, - marque: apiTpe.marque, - modele: apiTpe.modele, - statut: transformStatut(apiTpe.statut), - agent: transformedAgent, - assigne: apiTpe.assigne, - createdAt: apiTpe.createdAt, - updatedAt: apiTpe.updatedAt, - }; - } + // return { + // id: String(apiTpe.id), + // imei: apiTpe.imei, + // serial: apiTpe.serial, + // type: apiTpe.type as TpeType, + // marque: apiTpe.marque, + // modele: apiTpe.modele, + // statut: transformStatut(apiTpe.statut), + // agent: transformedAgent, + // assigne: apiTpe.assigne, + // createdAt: apiTpe.createdAt, + // updatedAt: apiTpe.updatedAt, + // }; + // } // Transform API response to Agent private transformAgent(apiAgent: AgentApiResponse): Agent { @@ -166,10 +159,7 @@ export class AgentService { statutMarital: apiAgent.statutMarital, epoux: apiAgent.epoux, autreTelephone: apiAgent.autreTelephone, - tpes: apiAgent.tpes?.map((tpe) => { - const transformed = this.transformTpe(tpe); - return transformed; - }), + tpes: apiAgent.tpes , createdAt: apiAgent.createdAt, updatedAt: apiAgent.updatedAt, createdBy: apiAgent.createdBy, diff --git a/src/app/core/services/points-vente.spec.ts b/src/app/core/services/points-vente.spec.ts new file mode 100644 index 0000000..4892435 --- /dev/null +++ b/src/app/core/services/points-vente.spec.ts @@ -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(); + }); +}); diff --git a/src/app/core/services/points-vente.ts b/src/app/core/services/points-vente.ts new file mode 100644 index 0000000..4f4ab0d --- /dev/null +++ b/src/app/core/services/points-vente.ts @@ -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 { + 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> { + return this.http + .get>(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 { + return this.http + .get(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }) + .pipe( + catchError((err) => { + console.error(`Error fetching point de vente ${id}:`, err); + return of(undefined); + }) + ); + } + + create(pointVente: Omit): Observable { + return this.http + .post(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): Observable { + return this.http + .put(`${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 { + return this.http + .delete(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }) + .pipe( + map(() => true), + catchError((err) => { + console.error(`Error deleting point de vente ${id}:`, err); + return of(false); + }) + ); + } +} diff --git a/src/app/core/services/tpe.ts b/src/app/core/services/tpe.ts index 51042d9..d227634 100644 --- a/src/app/core/services/tpe.ts +++ b/src/app/core/services/tpe.ts @@ -104,17 +104,10 @@ export class TpeService { private transformStatut(apiStatut: string): TpeStatus { const upperStatut = apiStatut.toUpperCase() as TpeStatus; const validStatuses: TpeStatus[] = [ - 'VALIDE', - 'INVALIDE', - 'EN_PANNE', - 'BLOQUE', - 'DISPONIBLE', - 'AFFECTE', - 'EN_MAINTENANCE', - 'HORS_SERVICE', - 'VOLE', + 'ACTIF', + 'HORS_SERVICE' ]; - 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) @@ -168,55 +161,55 @@ export class TpeService { } // Transform API response to TpeDevice - private transformTpe(apiTpe: TpeApiResponse): TpeDevice { - // Map API-specific names to our generic interface where possible - const serial = (apiTpe as any).numeroSerie || (apiTpe as any).serial || ''; - const imei = (apiTpe as any).imei || serial || ''; - const typeRaw = String((apiTpe as any).typeTerminal || '').toUpperCase(); - const type = typeRaw.includes('POS') ? ('POS' as TpeType) : ('OTHER' as TpeType); - const marque = (apiTpe as any).plateforme || (apiTpe as any).marque || ''; - const modele = (apiTpe as any).modeleAppareil || (apiTpe as any).modele || ''; - const statut = this.transformStatut(String(apiTpe.statut || 'INVALIDE')); - // Agent mapping: sometimes API returns an agent object or only an id - let agent: Agent | undefined = undefined; - if ((apiTpe as any).agent && typeof (apiTpe as any).agent === 'object' && (apiTpe as any).agent.id) { - agent = this.transformAgent((apiTpe as any).agent as AgentApiResponse); - } - const assigne = Boolean((apiTpe as any).agentConnecteId || (apiTpe as any).assigne); + // private transformTpe(apiTpe: TpeApiResponse): TpeDevice { + // // Map API-specific names to our generic interface where possible + // const serial = (apiTpe as any).numeroSerie || (apiTpe as any).serial || ''; + // const imei = (apiTpe as any).imei || serial || ''; + // const typeRaw = String((apiTpe as any).typeTerminal || '').toUpperCase(); + // const type = typeRaw.includes('POS') ? ('POS' as TpeType) : ('OTHER' as TpeType); + // const marque = (apiTpe as any).plateforme || (apiTpe as any).marque || ''; + // const modele = (apiTpe as any).modeleAppareil || (apiTpe as any).modele || ''; + // const statut = this.transformStatut(String(apiTpe.statut || 'INVALIDE')); + // // Agent mapping: sometimes API returns an agent object or only an id + // let agent: Agent | undefined = undefined; + // if ((apiTpe as any).agent && typeof (apiTpe as any).agent === 'object' && (apiTpe as any).agent.id) { + // agent = this.transformAgent((apiTpe as any).agent as AgentApiResponse); + // } + // const assigne = Boolean((apiTpe as any).agentConnecteId || (apiTpe as any).assigne); - return { - id: String(apiTpe.id), - imei: String(imei), - serial: String(serial), - type, - marque: String(marque), - modele: String(modele), - statut, - agent, - assigne, - createdAt: (apiTpe as any).createdAt, - updatedAt: (apiTpe as any).updatedAt, - }; - } + // return { + // id: String(apiTpe.id), + // imei: String(imei), + // serial: String(serial), + // type, + // marque: String(marque), + // modele: String(modele), + // statut, + // agent, + // assigne, + // createdAt: (apiTpe as any).createdAt, + // updatedAt: (apiTpe as any).updatedAt, + // }; + // } // Transform TpeDevice to API payload (best-effort) - private transformToApiPayload(tpe: Partial): 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; - } + // private transformToApiPayload(tpe: Partial): 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 getById(id: string): Observable { if (USE_SERVER) { - return this.http.get(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }).pipe( - map((api) => this.transformTpe(api)), + return this.http.get(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }).pipe( + map((api) => api), catchError((err) => { console.error(`Error fetching TPE ${id}:`, err); return of(undefined); @@ -239,13 +232,13 @@ getById(id: string): Observable { } return this.http - .get>(this.apiUrl, { + .get>(this.apiUrl, { params: httpParams, headers: this.getNgrokHeaders(), }) .pipe( map((list) => { - const content = (list.content || []).map((api) => this.transformTpe(api)); + const content = (list.content || []).map((api) => api); return { ...list, content } as PagedResult; }), catchError((err) => { @@ -257,11 +250,10 @@ getById(id: string): Observable { // POST /api/v1/tpes - Create create(payload: Partial): Observable { - const apiPayload = this.transformToApiPayload(payload); return this.http - .post(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() }) + .post(this.apiUrl, payload, { headers: this.getNgrokHeaders() }) .pipe( - map((apiTpe) => this.transformTpe(apiTpe)), + map((apiTpe) => apiTpe), catchError((err) => { console.error('Error creating TPE:', err); throw err; @@ -271,13 +263,12 @@ getById(id: string): Observable { // PUT /api/v1/tpes/{id} - Update update(id: string, payload: Partial): Observable { - const apiPayload = this.transformToApiPayload(payload); return this.http - .put(`${this.apiUrl}/${id}`, apiPayload, { + .put(`${this.apiUrl}/${id}`, payload, { headers: this.getNgrokHeaders(), }) .pipe( - map((apiTpe) => this.transformTpe(apiTpe)), + map((apiTpe) => apiTpe), catchError((err) => { console.error(`Error updating TPE ${id}:`, err); return of(undefined); @@ -305,13 +296,13 @@ update(id: string, payload: Partial): Observable { if (USE_SERVER) { return this.http - .patch( + .patch( `${this.apiUrl}/${id}/statut`, { statut: this.transformStatutToApi(statut) }, { headers: this.getNgrokHeaders() } ) .pipe( - map((apiTpe) => this.transformTpe(apiTpe)), + map((apiTpe) => apiTpe), catchError((err) => { console.error(`Error updating TPE statut ${id}:`, err); return of(undefined); @@ -323,77 +314,71 @@ update(id: string, payload: Partial): Observable { - if (USE_SERVER) { // First get the current TPE data - return this.getById(id).pipe( - switchMap((tpe) => { - if (!tpe) { - return of(undefined); - } - // Update the whole TPE with assigne set to false and statut to DISPONIBLE - const updatedTpe = { ...tpe, assigne: false, statut: 'DISPONIBLE' as TpeStatus }; - const apiPayload = this.transformToApiPayload(updatedTpe); - return this.http - .patch(`${this.apiUrl}/liberer/${id}`, apiPayload, { - headers: this.getNgrokHeaders(), - }) - .pipe( - map((apiTpe) => this.transformTpe(apiTpe)), - catchError((err) => { - console.error(`Error liberating TPE ${id}:`, err); - return of(undefined); - }) - ); - }), - catchError((err) => { - console.error(`Error fetching TPE ${id} for liberation:`, err); + return this.getById(id).pipe( + switchMap((tpe) => { + if (!tpe) { return of(undefined); - }) - ); - } - return of(undefined); + } + // Update the whole TPE with assigne set to false and statut to DISPONIBLE + const updatedTpe = { + ...tpe, + statut: 'ACTIF' + }; + return this.http + .patch(`${this.apiUrl}/liberer/${id}`, updatedTpe, { + headers: this.getNgrokHeaders(), + }) + .pipe( + map((apiTpe) => apiTpe), + catchError((err) => { + console.error(`Error liberating TPE ${id}:`, err); + return of(undefined); + }) + ); + }), + catchError((err) => { + console.error(`Error fetching TPE ${id} for liberation:`, err); + return of(undefined); + }) + ); + } // PATCH /api/v1/tpes/assigner - Assign TPE // Payload: { tpeId: number, agentId: number } assigner(id: string, agentId: string): Observable { - if (USE_SERVER) { - const payload = { - tpeId: Number(id), - agentId: Number(agentId), - }; - return this.http - .patch(`${this.apiUrl}/assigner`, payload, { - headers: this.getNgrokHeaders(), + const payload = { + tpeId: Number(id), + agentId: Number(agentId), + }; + return this.http + .patch(`${this.apiUrl}/assigner`, payload, { + headers: this.getNgrokHeaders(), + }) + .pipe( + map((apiTpe) => apiTpe), + catchError((err) => { + console.error(`Error assigning TPE ${id}:`, err); + return of(undefined); }) - .pipe( - map((apiTpe) => this.transformTpe(apiTpe)), - catchError((err) => { - console.error(`Error assigning TPE ${id}:`, err); - return of(undefined); - }) - ); - } - return of(undefined); + ); } // GET /api/v1/tpes/statut/{statut} - List by statut getByStatut(statut: TpeStatus): Observable { - if (USE_SERVER) { - const apiStatut = this.transformStatutToApi(statut); - return this.http - .get(`${this.apiUrl}/statut/${apiStatut}`, { - headers: this.getNgrokHeaders(), + const apiStatut = this.transformStatutToApi(statut); + return this.http + .get(`${this.apiUrl}/statut/${apiStatut}`, { + headers: this.getNgrokHeaders(), + }) + .pipe( + map((list) => list.map((apiTpe) => apiTpe)), + catchError((err) => { + console.error(`Error fetching TPEs by statut ${statut}:`, err); + return of([]); }) - .pipe( - map((list) => list.map((apiTpe) => this.transformTpe(apiTpe))), - catchError((err) => { - console.error(`Error fetching TPEs by statut ${statut}:`, err); - return of([]); - }) - ); - } - return of([]); + ); } // GET /api/v1/tpes/stats/count-by-statut - Get count by statut @@ -432,36 +417,30 @@ update(id: string, payload: Partial): Observable { - if (USE_SERVER) { - return this.http - .get(`${this.apiUrl}/search`, { - params: { q: query.trim() }, - headers: this.getNgrokHeaders(), + return this.http + .get(`${this.apiUrl}/search`, { + params: { q: query.trim() }, + headers: this.getNgrokHeaders(), + }) + .pipe( + map((list) => list.map((apiTpe) => apiTpe)), + catchError((err) => { + console.error(`Error searching TPEs with query ${query}:`, err); + return of([]); }) - .pipe( - map((list) => list.map((apiTpe) => this.transformTpe(apiTpe))), - catchError((err) => { - console.error(`Error searching TPEs with query ${query}:`, err); - return of([]); - }) - ); - } - return of([]); + ); } // GET /api/v1/tpes/disponibles - List available TPEs getDisponibles(): Observable { - if (USE_SERVER) { - return this.http - .get(`${this.apiUrl}/disponibles`, { headers: this.getNgrokHeaders() }) - .pipe( - map((list) => list.map((apiTpe) => this.transformTpe(apiTpe))), - catchError((err) => { - console.error('Error fetching available TPEs:', err); - return of([]); - }) - ); - } - return of([]); + return this.http + .get(`${this.apiUrl}/disponibles`, { headers: this.getNgrokHeaders() }) + .pipe( + map((list) => list.map((apiTpe) => apiTpe)), + catchError((err) => { + console.error('Error fetching available TPEs:', err); + return of([]); + }) + ); } } diff --git a/src/app/dashboard/dashboard-routing-module.ts b/src/app/dashboard/dashboard-routing-module.ts index 3b61cba..4a3b1d6 100644 --- a/src/app/dashboard/dashboard-routing-module.ts +++ b/src/app/dashboard/dashboard-routing-module.ts @@ -57,6 +57,10 @@ const routes: Routes = [ path: 'limits', loadComponent: () => import('./pages/limits/limits').then((m) => m.LimitsPage), }, + { + path: 'points-vente', + loadComponent: () => import('./pages/point-vente/point-vente').then((m) => m.PointVentePage), + }, ], }, ]; diff --git a/src/app/dashboard/layout/layout.ts b/src/app/dashboard/layout/layout.ts index 9308c25..2b80d98 100644 --- a/src/app/dashboard/layout/layout.ts +++ b/src/app/dashboard/layout/layout.ts @@ -60,6 +60,7 @@ export class Layout { ], }, { icon: 'icon-monitor', label: 'Gestion des TPE', link: '/tpes' }, + { icon: 'icon-map-pin', label: 'Points de vente', link: '/points-vente' }, { icon: 'icon-users', label: 'Utilisateurs', diff --git a/src/app/dashboard/pages/agents/agents.html b/src/app/dashboard/pages/agents/agents.html index 2e70f05..00849f0 100644 --- a/src/app/dashboard/pages/agents/agents.html +++ b/src/app/dashboard/pages/agents/agents.html @@ -35,16 +35,11 @@ - -
- Annuler - Enregistrer -
@@ -319,7 +314,7 @@ @for (tpe of getAgentTpes(agent.id); track tpe.id) {
-
{{ tpe.imei }}
+
{{ tpe.numeroSerie }}
@if (tpe.statut) { {{ formatTpeStatut(tpe.statut) }} @@ -327,19 +322,19 @@ }
- @if (tpe.marque || tpe.modele) { + @if (tpe.modeleAppareil) {
- Modèle: {{ tpe.marque }} {{ tpe.modele }} + Modèle: {{ tpe.modeleAppareil }}
} - @if (tpe.serial) { + @if (tpe.numeroSerie) {
- Série: {{ tpe.serial }} + Série: {{ tpe.numeroSerie }}
} - @if (tpe.type) { + @if (tpe.typeTerminal) {
- Type: {{ tpe.type }} + Type: {{ tpe.typeTerminal }}
}
@@ -385,9 +380,9 @@ > @for (tpe of availableTpes(); track tpe.id) { - {{ tpe.imei }} - {{ tpe.marque }} {{ tpe.modele }} - @if (tpe.statut === 'VALIDE') { - (Valide) + {{ tpe.numeroSerie }} - {{ tpe.modeleAppareil }} + @if (tpe.statut === 'ACTIF') { + (Actif) } } diff --git a/src/app/dashboard/pages/agents/agents.ts b/src/app/dashboard/pages/agents/agents.ts index 0b83f3f..cf643d0 100644 --- a/src/app/dashboard/pages/agents/agents.ts +++ b/src/app/dashboard/pages/agents/agents.ts @@ -26,6 +26,7 @@ import { AgentFullForm } from '@shared/forms/agent-full-form/agent-full-form'; import { forkJoin, of } from 'rxjs'; import { switchMap, catchError } from 'rxjs/operators'; import { toast } from 'ngx-sonner'; +import { AgentForm } from '@shared/forms/agent-form/agent-form'; @Component({ standalone: true, @@ -43,7 +44,7 @@ import { toast } from 'ngx-sonner'; ZardSelectComponent, ZardSelectItemComponent, ZardFormModule, - AgentFullForm, + AgentForm ], }) export class AgentsPage { @@ -111,7 +112,7 @@ export class AgentsPage { ) { // Preload TPE maps for display this.tpeSvc - .list({ page: 1, size: 200, search: '', sortKey: 'imei', sortDir: 'asc' } as any) + .list({ page: 1, size: 200, search: '', sortKey: 'id', sortDir: 'asc' } as any) .subscribe((res) => { const tpes = res.content as TpeDevice[]; this.rebuildTpeMaps(tpes); @@ -138,6 +139,7 @@ export class AgentsPage { this.loading.set(true); this.api.list(params).subscribe({ next: (res) => { + console.log(res.content); this.rows.set(res.content); this.total.set(res.pageable.total); this.loading.set(false); @@ -166,7 +168,7 @@ export class AgentsPage { this.agentTpesMap.clear(); tpes.forEach((t) => { this.tpeMap.set(t.id, t); - const agentId = t.agent?.id; + const agentId = t.agentConnecteId; if (agentId) { const list = this.agentTpesMap.get(agentId) || []; list.push(t); @@ -417,14 +419,14 @@ export class AgentsPage { const agentTpeIds = new Set(currentAgentTpes.map((t) => t.id)); // 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]) => { // 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 available = allTpes.filter( (t) => - !t.assigne && - (t.statut === 'DISPONIBLE' || t.statut === 'VALIDE') && + !t.agentConnecteId && + (t.statut === 'ACTIF') && !agentTpeIds.has(t.id) ); // Remove duplicates diff --git a/src/app/dashboard/pages/point-vente/point-vente.css b/src/app/dashboard/pages/point-vente/point-vente.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dashboard/pages/point-vente/point-vente.html b/src/app/dashboard/pages/point-vente/point-vente.html new file mode 100644 index 0000000..6c70c38 --- /dev/null +++ b/src/app/dashboard/pages/point-vente/point-vente.html @@ -0,0 +1,41 @@ +
+
+

Gestion des Points de Vente

+ Nouveau point de vente +
+ + + + + +
+ + +
+
+
+ + +
+ + + +
+ Annuler + Enregistrer +
+
diff --git a/src/app/dashboard/pages/point-vente/point-vente.spec.ts b/src/app/dashboard/pages/point-vente/point-vente.spec.ts new file mode 100644 index 0000000..cbab579 --- /dev/null +++ b/src/app/dashboard/pages/point-vente/point-vente.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PointVente } from './point-vente'; + +describe('PointVente', () => { + let component: PointVente; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PointVente] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PointVente); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dashboard/pages/point-vente/point-vente.ts b/src/app/dashboard/pages/point-vente/point-vente.ts new file mode 100644 index 0000000..b6f53de --- /dev/null +++ b/src/app/dashboard/pages/point-vente/point-vente.ts @@ -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([]); + total = signal(0); + loading = signal(false); + + page = signal(0); + size = signal(10); + search = signal(''); + sort = signal({ key: 'nom', dir: 'asc' }); + + modalOpen = signal(false); + modalTitle = signal('Nouveau point de vente'); + editingItem = signal(null); + + @ViewChild(PointsVenteForm) formComp?: PointsVenteForm; + + cols: TableColumn[] = [ + { 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 === 'ACTIVE') { + return ` Actif`; + } + return ` Inactif`; + } + + 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) { + const current = this.editingItem(); + const isCreating = !current?.id; + + const req$ = current?.id + ? this.api.update(current.id, payload) + : this.api.create(payload as Omit); + + 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"); + }, + }); + } +} diff --git a/src/app/dashboard/pages/tpe/tpe.html b/src/app/dashboard/pages/tpe/tpe.html index b5b47eb..fed2f12 100644 --- a/src/app/dashboard/pages/tpe/tpe.html +++ b/src/app/dashboard/pages/tpe/tpe.html @@ -159,7 +159,7 @@ diff --git a/src/app/dashboard/pages/tpe/tpe.ts b/src/app/dashboard/pages/tpe/tpe.ts index c0b3934..292a6df 100644 --- a/src/app/dashboard/pages/tpe/tpe.ts +++ b/src/app/dashboard/pages/tpe/tpe.ts @@ -26,6 +26,7 @@ import { ZardSelectItemComponent } from '@shared/components/select/select-item.c import { ZardFormModule } from '@shared/components/form/form.module'; import { forkJoin, Subject } from 'rxjs'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; +import { toast } from 'ngx-sonner'; @Component({ standalone: true, @@ -105,11 +106,12 @@ export class TpePage implements OnInit { key: 'assigne', label: 'Assigné à', cell: (d) => { - if (!d.assigne || !d.agent) { + if (!d.agentConnecteId) { return 'Non assigné'; } - const agent = d.agent; - const code = agent.code + // a rectifier apres avec les données des agents! + const agent = {} as any; + const code = agent?.code ? `${agent.code}` : ''; const name = @@ -135,15 +137,8 @@ export class TpePage implements OnInit { ]; allStatuses: TpeStatus[] = [ - 'VALIDE', - 'INVALIDE', - 'EN_PANNE', - 'BLOQUE', - 'DISPONIBLE', - 'AFFECTE', - 'EN_MAINTENANCE', - 'HORS_SERVICE', - 'VOLE', + 'ACTIF', + 'HORS_SERVICE' ]; constructor(private api: TpeService, private agentService: AgentService) { @@ -326,7 +321,7 @@ export class TpePage implements OnInit { } 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({ next: () => { this.fetch({ @@ -342,7 +337,7 @@ export class TpePage implements OnInit { } 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({ next: () => { this.fetch({ @@ -440,6 +435,7 @@ export class TpePage implements OnInit { : this.api.create(payload as Omit); req$.subscribe({ next: (result) => { + toast.success('Tpe créé avec succès!') // For update, check if result is valid (update can return undefined on error) if (current?.id && !result) { console.error('Update failed - result is undefined'); @@ -472,7 +468,7 @@ export class TpePage implements OnInit { } 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.fetch({ page: this.page(), diff --git a/src/app/shared/forms/agent-form/agent-form.html b/src/app/shared/forms/agent-form/agent-form.html index 9a6c219..7ab1325 100644 --- a/src/app/shared/forms/agent-form/agent-form.html +++ b/src/app/shared/forms/agent-form/agent-form.html @@ -1,91 +1,293 @@ -
-
- - -
-
- - -
-
+ - - -
- - Actif - Inactif - Suspendu - -
-
- - -
-
- - -
-
- - -
-
+ +
+

Identification

+
- - -
-
- - -
-
- - -
-
+ + +
+ +
+
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
+ + +
+ + @for (p of profils; track p) { + {{ p.label }} + } + +
+
+ + +
+ +
+
+ + +
+ +
+
- - -
- - @for (l of limits; track l.id) { - {{ l.nom }} - } - -
-
+ + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ + @for (s of statutOptions; track s) { + {{ s.label }} + } + +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ +
-
- Annuler - Enregistrer + +
+

Contact & Identité

+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ +
+ + +
+

Limites & Périphériques

+
+ + +
+ +
+
+ + + +
+ +
+
+ + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ +
+
+ + +
+

Informations légales

+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + +
+ +
+
+
+
+ + +
+ + + +
+ - - diff --git a/src/app/shared/forms/agent-form/agent-form.ts b/src/app/shared/forms/agent-form/agent-form.ts index 85df511..b81cc0f 100644 --- a/src/app/shared/forms/agent-form/agent-form.ts +++ b/src/app/shared/forms/agent-form/agent-form.ts @@ -9,6 +9,7 @@ import { ZardButtonComponent } from '@shared/components/button/button.component' import { Agent, AgentStatus } from 'src/app/core/interfaces/agent'; import { AgentLimit } from 'src/app/core/interfaces/agent-limit'; import { AgentLimitService } from 'src/app/core/services/agent-limit'; +import { ZardCheckboxComponent } from "@shared/components/checkbox/checkbox.component"; @Component({ selector: 'app-agent-form', @@ -23,6 +24,33 @@ export class AgentForm { 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; @Input() set value(v: Agent | undefined) { this._value = v; this.hydrateFromValue(v); } get value(): Agent | undefined { return this._value; } @@ -33,25 +61,46 @@ export class AgentForm { constructor(private fb: FormBuilder, private limitService: AgentLimitService) { this.form = this.fb.group({ code: ['', Validators.required], - profil: ['', Validators.required], - statut: ['ACTIF' as AgentStatus, Validators.required], + profil: ['CAISSIER', Validators.required], + principalCode: ['', Validators.required], + caisseProfile: ['', Validators.required], + statut: ['ACTIF', Validators.required], zone: [''], kiosk: [''], fonction: [''], dateEmbauche: [''], - nom: ['', Validators.required], prenom: ['', Validators.required], - phone: ['', [Validators.required, Validators.minLength(6)]], + autresNoms: [''], + dateNaissance: [''], + lieuNaissance: [''], + ville: [''], + adresse: [''], + autoriserAides: [false], - limiteInferieure: [0, [Validators.min(0)]], - limiteSuperieure: [0, [Validators.min(0)]], - limiteParTransaction: [0, [Validators.min(0)]], - limiteMinAirtime: [0, [Validators.min(0)]], - limiteMaxAirtime: [0, [Validators.min(0)]], - maxPeripheriques: [0, [Validators.min(0)]], + phone: ['', Validators.required], + pin: [''], + + limiteInferieure: [''], + limiteSuperieure: [''], + limiteParTransaction: [''], + limiteMinAirtime: [''], + limiteMaxAirtime: [''], + + maxPeripheriques: [''], + + limitId: ['1'], + + nationalite: [''], + cni: [''], + cniDelivreeLe: [''], + cniDelivreeA: [''], + residence: [''], + autreAdresse1: [''], + statutMarital: [''], + epoux: [''], + autreTelephone: [''], - limitId: ['', Validators.required], }); this.limitService @@ -59,29 +108,122 @@ export class AgentForm { .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 ''; } private hydrateFromValue(v?: Agent) { if (!v) { this.form.reset({ - code: '', profil: '', 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; } this.form.reset({ - code: v.code, profil: v.profil, 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() { 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 payload: Agent = { id: this.value?.id ?? '', code: raw.code, profil: raw.profil, 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); } + + onClose(){ + this.cancel.emit() + } } diff --git a/src/app/shared/forms/agent-full-form/agent-full-form.html b/src/app/shared/forms/agent-full-form/agent-full-form.html index dd64095..f414cc6 100644 --- a/src/app/shared/forms/agent-full-form/agent-full-form.html +++ b/src/app/shared/forms/agent-full-form/agent-full-form.html @@ -1,5 +1,5 @@
-
+
Informations Emploi
@@ -10,8 +10,8 @@ > -
-
+
@@ -94,6 +94,88 @@ >
+ + +
Paramètres de connexion
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + @for (l of limits; track l.id) { + + {{ l.nom }} + @if (l.isDefault) { + (Par défaut) + } @if (l.actif) { + • Actif + } + + } + +
+ @if (selectedLimit) { +
+
{{ selectedLimit.nom }}
+
+ @if (selectedLimit.isDefault) { + Limite par défaut + } @if (selectedLimit.actif) { + Contrôle actif + } @else { + Contrôle inactif + } +
+
+
Min Bet: {{ (selectedLimit.betMin ?? 0).toLocaleString('fr-FR') }}
+
Max Bet: {{ (selectedLimit.betMax ?? 0).toLocaleString('fr-FR') }}
+
Max Bet (tx): {{ (selectedLimit.maxBet ?? 0).toLocaleString('fr-FR') }}
+
+
+ } +
+
+
@@ -138,57 +220,15 @@ >
- -
Paramètres de connexion
-
- -
-
- -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- +
- + +
+
Assignation TPE (actifs)
+ Gérer +
+
+ @for (id of tpeArray.value; track id) { + + {{ tpeLabel(id) }} + + + } @empty { + Aucun TPE assigné + } +
+
- + + + + + @if (!isSelectedTpe(row.id)) { + + } @else { + + } + + + +
+ Terminer +
+
diff --git a/src/app/shared/forms/agent-full-form/agent-full-form.ts b/src/app/shared/forms/agent-full-form/agent-full-form.ts index 604ce2d..ba563f9 100644 --- a/src/app/shared/forms/agent-full-form/agent-full-form.ts +++ b/src/app/shared/forms/agent-full-form/agent-full-form.ts @@ -7,12 +7,16 @@ import { ZardSelectComponent } from '@shared/components/select/select.component' import { ZardSelectItemComponent } from '@shared/components/select/select-item.component'; import { ZardButtonComponent } from '@shared/components/button/button.component'; import { ZardCardComponent } from '@shared/components/card/card.component'; -// DataTable removed from this form (TPE UI was removed) +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 { Agent, AgentFamilyMember } from 'src/app/core/interfaces/agent'; import { AgentLimit } from 'src/app/core/interfaces/agent-limit'; import { AgentLimitService } from 'src/app/core/services/agent-limit'; import { AgentFamilyMemberService } from 'src/app/core/services/agent-family-member'; -// TPE assignment removed from form (handled elsewhere) +import { TpeDevice } from 'src/app/core/interfaces/tpe'; +import { TpeService } from 'src/app/core/services/tpe'; @Component({ selector: 'app-agent-full-form', @@ -26,18 +30,36 @@ import { AgentFamilyMemberService } from 'src/app/core/services/agent-family-mem ZardInputDirective, ZardSelectComponent, ZardSelectItemComponent, + ZardButtonComponent, ZardCardComponent, - // TPE UI components removed from this form + DataTable, + Paginator, + SearchBar, + Modal, ], }) export class AgentFullForm { - @Output() save = new EventEmitter>(); + @Output() save = new EventEmitter(); @Output() cancel = new EventEmitter(); - @Input() compact = false; // when true, hide family and TPE sections for a shorter creation form limits: AgentLimit[] = []; selectedLimit: AgentLimit | null = null; - // TPE assignment removed from the creation form; handled in separate flows + tpes: TpeDevice[] = []; + // TPE picker state + assignModalOpen = false; + tpeRows: TpeDevice[] = []; + tpeTotal = 0; + tpePage = 1; + tpePerPage = 10; + tpeSearch = ''; + tpeCols: TableColumn[] = [ + { key: 'imei', label: 'IMEI', sortable: true }, + { key: 'serial', label: 'N° Série', sortable: true }, + { key: 'marque', label: 'Marque', sortable: true }, + { key: 'modele', label: 'Modèle', sortable: true }, + { key: 'type', label: 'Type', sortable: true }, + { key: 'statut', label: 'Statut', sortable: true }, + ]; private _value?: Agent; @Input() set value(v: Agent | undefined) { this._value = v; this.hydrate(v); } @@ -49,19 +71,21 @@ export class AgentFullForm { constructor( private fb: FormBuilder, private limitService: AgentLimitService, + private tpeService: TpeService, private familyMemberService: AgentFamilyMemberService ) { this.form = this.fb.group({ // Emploi - code: ['', Validators.required], profil: ['', Validators.required], principalCode: [''], caisseProfile: [''], statut: ['ACTIF', Validators.required], zone: [''], kiosk: [''], fonction: [''], dateEmbauche: [''], + code: ['', Validators.required], profile: ['', Validators.required], principalCode: [''], caisseProfile: [''], statut: ['ACTIF', Validators.required], zone: [''], kiosk: [''], fonction: [''], dateEmbauche: [''], // Perso nom: ['', Validators.required], prenom: ['', Validators.required], autresNoms: [''], dateNaissance: [''], lieuNaissance: [''], adresse: [''], ville: [''], autoriserAides: [false], maxPeripheriques: [0, [Validators.min(0)]], // Connexion & limites - phone: ['', Validators.required], pin: [''], limiteInferieure: [0], limiteSuperieure: [0], limiteParTransaction: [0], limiteMinAirtime: [0], limiteMaxAirtime: [0], limitId: ['1'], + phone: ['', Validators.required], pin: [''], limiteInferieure: [0], limiteSuperieure: [0], limiteParTransaction: [0], limiteMinAirtime: [0], limiteMaxAirtime: [0], limitId: ['', Validators.required], // Légales nationalite: [''], cni: [''], cniDelivreeLe: [''], cniDelivreeA: [''], residence: [''], autreAdresse1: [''], statutMarital: [''], epoux: [''], autreTelephone: [''], // Famille famille: this.fb.array([]), + // TPE (stored as IDs in form, converted to full objects on submit) }); this.limitService.list({ page: 1, perPage: 100, search: '', sortKey: 'nom', sortDir: 'asc' } as any).subscribe((res) => { @@ -89,14 +113,14 @@ export class AgentFullForm { this.selectedLimit = defaultLimit; } }); - // TPE fetching/assignment handled outside of this form + // initial fetch page for TPE modal + this.fetchTpes(); } get famille(): FormArray { return this.form.get('famille') as FormArray; } - + get tpeArray(): FormArray { return this.form.get('tpeIds') as FormArray; } addFamily() { - if (this.compact) return; // no family in compact mode this.famille.push( this.fb.group({ id: [''], // Will be set when saved @@ -118,6 +142,7 @@ export class AgentFullForm { private hydrate(v?: Agent) { this.famille.clear(); + this.tpeArray.clear(); this.selectedLimit = null; if (!v) { @@ -127,7 +152,7 @@ export class AgentFullForm { this.form.reset({ code: '', - profil: '', + profile: '', principalCode: '', caisseProfile: '', statut: 'ACTIF', @@ -200,10 +225,59 @@ export class AgentFullForm { }); } - // Assigned TPEs are not handled in this form + } - // TPE assignment / picker removed from this form + onToggleTpe(id: string, checked: boolean) { + const idx = this.tpeArray.value.indexOf(id); + if (checked && idx === -1) this.tpeArray.push(this.fb.control(id)); + if (!checked && idx !== -1) this.tpeArray.removeAt(idx); + } + + openAssignTpe() { + this.assignModalOpen = true; + this.fetchTpes(); + } + closeAssignTpe() { + this.assignModalOpen = false; + // Refresh TPE list to show current assignments + this.fetchTpes(); + } + onTpeSearch(q: string) { this.tpeSearch = q; this.tpePage = 1; this.fetchTpes(); } + onTpePageChange(p: number) { this.tpePage = p; this.fetchTpes(); } + onTpePerPageChange(pp: number) { this.tpePerPage = pp; this.fetchTpes(); } + isSelectedTpe(id: string) { return (this.tpeArray.value as string[]).includes(id); } + + fetchTpes() { + this.tpeService + .list({ page: this.tpePage, perPage: this.tpePerPage, search: this.tpeSearch, sortKey: 'imei', sortDir: 'asc' } as any) + .subscribe((res) => { + // Only show VALIDE TPEs that are either not assigned or assigned to this agent + const currentAgentId = this.value?.id; + this.tpeRows = res.content.filter((t) => { + if (t.statut !== 'ACTIF') return false; + // If TPE is assigned but to this agent, show it + if (t.agentConnecteId && currentAgentId) { + // We need to check if this TPE is assigned to this agent + // For now, show all VALIDE TPEs - the backend should handle assignment logic + return true; + } + // Show unassigned TPEs + return !t.agentConnecteId; + }); + this.tpeTotal = this.tpeRows.length; + }); + } + + tpeLabel(id: string): string { + const list = [...(this.tpeRows ?? []), ...(this.tpes ?? [])]; + const found = list.find((t) => t.id === id); + if (found) { + return `${found.numeroSerie}`; + } + // Try to load from service if not in current list + return id; + } // === Validation helpers === error(control: string): string { @@ -237,13 +311,23 @@ export class AgentFullForm { return; } const raw = this.form.getRawValue() as any; - // Prepare partial agent payload (family members are handled separately) - const payload: Partial = { + + // Convert TPE IDs to full TPE object + const tpes: TpeDevice[] = []; + + // Prepare agent payload (without famille - family members are handled separately) + const payload: Agent = { + ...(this.value ?? {}), + id: this.value?.id || '', ...raw, - ...(this.value?.id ? { id: this.value.id } : {}), - }; - - // Emit the partial agent payload (parent will decide create vs update) + tpes: tpes, + } as Agent; + + // Remove tpeIds from payload (it's not part of Agent interface) + delete (payload as any).tpeIds; + + // Emit the agent payload first + // Family members will be saved separately in the parent component this.save.emit(payload); } @@ -263,6 +347,7 @@ export class AgentFullForm { this._value = undefined; this.selectedLimit = null; this.famille.clear(); + this.tpeArray.clear(); // Find default limit to assign it automatically const defaultLimit = this.limits.find((l) => l.isDefault); @@ -270,7 +355,7 @@ export class AgentFullForm { this.form.reset({ code: '', - profil: '', + profile: '', principalCode: '', caisseProfile: '', statut: 'ACTIF', diff --git a/src/app/shared/forms/course-form/course-form.html b/src/app/shared/forms/course-form/course-form.html index c60af97..26585c7 100644 --- a/src/app/shared/forms/course-form/course-form.html +++ b/src/app/shared/forms/course-form/course-form.html @@ -180,14 +180,14 @@ Types de paris ouverts - + @for (t of courseTypes; track t.value) { diff --git a/src/app/shared/forms/course-form/course-form.ts b/src/app/shared/forms/course-form/course-form.ts index 0a74f39..c8ab899 100644 --- a/src/app/shared/forms/course-form/course-form.ts +++ b/src/app/shared/forms/course-form/course-form.ts @@ -216,8 +216,6 @@ export class CourseForm implements OnInit, AfterViewInit, OnDestroy { this.selectedHippodromeLabel.set(''); this.form.markAsPristine(); this.form.markAsUntouched(); - // Ensure UI updates for cleared form - this.cdr.markForCheck(); return; } @@ -248,9 +246,6 @@ export class CourseForm implements OnInit, AfterViewInit, OnDestroy { { emitEvent: false } ); - // Ensure view updates when hydrating values (OnPush component) - this.cdr.markForCheck(); - // Set hippodrome label if available if (hippodromeId && this.hippodromes().length > 0) { const h = this.hippodromes().find((r) => String(r.id) === hippodromeId); @@ -279,9 +274,6 @@ export class CourseForm implements OnInit, AfterViewInit, OnDestroy { ? [...current, value] : current.filter((v: string) => v !== value) }); - - // Trigger change detection so checkbox states update in OnPush mode - this.cdr.markForCheck(); } @@ -300,6 +292,7 @@ onSubmit() { const foundHippodrome = this.hippodromes().find(h => String(h.id) === String(hippodromeId)); const hippodromeObj = foundHippodrome ?? (hippodromeId ? { id: +hippodromeId } : undefined); + // 2️⃣ Transformer typesParisOuverts CSV → tablea // 3️⃣ Construire payload @@ -325,8 +318,6 @@ onSubmit() { }; - - // Persist: create or update via CourseService, then emit the saved Course if (this.value && this.value.id) { this.courseServive.update(this.value.id, payload).subscribe({ @@ -334,8 +325,7 @@ onSubmit() { if (updated) this.save.emit(updated); else console.error('Update returned empty result'); }, - error: (err) => { - console.error('Error updating course:', err)}, + error: (err) => console.error('Error updating course:', err), }); } else { this.courseServive.create(payload).subscribe({ diff --git a/src/app/shared/forms/point-vente-form/point-vente-form.html b/src/app/shared/forms/point-vente-form/point-vente-form.html new file mode 100644 index 0000000..2580158 --- /dev/null +++ b/src/app/shared/forms/point-vente-form/point-vente-form.html @@ -0,0 +1,60 @@ +
+
+ + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ + Actif + Inactif + +
+
+
+
diff --git a/src/app/shared/forms/point-vente-form/point-vente-form.ts b/src/app/shared/forms/point-vente-form/point-vente-form.ts new file mode 100644 index 0000000..3032d9d --- /dev/null +++ b/src/app/shared/forms/point-vente-form/point-vente-form.ts @@ -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>(); + @Output() cancel = new EventEmitter(); + + 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 = { + ...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; + } +} diff --git a/src/app/shared/forms/points-vente-form/points-vente-form.css b/src/app/shared/forms/points-vente-form/points-vente-form.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/forms/points-vente-form/points-vente-form.html b/src/app/shared/forms/points-vente-form/points-vente-form.html new file mode 100644 index 0000000..3ffcb54 --- /dev/null +++ b/src/app/shared/forms/points-vente-form/points-vente-form.html @@ -0,0 +1,65 @@ +
+
+ + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ + {{ statut.label }} + +
+
+
+
diff --git a/src/app/shared/forms/points-vente-form/points-vente-form.spec.ts b/src/app/shared/forms/points-vente-form/points-vente-form.spec.ts new file mode 100644 index 0000000..fd1b715 --- /dev/null +++ b/src/app/shared/forms/points-vente-form/points-vente-form.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PointsVenteForm] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PointsVenteForm); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/forms/points-vente-form/points-vente-form.ts b/src/app/shared/forms/points-vente-form/points-vente-form.ts new file mode 100644 index 0000000..70d0e84 --- /dev/null +++ b/src/app/shared/forms/points-vente-form/points-vente-form.ts @@ -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>(); + @Output() cancel = new EventEmitter(); + + 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 = { + ...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; + } +} diff --git a/src/app/shared/forms/resultat-form/resultat-form.ts b/src/app/shared/forms/resultat-form/resultat-form.ts index 0e08731..fa63816 100644 --- a/src/app/shared/forms/resultat-form/resultat-form.ts +++ b/src/app/shared/forms/resultat-form/resultat-form.ts @@ -5,7 +5,7 @@ import { ZardFormModule } from '@shared/components/form/form.module'; import { ZardSelectComponent } from '@shared/components/select/select.component'; import { ZardSelectItemComponent } from '@shared/components/select/select-item.component'; import { Course, CourseType } from 'src/app/core/interfaces/course'; -import { Resultat, ResultatStatut } from 'src/app/core/interfaces/resultat'; +import { Resultat } from 'src/app/core/interfaces/resultat'; type PlaceRow = { picks: FormArray> }; type ResultatShape = { places: FormArray> }; @@ -61,16 +61,16 @@ export class ResultatForm { maxNum = computed(() => this.course?.nombrePartants ?? 0); // Ensure non-partants are compared as strings to avoid type mismatches npSet = computed(() => new Set((this.course?.nonPartants ?? []).map((v) => String(v)))); - statut = computed((): ResultatStatut => { - return this.resultat ? this.resultat.statut : ResultatStatut.EN_ATTENTE; -}); + statut = computed((): 'PROVISOIRE' | 'OFFICIEL' | 'ANNULE' | 'EN_ATTENTE' => { + return this.resultat ? 'PROVISOIRE' : 'EN_ATTENTE'; + }); canValidate(): boolean { - return String(this.statut()) === "EN_ATTENTE"; + return this.statut() === 'EN_ATTENTE'; } canConfirm(): boolean { - return String(this.statut()) === "PROVISOIRE"; + return this.statut() === 'PROVISOIRE'; } // Helper methods for template diff --git a/src/app/shared/forms/tpe-form/tpe-form.html b/src/app/shared/forms/tpe-form/tpe-form.html index 54fb544..f4e8900 100644 --- a/src/app/shared/forms/tpe-form/tpe-form.html +++ b/src/app/shared/forms/tpe-form/tpe-form.html @@ -1,60 +1,110 @@ -
+
+ - -
- + +
+
- -
- + +
+
- -
- - @for (t of types; track t.value) { - {{ t.label }} + +
+ +
+ + + + +
+ +
+
+ + + +
+ + @for (os of osList; track os) { + {{ os }} }
+ + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ +
+
+ + + + +
+ @if (pointDeVenteLoading()){ +
+
+
+ + } + @if (pointsDevente.length > 0 && !pointDeVenteLoading()) { +
    + @for (pdv of pointsDevente; track pdv.id) { +
  • + {{ pdv.nom }} / {{ pdv.code }} +
  • + } +
+ } +
+
+ -
- - @for (s of allStatuses; track s) { - {{ s }} +
+ + @for (s of statuts; track s) { + {{ s.label }} }
- - -
- -
-
- - - -
- -
-
- - - -
- -
-
diff --git a/src/app/shared/forms/tpe-form/tpe-form.ts b/src/app/shared/forms/tpe-form/tpe-form.ts index ee1f634..f468711 100644 --- a/src/app/shared/forms/tpe-form/tpe-form.ts +++ b/src/app/shared/forms/tpe-form/tpe-form.ts @@ -1,11 +1,14 @@ 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 { 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 { TpeDevice, TpeStatus, 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({ selector: 'app-tpe-form', @@ -27,6 +30,13 @@ export class TpeForm { private _value?: TpeDevice; private _skipHydration = false; + + + pointDeVenteText = signal(''); + + pointDeVenteLoading = signal(false); + + @Input() set value(v: TpeDevice | undefined) { this._value = v; if (!this._skipHydration) { @@ -37,21 +47,89 @@ export class TpeForm { return this._value; } + osList = ['Windows', 'Linux', 'Mac Os']; + + pointsDevente:PointVente[] = []; + + statuts = [ + { + label: 'Actif', + value: 'ACTIF' + }, + { + label: 'Hors service', + value: 'HORS_SERVICE' + } + ] + form: FormGroup; submitted = false; - constructor(private fb: FormBuilder) { + constructor(private fb: FormBuilder, private pointsVenteService: PointsVenteService) { this.form = this.fb.group({ - imei: ['', [Validators.required, Validators.minLength(10)]], - serial: ['', Validators.required], - type: ['POS' as TpeType, Validators.required], - marque: ['', Validators.required], - modele: ['', Validators.required], - statut: ['VALIDE' as TpeStatus, Validators.required], - assigne: [false], + numeroSerie: ['', [Validators.required, Validators.minLength(10)]], + pointDeVenteId: ['', Validators.required], + statut: ['', Validators.required], + versionLogicielle: ['', 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'], }); + + effect(()=>{ + const text = this.pointDeVenteText(); + const params: ListParams = { + page: 0, + size: 10, + search: text + } + this.getPointDeventeFromText(text, params); + }) } + + + + pointDeVenteTextChange =(event: Event)=>{ + const value = (event.target as HTMLInputElement).value; + this.pointDeVenteText.set(value); + } + + 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 { const c = this.form.get(control); return !!(c && c.invalid && (c.touched || this.submitted)); @@ -67,25 +145,37 @@ export class TpeForm { private hydrateFromValue(v?: TpeDevice) { if (!v) { this.form.reset({ - imei: '', - serial: '', - type: 'POS', - statut: 'VALIDE', - assigne: false, - marque: '', - modele: '', + numeroSerie: '', + pointDeVenteId: 0, + statut: 'ACTIF', + versionLogicielle: '', + typeTerminal: '', + plateforme: '', + modeleAppareil: '', + systemeExploitation: '', + versionOs: '', + adresseIp: '', + adresseMac: '', + agentConnecteId: '', + journalSession: '', }); this.submitted = false; // Reset submitted flag when form is cleared return; } this.form.reset({ - imei: v.imei, - serial: v.serial, - type: v.type, - statut: v.statut, - assigne: !!v.assigne, - marque: v.marque, - modele: v.modele, + numeroSerie: v.numeroSerie, + pointDeVenteId: v.pointDeVenteId, + statut: 'ACTIF', + versionLogicielle: v.versionLogicielle, + 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, }); } @@ -95,20 +185,16 @@ export class TpeForm { this.form.markAllAsTouched(); return; } - const raw = this.form.getRawValue() as any; + const raw = this.form.getRawValue() as Partial; const payload: Partial = { - imei: raw.imei, - serial: raw.serial, - type: raw.type, - statut: raw.statut, - assigne: !!raw.assigne, - marque: raw.marque, - modele: raw.modele, + ...raw }; // Preserve existing id, statut, and assigne if editing if (this.value?.id) { payload.id = this.value.id; } + + this.save.emit(payload as TpeDevice); } @@ -116,11 +202,19 @@ export class TpeForm { this._skipHydration = true; // Prevent hydration when clearing value this._value = undefined; this.form.reset({ - imei: '', - serial: '', - type: 'POS', - marque: '', - modele: '', + numeroSerie: '', + pointDeVenteId: 0, + statut: 'ACTIF', + versionLogicielle: '', + typeTerminal: '', + plateforme: '', + modeleAppareil: '', + systemeExploitation: '', + versionOs: '', + adresseIp: '', + adresseMac: '', + agentConnecteId: '', + journalSession: '', }); this.submitted = false; this._skipHydration = false; // Re-enable hydration @@ -130,16 +224,4 @@ export class TpeForm { { label: 'POS', value: 'POS' as TpeType }, { label: 'Autre', value: 'OTHER' as TpeType }, ]; - - allStatuses: TpeStatus[] = [ - 'VALIDE', - 'INVALIDE', - 'EN_PANNE', - 'BLOQUE', - 'DISPONIBLE', - 'AFFECTE', - 'EN_MAINTENANCE', - 'HORS_SERVICE', - 'VOLE', - ]; } diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index d1c6022..a85696d 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,5 +1,5 @@ export const environment = { production: false, - apiBaseUrl: 'https://cuddly-years-work.loca.lt', + apiBaseUrl: 'http://192.168.40.225:8080', depouillementBaseUrl: 'http://192.168.1.235:8383' };