import { Injectable, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of, forkJoin } from 'rxjs'; import { map, catchError, switchMap } from 'rxjs/operators'; import { Reunion } from '../interfaces/reunion'; import { Hippodrome } from '../interfaces/hippodrome'; 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 { HippodromeService } from './hippodrome'; // API response interface (has hippodromeId instead of hippodrome) interface ReunionApiResponse { id: string; code: string; nom: string; date: string; numero: number; statut: string; hippodromeId: string; totalCourses?: number; createdAt: string; updatedAt: string; } const USE_SERVER = true; const API_BASE = '/api/reunions'; @Injectable({ providedIn: 'root' }) export class ReunionService { private apiUrl = environment.apiBaseUrl + API_BASE; private store = signal([]); constructor( private http: HttpClient, private paginatedHttp: PaginatedHttpService, private hippodromeService: HippodromeService ) {} // 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, usePaginationEndpoint: boolean = true ): Observable> { if (USE_SERVER) { if (usePaginationEndpoint) { return this.paginatedHttp .fetch(this.apiUrl, params, { zeroBasedPageIndex: false, buildSort: (key, dir) => (key && dir ? ['sort', `${key},${dir}`] : null), }) .pipe( switchMap((pagedResult) => { // Handle empty data case if (!pagedResult.content || pagedResult.content.length === 0) { return of({ ...pagedResult, content: [], pageable: { ...pagedResult.pageable, }, }); } // Extract unique hippodrome IDs from the paginated data const uniqueHippodromeIds = [ ...new Set(pagedResult.content.map((r) => String(r.hippodromeId))), ]; // Handle case where there are no unique IDs if (uniqueHippodromeIds.length === 0) { return of({ ...pagedResult, content: [], pageable: { ...pagedResult.pageable, }, }); } // Fetch all unique hippodromes in parallel const hippodromeRequests = uniqueHippodromeIds.map((id) => this.hippodromeService .getById(id) .pipe(catchError(() => of())) ); // Fetch courses to calculate counts per reunion const coursesRequest = this.http .get(`${environment.apiBaseUrl}/api/courses`, { headers: this.getNgrokHeaders(), }) .pipe( catchError(() => of([])), map((data) => data || []) ); return forkJoin({ hippodromes: forkJoin(hippodromeRequests), courses: coursesRequest, }).pipe( map(({ hippodromes, courses }) => { // Create a map of hippodrome ID to hippodrome object const hippodromeMap = new Map(); uniqueHippodromeIds.forEach((id, index) => { const hippodrome = hippodromes[index]; if (hippodrome) { hippodromeMap.set(id, hippodrome); } }); // Count courses per reunion const courseCountMap = new Map(); courses.forEach((course: any) => { const reunionId = String(course.reunionId || course.reunion?.id); if (reunionId && reunionId !== 'undefined' && reunionId !== 'null') { courseCountMap.set( reunionId, (courseCountMap.get(reunionId) || 0) + 1 ); } }); // Transform API responses to Reunion objects const transformedData: Reunion[] = pagedResult.content .map((apiReunion) => { const hippodrome = hippodromeMap.get(String(apiReunion.hippodromeId)); if (!hippodrome) { return null; } const reunionId = String(apiReunion.id); const courseCount = courseCountMap.get(reunionId) ?? apiReunion.totalCourses ?? 0; return { id: reunionId, code: apiReunion.code, nom: apiReunion.nom, date: apiReunion.date, numero: apiReunion.numero, statut: apiReunion.statut as any, hippodrome, totalCourses: courseCount, createdAt: apiReunion.createdAt, updatedAt: apiReunion.updatedAt, } as Reunion; }) .filter((r): r is Reunion => r !== null && r !== undefined); return { ...pagedResult, content: transformedData, } }) ); }), catchError((err) => { console.error('Error fetching reunions:', err); return this.getMockList(params); }) ); } else { // Fetch all data and apply client-side pagination return this.http .get(this.apiUrl, { headers: this.getNgrokHeaders() }) .pipe( switchMap((apiData) => { // Handle empty data case if (!apiData || apiData.length === 0) { return of( normalizePage( { data: [], meta: { total: 0, uniqueHippodromes: 0, upcomingReunions: 0, pastReunions: 0 }, }, params.page, params.size ) ); } // Extract unique hippodrome IDs const uniqueHippodromeIds = [...new Set(apiData.map((r) => String(r.hippodromeId)))]; // Handle case where there are no unique IDs (shouldn't happen, but be safe) if (uniqueHippodromeIds.length === 0) { return of( normalizePage( { data: [], meta: { total: 0, uniqueHippodromes: 0, upcomingReunions: 0, pastReunions: 0 }, }, params.page, params.size ) ); } // Fetch all unique hippodromes and all courses in parallel const hippodromeRequests = uniqueHippodromeIds.map((id) => this.hippodromeService .getById(id) .pipe(catchError(() => of(undefined))) ); // Fetch courses to calculate counts per reunion const coursesRequest = this.http .get(`${environment.apiBaseUrl}/api/v1/courses`, { headers: this.getNgrokHeaders(), }) .pipe( catchError(() => of([])), map((data) => data || []) ); return forkJoin({ hippodromes: forkJoin(hippodromeRequests), courses: coursesRequest, }).pipe( map(({ hippodromes, courses }) => { // Create a map of hippodrome ID to hippodrome object const hippodromeMap = new Map(); uniqueHippodromeIds.forEach((id, index) => { const hippodrome = hippodromes[index]; if (hippodrome) { hippodromeMap.set(id, hippodrome); } }); // Count courses per reunion const courseCountMap = new Map(); courses.forEach((course: any) => { const reunionId = String(course.reunionId || course.reunion?.id); if (reunionId && reunionId !== 'undefined' && reunionId !== 'null') { courseCountMap.set( reunionId, (courseCountMap.get(reunionId) || 0) + 1 ); } }); // Transform API responses to Reunion objects const transformedData: Reunion[] = apiData .map((apiReunion) => { const hippodrome = hippodromeMap.get(String(apiReunion.hippodromeId)); if (!hippodrome) { // Skip if hippodrome not found return null; } const reunionId = String(apiReunion.id); const courseCount = courseCountMap.get(reunionId) ?? apiReunion.totalCourses ?? 0; return { id: reunionId, code: apiReunion.code, nom: apiReunion.nom, date: apiReunion.date, numero: apiReunion.numero, statut: apiReunion.statut as any, hippodrome, totalCourses: courseCount, createdAt: apiReunion.createdAt, updatedAt: apiReunion.updatedAt, } as Reunion; }) .filter((r): r is Reunion => r !== null && r !== undefined); // Apply client-side filtering, sorting, and pagination let filtered = this.applyClientFilters(transformedData, params); const total = filtered.length; const start = (params.page - 1) * params.size; const pageData = filtered.slice(start, start + params.size); const upcomingReunions = filtered.filter( (r) => new Date(r.date) >= new Date() ).length; const pastReunions = filtered.filter((r) => new Date(r.date) < new Date()).length; const uniqueHippodromes = new Set(filtered.map((r) => r.hippodrome.id)).size; return normalizePage( { data: pageData, meta: { total, uniqueHippodromes, upcomingReunions, pastReunions }, }, params.page, params.size ); }) ); }), catchError((err) => { console.error('Error fetching reunions:', err); return this.getMockList(params); }) ); } } return this.getMockList(params); } private applyClientFilters(data: Reunion[], params: ListParams): Reunion[] { let filtered = [...data]; // Search filter const q = (params.search ?? '').toLowerCase(); if (q) { filtered = filtered.filter( (r) => r.nom.toLowerCase().includes(q) || r.hippodrome.nom.toLowerCase().includes(q) || r.hippodrome.ville.toLowerCase().includes(q) ); } // Sort if (params.sortKey && params.sortDir) { const { sortKey, sortDir } = params; filtered.sort((a: any, b: any) => { const va = a[sortKey!], vb = b[sortKey!]; const sa = va == null ? '' : String(va); const sb = vb == null ? '' : String(vb); const cmp = sa.localeCompare(sb, 'fr', { numeric: true, sensitivity: 'base' }); return sortDir === 'asc' ? cmp : -cmp; }); } return filtered; } private getMockList(params: ListParams): Observable> { const q = (params.search ?? '').toLowerCase(); let data = this.store(); if (q) { data = data.filter( (r) => r.nom.toLowerCase().includes(q) || r.hippodrome.nom.toLowerCase().includes(q) || r.hippodrome.ville.toLowerCase().includes(q) ); } if (params.sortKey && params.sortDir) { const { sortKey, sortDir } = params; data = [...data].sort((a: any, b: any) => { const va = a[sortKey!], vb = b[sortKey!]; const sa = va == null ? '' : String(va); const sb = vb == null ? '' : String(vb); const cmp = sa.localeCompare(sb, 'fr', { numeric: true, sensitivity: 'base' }); return sortDir === 'asc' ? cmp : -cmp; }); } const start = (params.page - 1) * params.size; const pageData = data.slice(start, start + params.size); const upcomingReunions = data.filter((r) => new Date(r.date) >= new Date()).length; const pastReunions = data.filter((r) => new Date(r.date) < new Date()).length; const uniqueHippodromes = new Set(data.map((r) => r.hippodrome.nom)).size; return of( normalizePage( { data: pageData, meta: { total: data.length, uniqueHippodromes, upcomingReunions, pastReunions }, }, params.page, params.size ) ); } getById(id: string): Observable { if (USE_SERVER) { return this.http .get(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }) .pipe( switchMap((apiReunion) => { // Fetch the hippodrome data return this.hippodromeService.getById(String(apiReunion.hippodromeId)).pipe( map((hippodrome) => { if (!hippodrome) { return undefined; } return { id: String(apiReunion.id), code: apiReunion.code, nom: apiReunion.nom, date: apiReunion.date, numero: apiReunion.numero, statut: apiReunion.statut as any, hippodrome, totalCourses: apiReunion.totalCourses, createdAt: apiReunion.createdAt, updatedAt: apiReunion.updatedAt, } as Reunion; }) ); }), catchError((err) => { console.error(`Error fetching reunion ${id}:`, err); return of(this.store().find((r) => r.id === id)); }) ); } const found = this.store().find((r) => r.id === id); return of(found); } getByCode(code: string): Observable { if (USE_SERVER) { return this.http .get(`${this.apiUrl}/code/${encodeURIComponent(code)}`, { headers: this.getNgrokHeaders(), }) .pipe( switchMap((apiReunion) => { // Fetch the hippodrome data return this.hippodromeService.getById(String(apiReunion.hippodromeId)).pipe( map((hippodrome) => { if (!hippodrome) { return undefined; } return { id: String(apiReunion.id), code: apiReunion.code, nom: apiReunion.nom, date: apiReunion.date, numero: apiReunion.numero, statut: apiReunion.statut as any, hippodrome, totalCourses: apiReunion.totalCourses, createdAt: apiReunion.createdAt, updatedAt: apiReunion.updatedAt, } as Reunion; }) ); }), catchError((err) => { console.error(`Error fetching reunion by code ${code}:`, err); return of(this.store().find((r) => r.code === code)); }) ); } return of(this.store().find((r) => r.code === code)); } create(payload: Omit): Observable { if (USE_SERVER) { return this.http .post(this.apiUrl, payload, { headers: this.getNgrokHeaders() }) .pipe( catchError((err) => { console.error('Error creating reunion:', err); const item: Reunion = { id: crypto.randomUUID(), ...payload }; this.store.set([item, ...this.store()]); return of(item); }) ); } const item: Reunion = { id: crypto.randomUUID(), ...payload }; this.store.set([item, ...this.store()]); return of(item); } update(id: string, payload: Partial): Observable { if (USE_SERVER) { return this.http .put(`${this.apiUrl}/${id}`, payload, { headers: this.getNgrokHeaders() }) .pipe( catchError((err) => { console.error(`Error updating reunion ${id}:`, err); let updated: Reunion | undefined; this.store.set( this.store().map((r) => { if (r.id === id) { updated = { ...r, ...payload }; return updated; } return r; }) ); return of(updated); }) ); } let updated: Reunion | undefined; this.store.set( this.store().map((r) => { if (r.id === id) { updated = { ...r, ...payload }; return updated; } return r; }) ); return of(updated); } 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 reunion ${id}:`, err); const before = this.store().length; this.store.set(this.store().filter((r) => r.id !== id)); return of(this.store().length < before); }) ); } const before = this.store().length; this.store.set(this.store().filter((r) => r.id !== id)); return of(this.store().length < before); } }