import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of, forkJoin } from 'rxjs'; import { map, catchError, switchMap } from 'rxjs/operators'; import { Course, CourseType, CourseStatut } from '../interfaces/course'; import { normalizePage } from '@shared/paging/normalize-page'; import { PaginatedHttpService } from '@shared/paging/paginated-http.service'; import { ListParams, PagedResult } from '@shared/paging/paging'; import { environment } from 'src/environments/environment.development'; import { Reunion } from '../interfaces/reunion'; import { ReunionService } from './reunion'; import { NonPartantService } from './non-partant'; import { ServicesUtils } from './services-utils'; import { HippodromeService } from './hippodrome'; const USE_SERVER = true; const API_BASE = '/api/courses'; // Interface to match the API response structure for Course export interface CourseApiResponse { id: string; hippodromeId: number; reunionNumero: number; reunionDate: string; nom: string; numero: number; heureDepartPrevue: string; discipline: string; distanceMetres: number; categorie: string; nombrePartants: number; statut: string; annulee: boolean; reporteeMemeJour: boolean; reporteeAutreJour: boolean; incidentTechnique: boolean; nonPartants: Array; typesParisOuverts: Array; } export interface NonApiRequest { nonPartants: String[] } @Injectable({ providedIn: 'root' }) export class CourseService { private apiUrl = environment.apiBaseUrl + API_BASE; constructor( private http: HttpClient, private paginatedHttp: PaginatedHttpService, private servivesUtil: ServicesUtils, private hippodromeService: HippodromeService, private reunionService: ReunionService, // Inject ReunionService private nonPartantService: NonPartantService // Inject NonPartantService ) {} // 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' } : {}; } list(params: ListParams): Observable> { const coursesList = this.http.get>(this.apiUrl, { headers: this.getNgrokHeaders(), params: this.servivesUtil.getParamsFromModel(params), }); return coursesList.pipe( switchMap((res) => { const items = res.content ?? []; // Transforme chaque course en Observable const courseObservables = items.map((apiCourse) => this.hippodromeService.getById(String(apiCourse.hippodromeId)).pipe( // Map le hippodrome récupéré dans l'objet Course final map((hippodrome) => ({ id: apiCourse.id, hippodrome: hippodrome ?? undefined, reunionNumero: apiCourse.reunionNumero, reunionDate: apiCourse.reunionDate, nom: apiCourse.nom, numero: apiCourse.numero, heureDepartPrevue: apiCourse.heureDepartPrevue, discipline: apiCourse.discipline, distanceMetres: apiCourse.distanceMetres, categorie: apiCourse.categorie, nombrePartants: apiCourse.nombrePartants, statut: apiCourse.statut, annulee: apiCourse.annulee, reporteeMemeJour: apiCourse.reporteeMemeJour, reporteeAutreJour: apiCourse.reporteeAutreJour, incidentTechnique: apiCourse.incidentTechnique, nonPartants: apiCourse.nonPartants || [], typesParisOuverts: apiCourse.typesParisOuverts || [], })) ) ); // ForkJoin pour attendre que tous les hippodromes soient résolus return forkJoin(courseObservables).pipe( map((courses) => { const mapped: PagedResult = { pageable: res.pageable, totalPages: res.totalPages, totalElements: res.totalElements, content: courses, }; return mapped; }) ); }), catchError((err) => { console.error('Error fetching courses list:', err); return of({ content: [], pageable: {pageNumber: 1, pageSize: 0, total: 0}, totalPages:1, totalElements:0 } as PagedResult); }) ); } private applyClientFilters(data: Course[], params: ListParams): Course[] { let filtered = [...data]; // Search filter // const q = (params.search ?? '').toLowerCase(); // if (q) { // filtered = filtered.filter((c) => { // const reunionName = c.reunion?.nom?.toLowerCase?.() ?? ''; // const hippodromeName = c.reunion?.hippodrome?.nom?.toLowerCase?.() ?? ''; // return ( // c.nom.toLowerCase().includes(q) || // c.type.toLowerCase().includes(q) || // reunionName.includes(q) || // hippodromeName.includes(q) // ); // }); // } // Sort if (params.sortKey && params.sortDir) { const { sortKey, sortDir } = params; filtered.sort((a: any, b: any) => { const getValue = (obj: any, path: string): any => path.split('.').reduce((o, key) => o?.[key], obj); const va = getValue(a, sortKey); const vb = getValue(b, sortKey); const sa = va == null ? '' : String(va); const sb = vb == null ? '' : String(vb); const cmp = sa.localeCompare(sb, 'fr', { numeric: true }); return sortDir === 'asc' ? cmp : -cmp; }); } return filtered; } getById(id: string): Observable { if (USE_SERVER) { return this.http .get(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }) .pipe( switchMap((apiCourse) => { // Fetch the reunion (non-partants are already included in the API response) return this.hippodromeService.getById(String(apiCourse.hippodromeId)).pipe( map((hippodrome) => { if (!hippodrome) { return undefined; } return { id: String(apiCourse.id), hippodrome: hippodrome ?? undefined, reunionNumero: apiCourse.reunionNumero, reunionDate: apiCourse.reunionDate, nom: apiCourse.nom, numero: apiCourse.numero, heureDepartPrevue: apiCourse.heureDepartPrevue, discipline: apiCourse.discipline, distanceMetres: apiCourse.distanceMetres, categorie: apiCourse.categorie, nombrePartants: apiCourse.nombrePartants, statut: apiCourse.statut, annulee: apiCourse.annulee, reporteeMemeJour: apiCourse.reporteeMemeJour, reporteeAutreJour: apiCourse.reporteeAutreJour, incidentTechnique: apiCourse.incidentTechnique, nonPartants: apiCourse.nonPartants || [], typesParisOuverts: apiCourse.typesParisOuverts || [] }; }) ); }), catchError((err) => { console.error(`Error fetching course ${id}:`, err); return of(undefined); }) ); } return of(undefined); } // getByReunionId(reunionId: string): Observable { // if (USE_SERVER) { // return this.http // .get(`${this.apiUrl}/reunion/${reunionId}`, { // headers: this.getNgrokHeaders(), // }) // .pipe( // switchMap((apiData) => { // // Fetch the reunion once // return this.reunionService.getById(reunionId).pipe( // map((reunion) => { // if (!reunion) { // return []; // } // // Transform all courses with the same reunion // return apiData.map((apiCourse) => ({ // id: String(apiCourse.id), // type: apiCourse.type, // numero: apiCourse.numero, // nom: apiCourse.nom, // dateDepartCourse: apiCourse.dateDepartCourse, // dateDebutParis: apiCourse.dateDebutParis, // dateFinParis: apiCourse.dateFinParis, // reunion, // reunionCourse: apiCourse.reunionCourse, // particularite: apiCourse.particularite, // partants: apiCourse.partants, // distance: apiCourse.distance, // condition: apiCourse.condition, // statut: apiCourse.statut as CourseStatut, // nonPartants: apiCourse.nonPartants || [], // estTerminee: apiCourse.estTerminee, // estAnnulee: apiCourse.estAnnulee, // nombreChevauxInscrits: apiCourse.nombreChevauxInscrits, // adeadHeat: apiCourse.adeadHeat, // createdBy: apiCourse.createdBy, // validatedBy: apiCourse.validatedBy, // createdAt: apiCourse.createdAt || new Date().toISOString(), // updatedAt: apiCourse.updatedAt || new Date().toISOString(), // })); // }) // ); // }), // catchError((err) => { // console.error(`Error fetching courses for reunion ${reunionId}:`, err); // return of([]); // }) // ); // } // return of([]); // } // search(query: string): Observable { // if (USE_SERVER) { // return this.http // .get(`${this.apiUrl}/search`, { // params: { q: query.trim() }, // headers: this.getNgrokHeaders(), // }) // .pipe( // switchMap((apiData) => { // // Extract unique reunionIds // const uniqueReunionIds = [...new Set(apiData.map((c) => String(c.reunionId)))]; // // Fetch all reunions in parallel // const reunionRequests = uniqueReunionIds.map((id) => // this.reunionService // .getById(id) // .pipe(catchError(() => of(undefined))) // ); // return forkJoin(reunionRequests).pipe( // map((reunions) => { // // Create a map of reunionId -> Reunion // const reunionMap = new Map(); // uniqueReunionIds.forEach((id, index) => { // const reunion = reunions[index]; // if (reunion) { // reunionMap.set(id, reunion); // } // }); // // Transform API data to Course objects // return apiData // .map((apiCourse) => { // const reunion = reunionMap.get(String(apiCourse.reunionId)); // if (!reunion) { // return null; // } // return { // id: String(apiCourse.id), // type: apiCourse.type, // numero: apiCourse.numero, // nom: apiCourse.nom, // dateDepartCourse: apiCourse.dateDepartCourse, // dateDebutParis: apiCourse.dateDebutParis, // dateFinParis: apiCourse.dateFinParis, // reunion, // reunionCourse: apiCourse.reunionCourse, // particularite: apiCourse.particularite, // partants: apiCourse.partants, // distance: apiCourse.distance, // condition: apiCourse.condition, // statut: apiCourse.statut as CourseStatut, // nonPartants: apiCourse.nonPartants || [], // estTerminee: apiCourse.estTerminee, // estAnnulee: apiCourse.estAnnulee, // nombreChevauxInscrits: apiCourse.nombreChevauxInscrits, // adeadHeat: apiCourse.adeadHeat, // createdBy: apiCourse.createdBy, // validatedBy: apiCourse.validatedBy, // createdAt: apiCourse.createdAt || new Date().toISOString(), // updatedAt: apiCourse.updatedAt || new Date().toISOString(), // } as Course; // }) // .filter((c): c is Course => c !== null); // }) // ); // }), // catchError((err) => { // console.error(`Error searching courses with query ${query}:`, err); // return of([]); // }) // ); // } // return of([]); // } create(payload: Partial): Observable { if (USE_SERVER) { // Transform payload to API format (send reunionId instead of reunion object) const apiPayload: Partial = payload; return this.http .post(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() }) .pipe( switchMap((apiCourse) => { // Fetch the reunion to build the full Course object return this.hippodromeService.getById(String(apiCourse.hippodromeId)).pipe( map((hippodrome) => { if (!hippodrome) { throw new Error('Hippodrome not found'); } const item: Course = { id: String(apiCourse.id), hippodrome: hippodrome ?? undefined, reunionNumero: apiCourse.reunionNumero, reunionDate: apiCourse.reunionDate, nom: apiCourse.nom, numero: apiCourse.numero, heureDepartPrevue: apiCourse.heureDepartPrevue, discipline: apiCourse.discipline, distanceMetres: apiCourse.distanceMetres, categorie: apiCourse.categorie, nombrePartants: apiCourse.nombrePartants, statut: apiCourse.statut, annulee: apiCourse.annulee, reporteeMemeJour: apiCourse.reporteeMemeJour, reporteeAutreJour: apiCourse.reporteeAutreJour, incidentTechnique: apiCourse.incidentTechnique, nonPartants: apiCourse.nonPartants || [], typesParisOuverts: apiCourse.typesParisOuverts || [] }; return item; }) ); }), catchError((err) => { console.error('Error creating course:', err); throw err; }) ); } throw new Error('Server mode is required'); } update(id: string, payload: Partial): Observable { if (USE_SERVER) { // Transform payload to API format (send reunionId instead of reunion object return this.http .put(`${this.apiUrl}/${id}`, payload, { headers: this.getNgrokHeaders(), }) .pipe( switchMap((apiCourse) => { // Fetch the reunion to build the full Course object return this.hippodromeService.getById(String(apiCourse.hippodromeId)).pipe( map((hippodrome) => { if (!hippodrome) { throw new Error('Reunion not found'); } return { id: String(apiCourse.id), hippodrome: hippodrome ?? undefined, reunionNumero: apiCourse.reunionNumero, reunionDate: apiCourse.reunionDate, nom: apiCourse.nom, numero: apiCourse.numero, heureDepartPrevue: apiCourse.heureDepartPrevue, discipline: apiCourse.discipline, distanceMetres: apiCourse.distanceMetres, categorie: apiCourse.categorie, nombrePartants: apiCourse.nombrePartants, statut: apiCourse.statut, annulee: apiCourse.annulee, reporteeMemeJour: apiCourse.reporteeMemeJour, reporteeAutreJour: apiCourse.reporteeAutreJour, incidentTechnique: apiCourse.incidentTechnique, nonPartants: apiCourse.nonPartants || [], typesParisOuverts: apiCourse.typesParisOuverts || [] }; }) ); }), catchError((err) => { console.error(`Error updating course ${id}:`, err); return of(undefined); }) ); } throw new Error('Server mode is required'); } updateStatut(id: string, statut: CourseStatut): Observable { if (USE_SERVER) { return this.http .patch( `${this.apiUrl}/${id}/statut`, { statut }, { headers: this.getNgrokHeaders() } ) .pipe( catchError((err) => { console.error(`Error updating course statut ${id}:`, err); return this.update(id, { statut }); }) ); } return this.update(id, { statut }); } 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 course ${id}:`, err); return of(false); }) ); } throw new Error('Server mode is required'); } addNonPartant(courseId: string, npList: string[]) { const payload = { nonPartants: [...npList] } console.warn('addNonPartant is deprecated. Use setNonPartants instead.'); return this.setNonPartants(courseId, payload); } setNonPartants(courseId: string, npList: NonApiRequest): Observable { return this.nonPartantService.replaceNonPartants(courseId, npList).pipe( map((updatedNonPartants) => updatedNonPartants), catchError((err) => { console.error(`Error setting nonPartants for course ${courseId}:`, err); return of(undefined); }) ); } }