first commit

This commit is contained in:
OnlyPapy98
2025-12-16 14:20:02 +01:00
commit dde2e8aebf
320 changed files with 30462 additions and 0 deletions

View File

@@ -0,0 +1,874 @@
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';
const USE_SERVER = true;
const API_BASE = '/api/v1/courses';
// Interface to match the API response structure for Course
interface CourseApiResponse {
id: number;
type: string;
numero: number;
nom: string;
dateDepartCourse: string;
dateDebutParis: string;
dateFinParis: string;
reunionId: number; // API returns reunionId
reunionCourse: number;
particularite?: string;
partants: number;
distance: number;
condition?: string;
estTerminee: boolean;
estAnnulee: boolean;
statut: CourseStatut;
nombreChevauxInscrits: number;
createdBy: string;
validatedBy: string | null;
createdAt: string | null;
updatedAt: string | null;
nonPartants: string[];
adeadHeat: boolean;
}
@Injectable({ providedIn: 'root' })
export class CourseService {
private apiUrl = environment.apiBaseUrl + API_BASE;
constructor(
private http: HttpClient,
private paginatedHttp: PaginatedHttpService,
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,
usePaginationEndpoint: boolean = false
): Observable<PagedResult<Course>> {
if (USE_SERVER) {
// If there's a search query, use the search endpoint
if (params.search && params.search.trim()) {
return this.search(params.search.trim()).pipe(
map((courses) => {
// Apply client-side sorting and pagination
let filtered = [...courses];
// 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;
});
}
const total = filtered.length;
const start = (params.page - 1) * params.perPage;
const pageData = filtered.slice(start, start + params.perPage);
const totalByType = filtered.reduce<Record<string, number>>((acc, c) => {
const type = String(c.type);
acc[type] = (acc[type] ?? 0) + 1;
return acc;
}, {});
const totalRunning = filtered.filter(
(c) => c.statut === CourseStatut.RUNNING || c.statut === 'RUNNING'
).length;
const totalClosed = filtered.filter(
(c) => c.statut === CourseStatut.CLOSED || c.statut === 'CLOSED'
).length;
return normalizePage<Course>(
{
data: pageData,
meta: {
total,
totalRunning,
totalClosed,
totalByType,
},
},
params.page,
params.perPage
);
}),
catchError((err) => {
console.error('Error searching courses:', err);
return of(
normalizePage<Course>(
{
data: [],
meta: {
total: 0,
totalRunning: 0,
totalClosed: 0,
totalByType: {},
},
},
params.page,
params.perPage
)
);
})
);
}
if (usePaginationEndpoint) {
return this.paginatedHttp
.fetch<CourseApiResponse>(this.apiUrl, params, {
zeroBasedPageIndex: false,
})
.pipe(
switchMap((pagedResult) => {
// Handle empty data case
if (!pagedResult.data || pagedResult.data.length === 0) {
return of(
normalizePage<Course>(
{
data: [],
meta: {
total: pagedResult.meta?.total ?? 0,
totalRunning: 0,
totalClosed: 0,
totalByType: {},
},
},
params.page,
params.perPage
)
);
}
// Extract unique reunionIds
const uniqueReunionIds = [
...new Set(pagedResult.data.map((c) => String(c.reunionId))),
];
// If no reunion IDs, we can't build valid Reunion objects return empty page
if (uniqueReunionIds.length === 0) {
return of(
normalizePage<Course>(
{
data: [],
meta: {
total: pagedResult.meta?.total ?? 0,
totalRunning: 0,
totalClosed: 0,
totalByType: {},
},
},
params.page,
params.perPage
)
);
}
// 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
const transformedData: Course[] = pagedResult.data
.map((apiCourse) => {
const reunion = reunionMap.get(String(apiCourse.reunionId));
if (!reunion) {
// If we couldn't resolve the reunion, drop this course to keep typing sound
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);
// Calculate meta stats
const totalByType = transformedData.reduce<Record<string, number>>((acc, c) => {
const type = String(c.type);
acc[type] = (acc[type] ?? 0) + 1;
return acc;
}, {});
const totalRunning = transformedData.filter(
(c) => c.statut === CourseStatut.RUNNING || c.statut === 'RUNNING'
).length;
const totalClosed = transformedData.filter(
(c) => c.statut === CourseStatut.CLOSED || c.statut === 'CLOSED'
).length;
return normalizePage<Course>(
{
data: transformedData,
meta: {
total: pagedResult.meta?.total ?? transformedData.length,
totalRunning,
totalClosed,
totalByType,
},
},
params.page,
params.perPage
);
})
);
}),
catchError((err) => {
console.error('Error fetching courses:', err);
return of(
normalizePage<Course>(
{
data: [],
meta: {
total: 0,
totalRunning: 0,
totalClosed: 0,
totalByType: {},
},
},
params.page,
params.perPage
)
);
})
);
} else {
// Fetch all data and apply client-side pagination
return this.http
.get<CourseApiResponse[]>(this.apiUrl, { headers: this.getNgrokHeaders() })
.pipe(
switchMap((apiData) => {
// Handle empty data case
if (!apiData || apiData.length === 0) {
return of(
normalizePage<Course>(
{
data: [],
meta: {
total: 0,
totalRunning: 0,
totalClosed: 0,
totalByType: {},
},
},
params.page,
params.perPage
)
);
}
// Extract unique reunionIds
const uniqueReunionIds = [...new Set(apiData.map((c) => String(c.reunionId)))];
// Handle case where there are no unique IDs (shouldn't happen, but be safe)
if (uniqueReunionIds.length === 0) {
return of(
normalizePage<Course>(
{
data: [],
meta: {
total: 0,
totalRunning: 0,
totalClosed: 0,
totalByType: {},
},
},
params.page,
params.perPage
)
);
}
// 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
const transformedData: Course[] = 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);
// Apply client-side filtering, sorting, and pagination
let filtered = this.applyClientFilters(transformedData, params);
const total = filtered.length;
const start = (params.page - 1) * params.perPage;
const pageData = filtered.slice(start, start + params.perPage);
const totalByType = filtered.reduce<Record<string, number>>((acc, c) => {
const type = String(c.type);
acc[type] = (acc[type] ?? 0) + 1;
return acc;
}, {});
const totalRunning = filtered.filter(
(c) => c.statut === CourseStatut.RUNNING || c.statut === 'RUNNING'
).length;
const totalClosed = filtered.filter(
(c) => c.statut === CourseStatut.CLOSED || c.statut === 'CLOSED'
).length;
return normalizePage<Course>(
{
data: pageData,
meta: {
total,
totalRunning,
totalClosed,
totalByType,
},
},
params.page,
params.perPage
);
})
);
}),
catchError((err) => {
console.error('Error fetching courses:', err);
return of(
normalizePage<Course>(
{
data: [],
meta: {
total: 0,
totalRunning: 0,
totalClosed: 0,
totalByType: {},
},
},
params.page,
params.perPage
)
);
})
);
}
}
// If USE_SERVER is false, return empty result
return of(
normalizePage<Course>(
{
data: [],
meta: {
total: 0,
totalRunning: 0,
totalClosed: 0,
totalByType: {},
},
},
params.page,
params.perPage
)
);
}
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.reunionService.getById(String(apiCourse.reunionId)).pipe(
map((reunion) => {
if (!reunion) {
return undefined;
}
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(),
};
})
);
}),
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: Omit<Course, 'id' | 'nonPartants'>): Observable<Course> {
if (USE_SERVER) {
// Transform payload to API format (send reunionId instead of reunion object)
const apiPayload: any = {
type: payload.type,
numero: payload.numero,
nom: payload.nom,
dateDepartCourse: payload.dateDepartCourse,
dateDebutParis: payload.dateDebutParis,
dateFinParis: payload.dateFinParis,
reunionId: typeof payload.reunion === 'object' ? payload.reunion.id : payload.reunion,
reunionCourse: payload.reunionCourse,
particularite: payload.particularite,
partants: payload.partants,
distance: payload.distance,
condition: payload.condition,
statut: payload.statut,
createdBy: payload.createdBy,
validatedBy: payload.validatedBy,
};
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.reunionService.getById(String(apiCourse.reunionId)).pipe(
map((reunion) => {
if (!reunion) {
throw new Error('Reunion not found');
}
const item: Course = {
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(),
};
return item;
})
);
}),
catchError((err) => {
console.error('Error creating course:', err);
throw err;
})
);
}
throw new Error('Server mode is required');
}
update(id: string, payload: Partial<Course>): Observable<Course | undefined> {
if (USE_SERVER) {
// Transform payload to API format (send reunionId instead of reunion object)
const apiPayload: any = { ...payload };
if (payload.reunion) {
apiPayload.reunionId =
typeof payload.reunion === 'object' ? payload.reunion.id : payload.reunion;
delete apiPayload.reunion;
}
return this.http
.put<CourseApiResponse>(`${this.apiUrl}/${id}`, apiPayload, {
headers: this.getNgrokHeaders(),
})
.pipe(
switchMap((apiCourse) => {
// Fetch the reunion to build the full Course object
return this.reunionService.getById(String(apiCourse.reunionId)).pipe(
map((reunion) => {
if (!reunion) {
throw new Error('Reunion not found');
}
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(),
};
})
);
}),
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[]) {
console.warn('addNonPartant is deprecated. Use setNonPartants instead.');
return this.setNonPartants(courseId, npList);
}
setNonPartants(courseId: string, npList: string[]): Observable<Course | undefined> {
if (USE_SERVER) {
// Use PUT endpoint to replace the entire list
return this.nonPartantService.replaceNonPartants(courseId, npList).pipe(
switchMap((updatedNonPartants) => {
// Fetch the updated course to return it
return this.getById(courseId).pipe(
map((course) => {
if (course) {
return {
...course,
nonPartants: updatedNonPartants,
};
}
return undefined;
})
);
}),
catchError((err) => {
console.error(`Error setting nonPartants for course ${courseId}:`, err);
return of(undefined);
})
);
}
throw new Error('Server mode is required');
}
}