first commit
This commit is contained in:
537
src/app/core/services/reunion.ts
Normal file
537
src/app/core/services/reunion.ts
Normal file
@@ -0,0 +1,537 @@
|
||||
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/v1/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 = false
|
||||
): 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.data || pagedResult.data.length === 0) {
|
||||
return of({
|
||||
...pagedResult,
|
||||
data: [],
|
||||
meta: {
|
||||
...pagedResult.meta,
|
||||
uniqueHippodromes: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Extract unique hippodrome IDs from the paginated data
|
||||
const uniqueHippodromeIds = [
|
||||
...new Set(pagedResult.data.map((r) => String(r.hippodromeId))),
|
||||
];
|
||||
|
||||
// Handle case where there are no unique IDs
|
||||
if (uniqueHippodromeIds.length === 0) {
|
||||
return of({
|
||||
...pagedResult,
|
||||
data: [],
|
||||
meta: {
|
||||
...pagedResult.meta,
|
||||
uniqueHippodromes: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch all unique hippodromes 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[] = pagedResult.data
|
||||
.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);
|
||||
|
||||
// Calculate unique hippodromes count
|
||||
const uniqueHippodromes = new Set(transformedData.map((r) => r.hippodrome.id))
|
||||
.size;
|
||||
|
||||
return {
|
||||
...pagedResult,
|
||||
data: transformedData,
|
||||
meta: {
|
||||
...pagedResult.meta,
|
||||
uniqueHippodromes,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
}),
|
||||
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.perPage
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 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.perPage
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 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.perPage;
|
||||
const pageData = filtered.slice(start, start + params.perPage);
|
||||
|
||||
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.perPage
|
||||
);
|
||||
})
|
||||
);
|
||||
}),
|
||||
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.perPage;
|
||||
const pageData = data.slice(start, start + params.perPage);
|
||||
|
||||
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.perPage
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user