import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { map, catchError, switchMap } from 'rxjs/operators'; import { TpeDevice, TpeStatus, TpeType } from '../interfaces/tpe'; import { Agent, AgentStatus } from '../interfaces/agent'; import { environment } from 'src/environments/environment.development'; import { normalizePage } from '@shared/paging/normalize-page'; import { ListParams, PagedResult } from '@shared/paging/paging'; const USE_SERVER = true; const API_BASE = '/api/terminaux'; // Interface to match the API response structure for Agent (nested in TPE) interface AgentApiResponse { 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, 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 interface CountByStatutResponse { [key: string]: number; } // Assignment stats is just a number (count of assigned TPEs) type AssignesStatsResponse = number; @Injectable({ providedIn: 'root' }) export class TpeService { private apiUrl = environment.apiBaseUrl + API_BASE; constructor(private http: HttpClient) {} // Helper method to get ngrok bypass headers 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' } : {}; } // Transform API statut to interface statut (both use uppercase now) 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', ]; return validStatuses.includes(upperStatut) ? upperStatut : 'INVALIDE'; } // Transform interface statut to API statut (both use uppercase now, so direct return) private transformStatutToApi(statut: TpeStatus): string { return statut; // Already uppercase, no transformation needed } // Transform API Agent response to Agent (lightweight mapping) private transformAgent(apiAgent: AgentApiResponse): Agent { return { id: String(apiAgent.id), 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 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, limiteMinAirtime: apiAgent.limiteMinAirtime, limiteMaxAirtime: apiAgent.limiteMaxAirtime, maxPeripheriques: apiAgent.maxPeripheriques, limitId: apiAgent.limitId ? String(apiAgent.limitId) : undefined, 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: 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; } // 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)), 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> { 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 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: 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 { 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); }) ); } // DELETE /api/v1/tpes/{id} - Delete delete(id: string): Observable { if (USE_SERVER) { return this.http .delete(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }) .pipe( map(() => true), catchError((err) => { console.error(`Error deleting TPE ${id}:`, err); return of(false); }) ); } return of(false); } // PATCH /api/v1/tpes/{id}/statut - Update statut updateStatut(id: string, statut: TpeStatus): Observable { if (USE_SERVER) { return this.http .patch( `${this.apiUrl}/${id}/statut`, { statut: this.transformStatutToApi(statut) }, { headers: this.getNgrokHeaders() } ) .pipe( map((apiTpe) => this.transformTpe(apiTpe)), catchError((err) => { console.error(`Error updating TPE statut ${id}:`, err); return of(undefined); }) ); } return of(undefined); } // PATCH /api/v1/tpes/{id}/liberer - Liberate TPE (updates whole TPE, sets assigne to false and statut to DISPONIBLE) liberer(id: string): 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 of(undefined); }) ); } 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(), }) .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(), }) .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 getCountByStatut(): Observable { if (USE_SERVER) { return this.http .get(`${this.apiUrl}/stats/count-by-statut`, { headers: this.getNgrokHeaders(), }) .pipe( catchError((err) => { console.error('Error fetching TPE count by statut:', err); return of({}); }) ); } return of({}); } // GET /api/v1/tpes/stats/assignes - Get assignment stats (returns a number) getAssignesStats(): Observable { if (USE_SERVER) { return this.http .get(`${this.apiUrl}/stats/assignes`, { headers: this.getNgrokHeaders(), }) .pipe( catchError((err) => { console.error('Error fetching TPE assignment stats:', err); return of(0); }) ); } return of(0); } // GET /api/v1/tpes/search - Search search(query: string): Observable { if (USE_SERVER) { return this.http .get(`${this.apiUrl}/search`, { params: { q: query.trim() }, headers: this.getNgrokHeaders(), }) .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([]); } }