From f21a5fd4e650bb3492e09ce1677a15b88e24fceb Mon Sep 17 00:00:00 2001 From: OnlyPapy98 Date: Tue, 30 Dec 2025 19:09:01 +0100 Subject: [PATCH] test --- src/app/core/interfaces/resultat.ts | 1 + src/app/core/services/agent.ts | 76 ++-- src/app/core/services/depouillement.spec.ts | 40 ++ src/app/core/services/depouillement.ts | 53 +++ src/app/core/services/resultat.ts | 42 +- src/app/core/services/tpe.ts | 364 +++++++++--------- src/app/dashboard/dashboard-routing-module.ts | 2 +- src/app/dashboard/layout/layout.ts | 2 +- src/app/dashboard/pages/agents/agents.ts | 74 ++-- src/app/dashboard/pages/courses/courses.html | 18 +- src/app/dashboard/pages/courses/courses.ts | 20 +- src/app/dashboard/pages/rapport/rapport.html | 11 +- src/app/dashboard/pages/rapport/rapport.ts | 90 ++++- src/app/dashboard/pages/reunion/reunion.html | 2 + src/app/dashboard/pages/tpe/tpe.html | 2 + .../shared/forms/course-form/course-form.html | 4 +- .../shared/forms/course-form/course-form.ts | 11 +- .../forms/resultat-form/resultat-form.ts | 12 +- src/app/shared/forms/tpe-form/tpe-form.html | 18 + src/app/shared/forms/tpe-form/tpe-form.ts | 22 +- src/environments/environment.development.ts | 3 +- src/environments/environment.ts | 2 +- 22 files changed, 554 insertions(+), 315 deletions(-) create mode 100644 src/app/core/services/depouillement.spec.ts create mode 100644 src/app/core/services/depouillement.ts diff --git a/src/app/core/interfaces/resultat.ts b/src/app/core/interfaces/resultat.ts index 6ed11b4..52c3e86 100644 --- a/src/app/core/interfaces/resultat.ts +++ b/src/app/core/interfaces/resultat.ts @@ -24,6 +24,7 @@ export interface Resultat { totalMises: number; masseAPartager: number; prelevementsLegaux: number; + statut: ResultatStatut; montantRembourse: number; montantCagnotte: number; adeadHeat: boolean; diff --git a/src/app/core/services/agent.ts b/src/app/core/services/agent.ts index dfa0c16..64883e9 100644 --- a/src/app/core/services/agent.ts +++ b/src/app/core/services/agent.ts @@ -9,7 +9,7 @@ import { normalizePage } from '@shared/paging/normalize-page'; import { ListParams, PagedResult } from '@shared/paging/paging'; 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) // Note: When TPE is nested in Agent's tpes array, the agent field might be omitted or be a reference @@ -270,49 +270,39 @@ export class AgentService { // GET /api/v1/agents - List all list(params?: ListParams): Observable> { - if (USE_SERVER) { - let httpParams = new HttpParams(); - if (params) { - if (params.page) httpParams = httpParams.set('page', params.page.toString()); - if (params.size) httpParams = httpParams.set('perPage', params.size.toString()); - if (params.search) httpParams = httpParams.set('search', params.search); - if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey); - if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir); - } - - return this.http - .get(this.apiUrl, { - params: httpParams, - headers: this.getNgrokHeaders(), - }) - .pipe( - map((list) => { - const agents = list.map((apiAgent) => { - const transformed = this.transformAgent(apiAgent); - return transformed; - }); - // If pagination params provided, return paginated result - if (params) { - return normalizePage( - { data: agents, meta: { total: agents.length } }, - params.page || 1, - params.size || 10 - ); - } - // Otherwise return all as single page - return normalizePage( - { data: agents, meta: { total: agents.length } }, - 1, - agents.length - ); - }), - catchError((err) => { - console.error('Error fetching agents:', err); - return of(normalizePage({ data: [], meta: { total: 0 } }, 1, 10)); - }) - ); + let httpParams = new HttpParams(); + if (params) { + if (params.page) httpParams = httpParams.set('page', params.page.toString()); + if (params.size) httpParams = httpParams.set('perPage', params.size.toString()); + if (params.search) httpParams = httpParams.set('search', params.search); + if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey); + if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir); } - return of(normalizePage({ data: [], meta: { total: 0 } }, 1, 10)); + + return this.http + .get>(this.apiUrl, { + params: httpParams, + headers: this.getNgrokHeaders(), + }) + .pipe( + map((res) => { + const agents = res.content.map((apiAgent) => { + const transformed = this.transformAgent(apiAgent); + return transformed; + }); + // If pagination params provided, return paginated result + const resAgent = { + ...res, + content: agents + } + // Otherwise return all as single page + return resAgent; + }), + catchError((err) => { + console.error('Error fetching agents:', err); + return of(normalizePage({ content: [], meta: { total: 0 } }, 1, 10)); + }) + ); } // POST /api/v1/agents - Create diff --git a/src/app/core/services/depouillement.spec.ts b/src/app/core/services/depouillement.spec.ts new file mode 100644 index 0000000..62de19c --- /dev/null +++ b/src/app/core/services/depouillement.spec.ts @@ -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); + }); +}); diff --git a/src/app/core/services/depouillement.ts b/src/app/core/services/depouillement.ts new file mode 100644 index 0000000..3b55458 --- /dev/null +++ b/src/app/core/services/depouillement.ts @@ -0,0 +1,53 @@ +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: Course; + statut: ResultatStatut; + ordreArrivee: string; + datePublication?: string; // ISO string + dateValidation?: string; // ISO string + dateAnnulation?: string; // ISO string + notes?: string; + createdAt?: string; + updatedAt?: 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 { + 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): Observable<{}> { + return this.http.post(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 + }) + ); + } +} diff --git a/src/app/core/services/resultat.ts b/src/app/core/services/resultat.ts index 72648c7..e953664 100644 --- a/src/app/core/services/resultat.ts +++ b/src/app/core/services/resultat.ts @@ -137,28 +137,43 @@ export class ResultatService { }) .pipe( switchMap((raw) => { - // Some courses don't have a resultat yet. - // In that case the API returns 200 with a body like: - // { "message": "Aucun résultat disponible pour cette course" } - // We interpret this as "no resultat" and return undefined. - if ( - raw && - typeof raw === 'object' && - 'message' in raw && - !('id' in raw) && - !('ordreArrivee' in raw) - ) { + // Debug raw response shape to help detect API changes + console.debug(`ResultatService.getByCourseId(${courseId}) raw:`, raw); + // Handle common variants of server responses: + // - { message: '...' } -> no resultat + // - { id: ..., ordreArrivee: '...' } -> single resultat + // - [ { ... } ] -> array of resultats (pick first) + // - { content: [...] } -> take first content + + if (!raw) return of(undefined); + + // message-only response -> no resultat + if (typeof raw === 'object' && 'message' in raw && !('id' in raw) && !('ordreArrivee' in raw)) { return of(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(undefined); return this.courseService.getById(courseId).pipe( map((course) => { if (!course) { return undefined; } - return this.transformApiResponse(apiResultat, course); + return this.transformApiResponse(apiResultat!, course); }) ); }), @@ -264,6 +279,7 @@ export class ResultatService { course, // API now returns 'ordreArrivee' as CSV/string; normalize to number[] ordreArrivee: apiResultat.ordreArrivee, + statut: apiResultat.statut, // dead-heat not provided by new API shape — default to empty chevauxDeadHeat: [], // Financial fields may not be present in new API; default to 0 diff --git a/src/app/core/services/tpe.ts b/src/app/core/services/tpe.ts index 3fb22ff..a059dd3 100644 --- a/src/app/core/services/tpe.ts +++ b/src/app/core/services/tpe.ts @@ -9,64 +9,72 @@ import { normalizePage } from '@shared/paging/normalize-page'; import { ListParams, PagedResult } from '@shared/paging/paging'; 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 AgentApiResponse { - id: number; - code: string; - profile: string; - principalCode?: string; - caisseProfile?: string; - statut: string; - zone?: string; - kiosk?: string; - fonction?: string; - dateEmbauche?: string; - nom: string; - prenom: string; - autresNoms?: string; - dateNaissance?: string; - lieuNaissance?: string; - ville?: string; - adresse?: string; - autoriserAides?: boolean; - phone: string; - pin?: string; - limiteInferieure?: number; - limiteSuperieure?: number; - limiteParTransaction?: number; - limiteMinAirtime?: number; - limiteMaxAirtime?: number; - maxPeripheriques?: number; - limitId?: number; - nationalite?: string; - cni?: string; - cniDelivreeLe?: string; - cniDelivreeA?: string; - residence?: string; - autreAdresse1?: string; - statutMarital?: string; - epoux?: string; - autreTelephone?: string; - createdAt?: string; - updatedAt?: string; - createdBy?: string; + id: number, + code: String, + profil: String, + principalCode: String, + caisseProfile: String, + statut: AgentStatus, + zone: String, + kiosk: String, + fonction: String, + dateEmbauche: String, + nom: String, + "prenom": String, + "autresNoms": String, + "dateNaissance": String, + "lieuNaissance": String, + "ville": String, + "adresse": String, + "autoriserAides": boolean, + "phone": String, + "limiteInferieure": number, + "limiteSuperieure": number, + "limiteParTransaction": number, + "limiteMinAirtime": number, + "limiteMaxAirtime": number, + "maxPeripheriques": number, + "limitId": String, + "nationalite": String, + "cni": String, + "cniDelivreeLe": String, + "cniDelivreeA": String, + "residence": String, + "autreAdresse1": String, + "statutMarital": String, + "epoux": String, + "autreTelephone": String, + "createdAt": String, + "updatedAt": String, + "createdBy": String, + "terminauxIds": [ + number[] + ] } // Interface to match the API response structure interface TpeApiResponse { - id: number; - imei: string; - serial: string; - type: string; - marque: string; - modele: string; - statut: string; // API uses uppercase: VALIDE, INVALIDE, EN_PANNE, BLOQUE - agent?: AgentApiResponse; - assigne: boolean; - createdAt?: string; - updatedAt?: string; + id: number, + numeroSerie: String, + pointDeVenteId: number, + statut: TpeStatus, + derniereConnexion: String, + versionLogicielle: String, + typeTerminal: String, + plateforme: String, + modeleAppareil: String, + systemeExploitation: String, + versionOs: String, + adresseIp: String, + adresseMac: String, + agentConnecteId: number, + derniereConnexionAgent: String, + derniereDeconnexionAgent: String, + journalSession: String } // Stats interfaces @@ -114,29 +122,29 @@ export class TpeService { 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 { return { id: String(apiAgent.id), - code: apiAgent.code, - profile: apiAgent.profile, - principalCode: apiAgent.principalCode, - caisseProfile: apiAgent.caisseProfile, + code: String((apiAgent as any).code || ''), + profile: String((apiAgent as any).profile || ''), + principalCode: (apiAgent as any).principalCode ? String((apiAgent as any).principalCode) : undefined, + caisseProfile: (apiAgent as any).caisseProfile ? String((apiAgent as any).caisseProfile) : undefined, statut: apiAgent.statut as AgentStatus, - zone: apiAgent.zone, - kiosk: apiAgent.kiosk, - fonction: apiAgent.fonction, - dateEmbauche: apiAgent.dateEmbauche, - nom: apiAgent.nom, - prenom: apiAgent.prenom, - autresNoms: apiAgent.autresNoms, - dateNaissance: apiAgent.dateNaissance, - lieuNaissance: apiAgent.lieuNaissance, - ville: apiAgent.ville, - adresse: apiAgent.adresse, - autoriserAides: apiAgent.autoriserAides, - phone: apiAgent.phone, - pin: apiAgent.pin, + zone: (apiAgent as any).zone ? String((apiAgent as any).zone) : undefined, + kiosk: (apiAgent as any).kiosk ? String((apiAgent as any).kiosk) : undefined, + fonction: (apiAgent as any).fonction ? String((apiAgent as any).fonction) : undefined, + dateEmbauche: apiAgent.dateEmbauche ? String(apiAgent.dateEmbauche) : undefined, + nom: String(apiAgent.nom || ''), + prenom: String(apiAgent.prenom || ''), + autresNoms: apiAgent.autresNoms ? String(apiAgent.autresNoms) : undefined, + dateNaissance: apiAgent.dateNaissance ? String(apiAgent.dateNaissance) : undefined, + lieuNaissance: apiAgent.lieuNaissance ? String(apiAgent.lieuNaissance) : undefined, + ville: apiAgent.ville ? String(apiAgent.ville) : undefined, + adresse: apiAgent.adresse ? String(apiAgent.adresse) : undefined, + autoriserAides: Boolean(apiAgent.autoriserAides), + phone: String(apiAgent.phone || ''), + pin: (apiAgent as any).pin ? String((apiAgent as any).pin) : undefined, limiteInferieure: apiAgent.limiteInferieure, limiteSuperieure: apiAgent.limiteSuperieure, limiteParTransaction: apiAgent.limiteParTransaction, @@ -144,146 +152,138 @@ export class TpeService { limiteMaxAirtime: apiAgent.limiteMaxAirtime, maxPeripheriques: apiAgent.maxPeripheriques, limitId: apiAgent.limitId ? String(apiAgent.limitId) : undefined, - nationalite: apiAgent.nationalite, - cni: apiAgent.cni, - cniDelivreeLe: apiAgent.cniDelivreeLe, - cniDelivreeA: apiAgent.cniDelivreeA, - residence: apiAgent.residence, - autreAdresse1: apiAgent.autreAdresse1, - statutMarital: apiAgent.statutMarital, - epoux: apiAgent.epoux, - autreTelephone: apiAgent.autreTelephone, - createdAt: apiAgent.createdAt, - updatedAt: apiAgent.updatedAt, - createdBy: apiAgent.createdBy, + nationalite: apiAgent.nationalite ? String(apiAgent.nationalite) : undefined, + cni: apiAgent.cni ? String(apiAgent.cni) : undefined, + cniDelivreeLe: apiAgent.cniDelivreeLe ? String(apiAgent.cniDelivreeLe) : undefined, + cniDelivreeA: apiAgent.cniDelivreeA ? String(apiAgent.cniDelivreeA) : undefined, + residence: apiAgent.residence ? String(apiAgent.residence) : undefined, + autreAdresse1: apiAgent.autreAdresse1 ? String(apiAgent.autreAdresse1) : undefined, + statutMarital: apiAgent.statutMarital ? String(apiAgent.statutMarital) : undefined, + epoux: apiAgent.epoux ? String(apiAgent.epoux) : undefined, + autreTelephone: apiAgent.autreTelephone ? String(apiAgent.autreTelephone) : undefined, + createdAt: apiAgent.createdAt ? String(apiAgent.createdAt) : undefined, + updatedAt: apiAgent.updatedAt ? String(apiAgent.updatedAt) : undefined, + createdBy: apiAgent.createdBy ? String(apiAgent.createdBy) : undefined, }; } // 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); + return { id: String(apiTpe.id), - imei: apiTpe.imei, - serial: apiTpe.serial, - type: apiTpe.type as TpeType, - marque: apiTpe.marque, - modele: apiTpe.modele, - statut: this.transformStatut(apiTpe.statut), - agent: apiTpe.agent ? this.transformAgent(apiTpe.agent) : undefined, - assigne: apiTpe.assigne, - createdAt: apiTpe.createdAt, - updatedAt: apiTpe.updatedAt, + 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 + // Transform TpeDevice to API payload (best-effort) private transformToApiPayload(tpe: Partial): any { const payload: any = {}; - if (tpe.imei !== undefined) payload.imei = tpe.imei; - if (tpe.serial !== undefined) payload.serial = tpe.serial; - if (tpe.type !== undefined) payload.type = tpe.type; - if (tpe.marque !== undefined) payload.marque = tpe.marque; - if (tpe.modele !== undefined) payload.modele = tpe.modele; + 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((apiTpe) => this.transformTpe(apiTpe)), - catchError((err) => { - console.error(`Error fetching TPE ${id}:`, err); - return of(undefined); - }) - ); - } - return of(undefined); +getById(id: string): Observable { + if (USE_SERVER) { + return this.http.get(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }).pipe( + map((api) => this.transformTpe(api)), + catchError((err) => { + console.error(`Error fetching TPE ${id}:`, err); + return of(undefined); + }) + ); } + return of(undefined); +} + // GET /api/v1/tpes - List all list(params?: ListParams): Observable> { - if (USE_SERVER) { - let httpParams = new HttpParams(); - if (params) { - if (params.page) httpParams = httpParams.set('page', params.page.toString()); - if (params.size) httpParams = httpParams.set('perPage', params.size.toString()); - if (params.search) httpParams = httpParams.set('search', params.search); - if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey); - if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir); - } - - return this.http - .get(this.apiUrl, { - params: httpParams, - headers: this.getNgrokHeaders(), - }) - .pipe( - map((list) => { - const tpes = list.map((apiTpe) => this.transformTpe(apiTpe)); - // If pagination params provided, return paginated result - if (params) { - return normalizePage( - { data: tpes, meta: { total: tpes.length } }, - params.page || 1, - params.size || 10 - ); - } - // Otherwise return all as single page - return normalizePage( - { data: tpes, meta: { total: tpes.length } }, - 1, - tpes.length - ); - }), - catchError((err) => { - console.error('Error fetching TPEs:', err); - return of(normalizePage({ data: [], meta: { total: 0 } }, 1, 10)); - }) - ); + let httpParams = new HttpParams(); + if (params) { + if (params.page) httpParams = httpParams.set('page', params.page.toString()); + if (params.size) httpParams = httpParams.set('perPage', params.size.toString()); + if (params.search) httpParams = httpParams.set('search', params.search); + if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey); + if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir); } - return of(normalizePage({ data: [], meta: { total: 0 } }, 1, 10)); - } + + return this.http + .get>(this.apiUrl, { + params: httpParams, + headers: this.getNgrokHeaders(), + }) + .pipe( + map((list) => { + const content = (list.content || []).map((api) => this.transformTpe(api)); + return { ...list, content } as PagedResult; + }), + catchError((err) => { + console.error('Error fetching TPEs:', err); + return of(normalizePage({ content: [], meta: { total: 0 } }, 1, 10)); + }) + ); +} // POST /api/v1/tpes - Create - create(payload: Omit): Observable { - if (USE_SERVER) { - const apiPayload = this.transformToApiPayload(payload); - return this.http - .post(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() }) - .pipe( - map((apiTpe) => this.transformTpe(apiTpe)), - catchError((err) => { - console.error('Error creating TPE:', err); - throw err; - }) - ); - } - throw new Error('Server mode is required'); + create(payload: Partial): Observable { + const apiPayload = this.transformToApiPayload(payload); + return this.http + .post(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() }) + .pipe( + map((apiTpe) => this.transformTpe(apiTpe)), + catchError((err) => { + console.error('Error creating TPE:', err); + throw err; + }) + ); } - // PUT /api/v1/tpes/{id} - Update - update(id: string, payload: Partial): Observable { - if (USE_SERVER) { - const apiPayload = this.transformToApiPayload(payload); - return this.http - .put(`${this.apiUrl}/${id}`, apiPayload, { - headers: this.getNgrokHeaders(), +// 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, { + headers: this.getNgrokHeaders(), + }) + .pipe( + map((apiTpe) => this.transformTpe(apiTpe)), + catchError((err) => { + console.error(`Error updating TPE ${id}:`, err); + return of(undefined); }) - .pipe( - map((apiTpe) => this.transformTpe(apiTpe)), - catchError((err) => { - console.error(`Error updating TPE ${id}:`, err); - return of(undefined); - }) - ); - } - return of(undefined); - } + ); +} // DELETE /api/v1/tpes/{id} - Delete delete(id: string): Observable { diff --git a/src/app/dashboard/dashboard-routing-module.ts b/src/app/dashboard/dashboard-routing-module.ts index 18df032..b0e18fd 100644 --- a/src/app/dashboard/dashboard-routing-module.ts +++ b/src/app/dashboard/dashboard-routing-module.ts @@ -22,7 +22,7 @@ const routes: Routes = [ loadComponent: () => import('./pages/reunion/reunion').then((m) => m.ReunionList), }, { - path: 'rapport', + path: 'resultat', loadComponent: () => import('./pages/rapport/rapport').then((m) => m.Rapport), }, { diff --git a/src/app/dashboard/layout/layout.ts b/src/app/dashboard/layout/layout.ts index cb4ece2..1539fc2 100644 --- a/src/app/dashboard/layout/layout.ts +++ b/src/app/dashboard/layout/layout.ts @@ -46,7 +46,7 @@ export class Layout { { icon: '🏟️', label: 'Hippodromes', link: '/hippodromes' }, { icon: '📅', label: 'Reunions', link: '/reunions' }, { 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' }, ]; workspaceMenuItems: MenuItem[] = [ diff --git a/src/app/dashboard/pages/agents/agents.ts b/src/app/dashboard/pages/agents/agents.ts index f5550a9..1b4fd89 100644 --- a/src/app/dashboard/pages/agents/agents.ts +++ b/src/app/dashboard/pages/agents/agents.ts @@ -88,54 +88,16 @@ export class AgentsPage { } cols: TableColumn[] = [ - { key: 'code', label: 'Code', sortable: true }, - { key: 'nom', label: 'Nom', sortable: true }, - { key: 'prenom', label: 'Prénom', sortable: true }, - { key: 'phone', label: 'Téléphone', sortable: true }, - { - key: 'tpes', - label: 'TPE assignés', - cell: (a) => { - const tpes = this.agentTpesMap.get(a.id) || []; - if (tpes.length === 0) { - return 'Aucun'; - } - // 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 = `
${t.imei}
`; - 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 - ? `
${details}
` - : ''; - return `
${imei}${detailsHtml}
`; - }) - .join(' '); - - const moreHtml = - remaining > 0 - ? `
+${remaining} autre${ - remaining > 1 ? 's' : '' - }
` - : ''; - - return `
${tpeCards}${moreHtml}
`; - }, - }, + { key: 'code', label: 'Code', sortable: true, defaultVisible: true }, + { key: 'nomPrenom', label: 'Nom complet', sortable: true, defaultVisible: true, cell: (a) => `${a.nom} ${a.prenom}` }, + { key: 'profile', label: 'Profil', sortable: true, defaultVisible: 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: '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 }, + { key: 'tpes', label: 'TPE', cell: (a) => `${(this.getAgentTpes(a.id) || []).length}` }, + { key: 'limites', label: 'Limites', cell: (a) => this.formatLimits(a) }, + { key: 'dateEmbauche', label: 'Embauché le', cell: (a) => (a.dateEmbauche ? new Date(a.dateEmbauche).toLocaleDateString() : '') }, ]; tpeMap = new Map(); @@ -216,6 +178,26 @@ export class AgentsPage { return this.agentTpesMap.get(agentId) || []; } + renderStatutBadge(statut: Agent['statut'] | string | undefined): string { + if (!statut) return ''; + const s = String(statut).toUpperCase(); + if (s === 'ACTIF') { + return ` Actif`; + } + if (s === 'INACTIF') { + return ` Inactif`; + } + return ` Suspendu`; + } + + formatLimits(a: Agent): string { + const parts: string[] = []; + const nf = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }); + if (a.limiteInferieure !== undefined) parts.push(nf.format(a.limiteInferieure)); + if (a.limiteSuperieure !== undefined) parts.push(nf.format(a.limiteSuperieure)); + return parts.length ? parts.join(' — ') : ''; + } + onSearch(q: string) { this.search.set(q); this.page.set(1); diff --git a/src/app/dashboard/pages/courses/courses.html b/src/app/dashboard/pages/courses/courses.html index 59e7f13..1557974 100644 --- a/src/app/dashboard/pages/courses/courses.html +++ b/src/app/dashboard/pages/courses/courses.html @@ -15,7 +15,7 @@ -
En cours
+
Ouverts
{{ runningCourses() }}
@@ -29,7 +29,7 @@
-
Par type
+
Nombre de statuts
@for (type of (byType() | keyvalue); track type.key) {
@@ -116,19 +116,27 @@
+ @if (modalOpen()) { - @if(modalOpen()) { - }
Annuler - Enregistrer + + Enregistrer +
+ } @if(selectedCourse()) { { this.rows.set(res.content); this.total.set(res.totalElements); - this.totalRunning.set(0); - this.totalClosed.set(0); - this.totalByType.set({}); + this.totalRunning.set(res.content.filter(c=> c.statut === String(CourseStatut.OUVERT)).length); + this.totalClosed.set(res.content.filter(c=>c.statut === String(CourseStatut.FERME)).length); + this.totalByType.set({ + + }); // Fetch resultats for all courses in parallel const courseIds = res.content.map((c) => c.id); @@ -465,7 +467,10 @@ export class Course { } // 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: () => { this.closeResultatModal(); this.fetch({ @@ -495,7 +500,10 @@ export class Course { } // 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: () => { this.closeResultatModal(); this.fetch({ diff --git a/src/app/dashboard/pages/rapport/rapport.html b/src/app/dashboard/pages/rapport/rapport.html index 236fbfb..5f32c86 100644 --- a/src/app/dashboard/pages/rapport/rapport.html +++ b/src/app/dashboard/pages/rapport/rapport.html @@ -1,13 +1,18 @@
-

Rapport — Courses avec résultats

- Récupérer le rapport +

Résultats — Courses

+ Récupérer les résultats
- +
diff --git a/src/app/dashboard/pages/rapport/rapport.ts b/src/app/dashboard/pages/rapport/rapport.ts index ba3f0ac..b72cb75 100644 --- a/src/app/dashboard/pages/rapport/rapport.ts +++ b/src/app/dashboard/pages/rapport/rapport.ts @@ -4,8 +4,11 @@ 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 { 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 { Depouillement, ResultatCourse } from 'src/app/core/services/depouillement'; +import { Course } from 'src/app/core/interfaces/course'; +import { toast } from 'ngx-sonner'; @Component({ standalone: true, @@ -17,6 +20,7 @@ import { ResultatService } from 'src/app/core/services/resultat'; export class Rapport { rows = signal([]); loading = signal(false); + sending = signal>(new Map()); // Pagination state page = signal(1); perPage = signal(10); @@ -34,7 +38,7 @@ export class Rapport { { key: 'dateValidation', label: 'Date validation' }, ]; - constructor(private api: ResultatService) { + constructor(private api: ResultatService, private depouillement: Depouillement) { // initial load this.fetch(); } @@ -85,10 +89,90 @@ export class Rapport { openReport(row: ResultatApiResponse) { try { // 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'); } catch (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: Course = { + id: String((row as any).courseId ?? ''), + hippodrome: undefined, + reunionNumero: Number((row as any).reunionNumero ?? 0), + reunionDate: '', + nom: row.courseNom ?? '', + numero: Number(row.courseNumero ?? 0), + heureDepartPrevue: '', + discipline: '', + distanceMetres: 0, + categorie: '', + nombrePartants: 0, + statut: '', + annulee: false, + reporteeMemeJour: false, + reporteeAutreJour: false, + incidentTechnique: false, + nonPartants: [], + typesParisOuverts: [], + }; + + const payload: ResultatCourse = { + id: Number(row.id as any), + course, + statut: (row.statut as any) ?? (0 as any), + ordreArrivee: String(row.ordreArrivee ?? ''), + datePublication: row.datePublication ?? row.createdAt, + dateValidation: row.dateValidation, + dateAnnulation: row.dateAnnulation, + notes: '', + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; + + this.depouillement.sendResultat(payload).subscribe({ + next: (res) => { + console.debug('Depouillement sent:', 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); + }, + }); + } } diff --git a/src/app/dashboard/pages/reunion/reunion.html b/src/app/dashboard/pages/reunion/reunion.html index 71e194b..5cccaa7 100644 --- a/src/app/dashboard/pages/reunion/reunion.html +++ b/src/app/dashboard/pages/reunion/reunion.html @@ -72,6 +72,7 @@ />
+ @if (modalOpen()) { Enregistrer
+ }
diff --git a/src/app/dashboard/pages/tpe/tpe.html b/src/app/dashboard/pages/tpe/tpe.html index 314e04e..b5b47eb 100644 --- a/src/app/dashboard/pages/tpe/tpe.html +++ b/src/app/dashboard/pages/tpe/tpe.html @@ -143,11 +143,13 @@ + @if (modalOpen()) { + }
Annuler Enregistrer diff --git a/src/app/shared/forms/course-form/course-form.html b/src/app/shared/forms/course-form/course-form.html index 26585c7..c60af97 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 c8ab899..91036c4 100644 --- a/src/app/shared/forms/course-form/course-form.ts +++ b/src/app/shared/forms/course-form/course-form.ts @@ -216,6 +216,8 @@ 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; } @@ -246,6 +248,9 @@ 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); @@ -274,6 +279,9 @@ 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(); } @@ -292,7 +300,6 @@ 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 @@ -318,6 +325,8 @@ 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({ diff --git a/src/app/shared/forms/resultat-form/resultat-form.ts b/src/app/shared/forms/resultat-form/resultat-form.ts index fa63816..0e08731 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 } from 'src/app/core/interfaces/resultat'; +import { Resultat, ResultatStatut } 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((): 'PROVISOIRE' | 'OFFICIEL' | 'ANNULE' | 'EN_ATTENTE' => { - return this.resultat ? 'PROVISOIRE' : 'EN_ATTENTE'; - }); + statut = computed((): ResultatStatut => { + return this.resultat ? this.resultat.statut : ResultatStatut.EN_ATTENTE; +}); canValidate(): boolean { - return this.statut() === 'EN_ATTENTE'; + return String(this.statut()) === "EN_ATTENTE"; } canConfirm(): boolean { - return this.statut() === 'PROVISOIRE'; + return String(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 993e779..54fb544 100644 --- a/src/app/shared/forms/tpe-form/tpe-form.html +++ b/src/app/shared/forms/tpe-form/tpe-form.html @@ -25,6 +25,24 @@
+ + +
+ + @for (s of allStatuses; track s) { + {{ s }} + } + +
+
+ + + +
+ +
+
+
diff --git a/src/app/shared/forms/tpe-form/tpe-form.ts b/src/app/shared/forms/tpe-form/tpe-form.ts index 52d968b..ee1f634 100644 --- a/src/app/shared/forms/tpe-form/tpe-form.ts +++ b/src/app/shared/forms/tpe-form/tpe-form.ts @@ -5,7 +5,7 @@ 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, TpeType } from 'src/app/core/interfaces/tpe'; +import { TpeDevice, TpeStatus, TpeType } from 'src/app/core/interfaces/tpe'; @Component({ selector: 'app-tpe-form', @@ -47,6 +47,8 @@ export class TpeForm { type: ['POS' as TpeType, Validators.required], marque: ['', Validators.required], modele: ['', Validators.required], + statut: ['VALIDE' as TpeStatus, Validators.required], + assigne: [false], }); } @@ -68,6 +70,8 @@ export class TpeForm { imei: '', serial: '', type: 'POS', + statut: 'VALIDE', + assigne: false, marque: '', modele: '', }); @@ -78,6 +82,8 @@ export class TpeForm { imei: v.imei, serial: v.serial, type: v.type, + statut: v.statut, + assigne: !!v.assigne, marque: v.marque, modele: v.modele, }); @@ -94,6 +100,8 @@ export class TpeForm { imei: raw.imei, serial: raw.serial, type: raw.type, + statut: raw.statut, + assigne: !!raw.assigne, marque: raw.marque, modele: raw.modele, }; @@ -122,4 +130,16 @@ 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 408a765..f6d87ec 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,4 +1,5 @@ export const environment = { production: false, - apiBaseUrl: 'http://192.168.1.235:8280', + apiBaseUrl: 'http://192.168.1.235:8381', + depouillementBaseUrl: 'http://192.168.1.235:8383' }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 408a765..ebdba47 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,4 +1,4 @@ export const environment = { production: false, - apiBaseUrl: 'http://192.168.1.235:8280', + apiBaseUrl: 'http://192.168.1.235:8383', };