478 lines
18 KiB
TypeScript
478 lines
18 KiB
TypeScript
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<unknown>;
|
|
typesParisOuverts: Array<string>;
|
|
}
|
|
|
|
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<string, string> {
|
|
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<PagedResult<Course>> {
|
|
const coursesList = this.http.get<PagedResult<CourseApiResponse>>(this.apiUrl, {
|
|
headers: this.getNgrokHeaders(),
|
|
params: this.servivesUtil.getParamsFromModel(params),
|
|
});
|
|
return coursesList.pipe(
|
|
switchMap((res) => {
|
|
const items = res.content ?? [];
|
|
|
|
// Transforme chaque course en Observable<Course>
|
|
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<Course> = {
|
|
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<Course>);
|
|
})
|
|
);
|
|
}
|
|
|
|
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<Course | undefined> {
|
|
if (USE_SERVER) {
|
|
return this.http
|
|
.get<CourseApiResponse>(`${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<Course[]> {
|
|
// if (USE_SERVER) {
|
|
// return this.http
|
|
// .get<CourseApiResponse[]>(`${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<Course[]> {
|
|
// if (USE_SERVER) {
|
|
// return this.http
|
|
// .get<CourseApiResponse[]>(`${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<Reunion | undefined>(undefined)))
|
|
// );
|
|
|
|
// return forkJoin(reunionRequests).pipe(
|
|
// map((reunions) => {
|
|
// // Create a map of reunionId -> Reunion
|
|
// const reunionMap = new Map<string, Reunion>();
|
|
// 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<CourseApiResponse>): Observable<Course> {
|
|
if (USE_SERVER) {
|
|
// Transform payload to API format (send reunionId instead of reunion object)
|
|
const apiPayload: Partial<CourseApiResponse> = payload;
|
|
|
|
return this.http
|
|
.post<CourseApiResponse>(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<CourseApiResponse>): Observable<Course | undefined> {
|
|
if (USE_SERVER) {
|
|
// Transform payload to API format (send reunionId instead of reunion object
|
|
|
|
return this.http
|
|
.put<CourseApiResponse>(`${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<Course | undefined> {
|
|
if (USE_SERVER) {
|
|
return this.http
|
|
.patch<Course>(
|
|
`${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<boolean> {
|
|
if (USE_SERVER) {
|
|
return this.http
|
|
.delete<void>(`${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<CourseApiResponse | undefined> {
|
|
return this.nonPartantService.replaceNonPartants(courseId, npList).pipe(
|
|
map((updatedNonPartants) => updatedNonPartants),
|
|
catchError((err) => {
|
|
console.error(`Error setting nonPartants for course ${courseId}:`, err);
|
|
return of(undefined);
|
|
})
|
|
);
|
|
}
|
|
}
|