Files
pmu-plateforme-jeux-admin-plr/src/app/core/services/reunion.ts
2025-12-29 13:56:18 +01:00

527 lines
19 KiB
TypeScript

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<Reunion[]>([]);
constructor(
private http: HttpClient,
private paginatedHttp: PaginatedHttpService,
private hippodromeService: HippodromeService
) {}
// 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 = true
): Observable<PagedResult<Reunion>> {
if (USE_SERVER) {
if (usePaginationEndpoint) {
return this.paginatedHttp
.fetch<ReunionApiResponse>(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<Hippodrome>()))
);
// Fetch courses to calculate counts per reunion
const coursesRequest = this.http
.get<any[]>(`${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<string, Hippodrome>();
uniqueHippodromeIds.forEach((id, index) => {
const hippodrome = hippodromes[index];
if (hippodrome) {
hippodromeMap.set(id, hippodrome);
}
});
// Count courses per reunion
const courseCountMap = new Map<string, number>();
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<ReunionApiResponse[]>(this.apiUrl, { headers: this.getNgrokHeaders() })
.pipe(
switchMap((apiData) => {
// Handle empty data case
if (!apiData || apiData.length === 0) {
return of(
normalizePage<Reunion>(
{
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<Reunion>(
{
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<Hippodrome | undefined>(undefined)))
);
// Fetch courses to calculate counts per reunion
const coursesRequest = this.http
.get<any[]>(`${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<string, Hippodrome>();
uniqueHippodromeIds.forEach((id, index) => {
const hippodrome = hippodromes[index];
if (hippodrome) {
hippodromeMap.set(id, hippodrome);
}
});
// Count courses per reunion
const courseCountMap = new Map<string, number>();
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<Reunion>(
{
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<PagedResult<Reunion>> {
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<Reunion>(
{
data: pageData,
meta: { total: data.length, uniqueHippodromes, upcomingReunions, pastReunions },
},
params.page,
params.size
)
);
}
getById(id: string): Observable<Reunion | undefined> {
if (USE_SERVER) {
return this.http
.get<ReunionApiResponse>(`${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<Reunion | undefined> {
if (USE_SERVER) {
return this.http
.get<ReunionApiResponse>(`${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<Reunion, 'id'>): Observable<Reunion> {
if (USE_SERVER) {
return this.http
.post<Reunion>(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<Reunion>): Observable<Reunion | undefined> {
if (USE_SERVER) {
return this.http
.put<Reunion>(`${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<boolean> {
if (USE_SERVER) {
return this.http
.delete<void>(`${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);
}
}