course creation

This commit is contained in:
OnlyPapy98
2025-12-19 18:00:50 +01:00
parent dde2e8aebf
commit 169b5ca412
39 changed files with 1250 additions and 2258 deletions

View File

@@ -1,18 +1,28 @@
import { Hippodrome } from './hippodrome';
import { Reunion } from './reunion';
export enum CourseType {
TIERCE = 'TIERCE',
QUARTE = 'QUARTE + TIERCE',
QUINTE = 'QUINTE + TIERCE',
GAGNANT = 'GAGNANT',
PLACE = 'PLACE',
JUMELE_GAGNANT = 'JUMELE_GAGNANT',
JUMELE_PLACE = "JUMELE_PLACE",
JUMELE_ORDRE = "JUMELE_ORDRE",
TRIO = "TRIO",
TRIO_ORDRE = "TRIO_ORDRE",
TRIPLET = "TRIPLET",
QUATRO = "QUATRO",
QUINTE = "QUINTE"
}
export enum CourseStatut {
PROGRAMMEE = 'PROGRAMMEE',
CREATED = 'CREATED',
VALIDATED = 'VALIDATED',
RUNNING = 'RUNNING',
CLOSED = 'CLOSED',
CANCELED = 'CANCELED',
BROUILLON = 'BROUILLON',
VALIDE = 'VALIDE',
OUVERT = 'OUVERT',
FERME = 'FERME',
RESULTAT_PROVISOIRE = 'RESULTAT_PROVISOIRE',
RESULTAT_OFFICIEL = 'RESULTAT_OFFICIEL',
REGLEE = 'REGLEE',
ANNULEE = 'ANNULEE'
}
export enum ResultatStatut {
@@ -25,34 +35,21 @@ export enum ResultatStatut {
export interface Course {
id: string;
type: CourseType | string; // API returns "Plat" as string
numero: number;
hippodrome: Hippodrome | undefined;
reunionNumero: number;
reunionDate: string;
nom: string;
dateDepartCourse: string;
dateDebutParis: string;
dateFinParis: string;
reunion: Reunion;
reunionCourse: number;
particularite?: string;
partants: number;
distance: number;
condition?: string;
statut: CourseStatut | string; // API returns "PROGRAMMEE" as string
nonPartants: string[];
// Additional API fields
estTerminee?: boolean;
estAnnulee?: boolean;
nombreChevauxInscrits?: number;
adeadHeat?: boolean;
createdBy: string;
validatedBy?: string | null;
createdAt: string | null;
updatedAt: string | null;
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>
}

View File

@@ -6,8 +6,6 @@ export interface Hippodrome {
actif: boolean;
capacite?: number;
description?: string;
reunionCount?: number;
courseCount?: number;
createdAt: string;
updatedAt: string;
}

View File

@@ -1,197 +1,197 @@
import { Course, CourseType, CourseStatut, ResultatStatut } from '../interfaces/course';
import { REUNIONS_MOCK } from './reunion.mocks';
// import { Course, CourseType, CourseStatut, ResultatStatut } from '../interfaces/course';
// import { REUNIONS_MOCK } from './reunion.mocks';
const now = new Date();
const COURSES_PER_REUNION_BASE = 6;
// const now = new Date();
// const COURSES_PER_REUNION_BASE = 6;
function requiredLength(t: CourseType): number {
switch (t) {
case CourseType.TIERCE:
return 3;
case CourseType.QUARTE:
return 4;
case CourseType.QUINTE:
return 5;
default:
return 0;
}
}
// function requiredLength(t: CourseType): number {
// switch (t) {
// case CourseType.TIERCE:
// return 3;
// case CourseType.QUARTE:
// return 4;
// case CourseType.QUINTE:
// return 5;
// default:
// return 0;
// }
// }
function rngPick<T>(arr: T[], seed: number): T {
const x = Math.abs(Math.sin(seed) * 10000);
const idx = Math.floor((x - Math.floor(x)) * arr.length) % arr.length;
return arr[idx];
}
// function rngPick<T>(arr: T[], seed: number): T {
// const x = Math.abs(Math.sin(seed) * 10000);
// const idx = Math.floor((x - Math.floor(x)) * arr.length) % arr.length;
// return arr[idx];
// }
function makeMockResultat(
type: CourseType,
partants: number,
nonPartantsNums: number[],
seed: number
): number[][] {
const req = requiredLength(type);
const np = new Set(nonPartantsNums);
const all = Array.from({ length: partants }, (_, i) => i + 1).filter((n) => !np.has(n));
const used = new Set<number>();
const places: number[][] = [];
// function makeMockResultat(
// type: CourseType,
// partants: number,
// nonPartantsNums: number[],
// seed: number
// ): number[][] {
// const req = requiredLength(type);
// const np = new Set(nonPartantsNums);
// const all = Array.from({ length: partants }, (_, i) => i + 1).filter((n) => !np.has(n));
// const used = new Set<number>();
// const places: number[][] = [];
const tiePlace = Math.abs(seed) % 10 === 0 ? ((seed % req) + req) % req : -1;
// const tiePlace = Math.abs(seed) % 10 === 0 ? ((seed % req) + req) % req : -1;
for (let i = 0; i < req; i++) {
const remaining = all.filter((n) => !used.has(n));
if (remaining.length === 0) {
places.push([]);
continue;
}
const first = rngPick(remaining, seed + i * 7);
used.add(first);
const slot = [first];
// for (let i = 0; i < req; i++) {
// const remaining = all.filter((n) => !used.has(n));
// if (remaining.length === 0) {
// places.push([]);
// continue;
// }
// const first = rngPick(remaining, seed + i * 7);
// used.add(first);
// const slot = [first];
if (i === tiePlace) {
const remaining2 = all.filter((n) => !used.has(n));
if (remaining2.length > 0) {
const second = rngPick(remaining2, seed + i * 13);
used.add(second);
slot.push(second);
slot.sort((a, b) => a - b);
}
}
// if (i === tiePlace) {
// const remaining2 = all.filter((n) => !used.has(n));
// if (remaining2.length > 0) {
// const second = rngPick(remaining2, seed + i * 13);
// used.add(second);
// slot.push(second);
// slot.sort((a, b) => a - b);
// }
// }
places.push(slot);
}
// places.push(slot);
// }
return places;
}
// return places;
// }
const COURSE_NAMES = [
'Prix du Delta',
'Coupe du Fleuve Niger',
'Trophée du Mandé',
'Challenge du Nord',
'Prix de Bamako',
'Grand Prix de Tombouctou',
'Prix du Sahara',
'Trophée du Mali',
'Prix de la Savane',
'Course de la Paix',
'Grand Prix du Sud',
'Coupe de lAvenir',
'Prix du Coton',
'Prix de la Liberté',
'Prix du Marché Central',
'Prix du Rail',
'Challenge du Faso',
'Prix du Soleil',
'Prix du Soudan',
'Grand Prix du Président',
'Prix de la Jeunesse',
'Coupe de la Nation',
'Prix des Cavaliers',
'Trophée de lUnité',
'Prix du Bénin',
'Grand Prix de Sikasso',
'Prix du Commerce',
'Prix du Plateau',
'Course des Champions',
'Trophée de lEspoir',
'Prix du Développement',
'Prix de lAmitié',
'Grand Prix International',
'Prix du Peuple',
'Prix de la Baie',
'Trophée des Pionniers',
'Prix du Littoral',
];
// const COURSE_NAMES = [
// 'Prix du Delta',
// 'Coupe du Fleuve Niger',
// 'Trophée du Mandé',
// 'Challenge du Nord',
// 'Prix de Bamako',
// 'Grand Prix de Tombouctou',
// 'Prix du Sahara',
// 'Trophée du Mali',
// 'Prix de la Savane',
// 'Course de la Paix',
// 'Grand Prix du Sud',
// 'Coupe de lAvenir',
// 'Prix du Coton',
// 'Prix de la Liberté',
// 'Prix du Marché Central',
// 'Prix du Rail',
// 'Challenge du Faso',
// 'Prix du Soleil',
// 'Prix du Soudan',
// 'Grand Prix du Président',
// 'Prix de la Jeunesse',
// 'Coupe de la Nation',
// 'Prix des Cavaliers',
// 'Trophée de lUnité',
// 'Prix du Bénin',
// 'Grand Prix de Sikasso',
// 'Prix du Commerce',
// 'Prix du Plateau',
// 'Course des Champions',
// 'Trophée de lEspoir',
// 'Prix du Développement',
// 'Prix de lAmitié',
// 'Grand Prix International',
// 'Prix du Peuple',
// 'Prix de la Baie',
// 'Trophée des Pionniers',
// 'Prix du Littoral',
// ];
const COURSE_TYPES = [CourseType.TIERCE, CourseType.QUARTE, CourseType.QUINTE];
const COURSE_STATUTS = [
CourseStatut.CREATED,
CourseStatut.VALIDATED,
CourseStatut.RUNNING,
CourseStatut.CLOSED,
CourseStatut.CANCELED,
];
// const COURSE_TYPES = [CourseType.TIERCE, CourseType.QUARTE, CourseType.QUINTE];
// const COURSE_STATUTS = [
// CourseStatut.CREATED,
// CourseStatut.VALIDATED,
// CourseStatut.RUNNING,
// CourseStatut.CLOSED,
// CourseStatut.CANCELED,
// ];
const coursesPerReunion = new Map<string, number>();
// const coursesPerReunion = new Map<string, number>();
const courses: Course[] = [];
// const courses: Course[] = [];
REUNIONS_MOCK.forEach((reunion, reunionIndex) => {
const courseCount = COURSES_PER_REUNION_BASE + (reunionIndex % 2);
const reunionDate = new Date(`${reunion.date}T00:00:00`);
// REUNIONS_MOCK.forEach((reunion, reunionIndex) => {
// const courseCount = COURSES_PER_REUNION_BASE + (reunionIndex % 2);
// const reunionDate = new Date(`${reunion.date}T00:00:00`);
for (let i = 0; i < courseCount; i++) {
const globalIndex = courses.length;
const type = COURSE_TYPES[(globalIndex + i) % COURSE_TYPES.length];
const statut = COURSE_STATUTS[(globalIndex + reunionIndex) % COURSE_STATUTS.length];
// for (let i = 0; i < courseCount; i++) {
// const globalIndex = courses.length;
// const type = COURSE_TYPES[(globalIndex + i) % COURSE_TYPES.length];
// const statut = COURSE_STATUTS[(globalIndex + reunionIndex) % COURSE_STATUTS.length];
const numberWithinReunion = (coursesPerReunion.get(reunion.id) ?? 0) + 1;
coursesPerReunion.set(reunion.id, numberWithinReunion);
// const numberWithinReunion = (coursesPerReunion.get(reunion.id) ?? 0) + 1;
// coursesPerReunion.set(reunion.id, numberWithinReunion);
const dateDebutParis = new Date(reunionDate);
dateDebutParis.setHours(8 + i, 0, 0, 0);
const dateFinParis = new Date(dateDebutParis);
dateFinParis.setHours(dateDebutParis.getHours() + 2);
const dateDepartCourse = new Date(reunionDate);
dateDepartCourse.setHours(12 + i, 30, 0, 0);
// const dateDebutParis = new Date(reunionDate);
// dateDebutParis.setHours(8 + i, 0, 0, 0);
// const dateFinParis = new Date(dateDebutParis);
// dateFinParis.setHours(dateDebutParis.getHours() + 2);
// const dateDepartCourse = new Date(reunionDate);
// dateDepartCourse.setHours(12 + i, 30, 0, 0);
const partants = 10 + ((reunionIndex + i) % 6) * 2;
// const partants = 10 + ((reunionIndex + i) % 6) * 2;
const nonPartants: string[] = numberWithinReunion % 4 === 0 ? [crypto.randomUUID()] : [];
// const nonPartants: string[] = numberWithinReunion % 4 === 0 ? [crypto.randomUUID()] : [];
const nonPartantsNums = nonPartants.map((np) => Number(np));
// const nonPartantsNums = nonPartants.map((np) => Number(np));
let resultat: number[][] | undefined;
let resultatStatut: ResultatStatut = ResultatStatut.NONE;
// let resultat: number[][] | undefined;
// let resultatStatut: ResultatStatut = ResultatStatut.NONE;
if (statut === CourseStatut.CLOSED) {
resultat = makeMockResultat(type, partants, nonPartantsNums, globalIndex * 31);
resultatStatut = ResultatStatut.CONFIRMED;
} else if (statut === CourseStatut.VALIDATED) {
resultat = makeMockResultat(type, partants, nonPartantsNums, globalIndex * 17);
resultatStatut = ResultatStatut.VALIDATED;
} else if (statut === CourseStatut.RUNNING && (globalIndex + reunionIndex) % 3 === 0) {
resultat = makeMockResultat(type, partants, nonPartantsNums, globalIndex * 7);
resultatStatut = ResultatStatut.CREATED;
}
// if (statut === CourseStatut.CLOSED) {
// resultat = makeMockResultat(type, partants, nonPartantsNums, globalIndex * 31);
// resultatStatut = ResultatStatut.CONFIRMED;
// } else if (statut === CourseStatut.VALIDATED) {
// resultat = makeMockResultat(type, partants, nonPartantsNums, globalIndex * 17);
// resultatStatut = ResultatStatut.VALIDATED;
// } else if (statut === CourseStatut.RUNNING && (globalIndex + reunionIndex) % 3 === 0) {
// resultat = makeMockResultat(type, partants, nonPartantsNums, globalIndex * 7);
// resultatStatut = ResultatStatut.CREATED;
// }
courses.push({
id: crypto.randomUUID(),
type,
numero: globalIndex + 1,
nom: `${COURSE_NAMES[(globalIndex + reunionIndex) % COURSE_NAMES.length]} - ${
reunion.hippodrome.ville
}`,
dateDebutParis: dateDebutParis.toISOString(),
dateFinParis: dateFinParis.toISOString(),
dateDepartCourse: dateDepartCourse.toISOString(),
reunion,
reunionCourse: numberWithinReunion,
particularite:
(globalIndex + reunionIndex) % 2 === 0
? 'Course de galop - conditions variées'
: 'Trot attelé - catégorie nationale',
partants,
distance: 2000 + ((reunionIndex + i) % 5) * 200,
condition:
(globalIndex + reunionIndex) % 3 === 0
? 'Réservée aux chevaux de 3 ans et plus'
: 'Course mixte - catégorie B',
statut,
nonPartants,
createdBy: `user-${((globalIndex + reunionIndex) % 5) + 1}`,
validatedBy: statut === CourseStatut.VALIDATED ? 'admin-1' : undefined,
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
});
}
});
// courses.push({
// id: crypto.randomUUID(),
// type,
// numero: globalIndex + 1,
// nom: `${COURSE_NAMES[(globalIndex + reunionIndex) % COURSE_NAMES.length]} - ${
// reunion.hippodrome.ville
// }`,
// dateDebutParis: dateDebutParis.toISOString(),
// dateFinParis: dateFinParis.toISOString(),
// dateDepartCourse: dateDepartCourse.toISOString(),
// reunion,
// reunionCourse: numberWithinReunion,
// particularite:
// (globalIndex + reunionIndex) % 2 === 0
// ? 'Course de galop - conditions variées'
// : 'Trot attelé - catégorie nationale',
// partants,
// distance: 2000 + ((reunionIndex + i) % 5) * 200,
// condition:
// (globalIndex + reunionIndex) % 3 === 0
// ? 'Réservée aux chevaux de 3 ans et plus'
// : 'Course mixte - catégorie B',
// statut,
// nonPartants,
// createdBy: `user-${((globalIndex + reunionIndex) % 5) + 1}`,
// validatedBy: statut === CourseStatut.VALIDATED ? 'admin-1' : undefined,
// createdAt: now.toISOString(),
// updatedAt: now.toISOString(),
// });
// }
// });
coursesPerReunion.forEach((count, reunionId) => {
const reunion = REUNIONS_MOCK.find((r) => r.id === reunionId);
if (reunion) {
reunion.totalCourses = count;
}
});
// coursesPerReunion.forEach((count, reunionId) => {
// const reunion = REUNIONS_MOCK.find((r) => r.id === reunionId);
// if (reunion) {
// reunion.totalCourses = count;
// }
// });
export const COURSES_MOCK: Course[] = courses;
// export const COURSES_MOCK: Course[] = courses;

View File

@@ -1,110 +1,110 @@
import { Course } from '../interfaces/course';
import {
CourseReportDetail,
CourseReportDetailRow,
CourseReportSummary,
} from '../interfaces/report';
import { COURSES_MOCK } from './course.mocks';
// import { Course } from '../interfaces/course';
// import {
// CourseReportDetail,
// CourseReportDetailRow,
// CourseReportSummary,
// } from '../interfaces/report';
// import { COURSES_MOCK } from './course.mocks';
function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// function randomInt(min: number, max: number) {
// return Math.floor(Math.random() * (max - min + 1)) + min;
// }
export function payoutRowsForCourse(c: Course): CourseReportDetailRow[] {
const base: CourseReportDetailRow[] = [
{
typeGain: 'QUINTE ORDRE',
typeJeu: 'Quinte+',
montant: 2840500,
nombre: randomInt(1, 30),
statut: 'Validée',
distributed: false,
externe: false,
},
{
typeGain: 'QUINTE DESORDRE',
typeJeu: 'Quinte+',
montant: 40000,
nombre: randomInt(300, 5000),
statut: 'Validée',
distributed: false,
externe: false,
},
{
typeGain: 'BONUS 4',
typeJeu: 'Quinte+',
montant: 2000,
nombre: randomInt(5000, 25000),
statut: 'Validée',
distributed: false,
externe: false,
},
{
typeGain: 'REMBOURSEMENT',
typeJeu: 'Quinte+',
montant: 300,
nombre: randomInt(10, 500),
statut: 'Validée',
distributed: false,
externe: false,
},
{
typeGain: 'TIERCE ORDRE',
typeJeu: 'Tierce',
montant: 37000,
nombre: randomInt(100, 2000),
statut: 'Validée',
distributed: false,
externe: false,
},
{
typeGain: 'TIERCE DESORDRE',
typeJeu: 'Tierce',
montant: 6000,
nombre: randomInt(500, 6000),
statut: 'Validée',
distributed: false,
externe: false,
},
{
typeGain: 'TRANSFORME COUPLE',
typeJeu: 'Tierce',
montant: 3000,
nombre: randomInt(200, 2000),
statut: 'Validée',
distributed: false,
externe: false,
},
{
typeGain: 'TRANSFORME SIMPLE',
typeJeu: 'Tierce',
montant: 1500,
nombre: randomInt(10, 500),
statut: 'Validée',
distributed: false,
externe: false,
},
];
return base;
}
// export function payoutRowsForCourse(c: Course): CourseReportDetailRow[] {
// const base: CourseReportDetailRow[] = [
// {
// typeGain: 'QUINTE ORDRE',
// typeJeu: 'Quinte+',
// montant: 2840500,
// nombre: randomInt(1, 30),
// statut: 'Validée',
// distributed: false,
// externe: false,
// },
// {
// typeGain: 'QUINTE DESORDRE',
// typeJeu: 'Quinte+',
// montant: 40000,
// nombre: randomInt(300, 5000),
// statut: 'Validée',
// distributed: false,
// externe: false,
// },
// {
// typeGain: 'BONUS 4',
// typeJeu: 'Quinte+',
// montant: 2000,
// nombre: randomInt(5000, 25000),
// statut: 'Validée',
// distributed: false,
// externe: false,
// },
// {
// typeGain: 'REMBOURSEMENT',
// typeJeu: 'Quinte+',
// montant: 300,
// nombre: randomInt(10, 500),
// statut: 'Validée',
// distributed: false,
// externe: false,
// },
// {
// typeGain: 'TIERCE ORDRE',
// typeJeu: 'Tierce',
// montant: 37000,
// nombre: randomInt(100, 2000),
// statut: 'Validée',
// distributed: false,
// externe: false,
// },
// {
// typeGain: 'TIERCE DESORDRE',
// typeJeu: 'Tierce',
// montant: 6000,
// nombre: randomInt(500, 6000),
// statut: 'Validée',
// distributed: false,
// externe: false,
// },
// {
// typeGain: 'TRANSFORME COUPLE',
// typeJeu: 'Tierce',
// montant: 3000,
// nombre: randomInt(200, 2000),
// statut: 'Validée',
// distributed: false,
// externe: false,
// },
// {
// typeGain: 'TRANSFORME SIMPLE',
// typeJeu: 'Tierce',
// montant: 1500,
// nombre: randomInt(10, 500),
// statut: 'Validée',
// distributed: false,
// externe: false,
// },
// ];
// return base;
// }
export const REPORT_SUMMARIES_MOCK: CourseReportSummary[] = COURSES_MOCK.filter(
(c) => c.statut === 'CLOSED'
)
.slice(0, 300)
.map(
(c) => ({ id: c.id, course: c, statut: 'En attente', confirmed: false } as CourseReportSummary)
);
// export const REPORT_SUMMARIES_MOCK: CourseReportSummary[] = COURSES_MOCK.filter(
// (c) => c.statut === 'CLOSED'
// )
// .slice(0, 300)
// .map(
// (c) => ({ id: c.id, course: c, statut: 'En attente', confirmed: false } as CourseReportSummary)
// );
export function buildDetailByCourseId(id: string): CourseReportDetail | undefined {
const summary = REPORT_SUMMARIES_MOCK.find((s) => s.id === id);
if (!summary) return undefined;
const rows = payoutRowsForCourse(summary.course as Course);
return { summary, rows } as CourseReportDetail;
}
// export function buildDetailByCourseId(id: string): CourseReportDetail | undefined {
// const summary = REPORT_SUMMARIES_MOCK.find((s) => s.id === id);
// if (!summary) return undefined;
// const rows = payoutRowsForCourse(summary.course as Course);
// return { summary, rows } as CourseReportDetail;
// }
// Pre-built rows map for in-memory updates
export const REPORT_DETAILS_MOCK = new Map<string, CourseReportDetailRow[]>();
for (const c of COURSES_MOCK.filter((c) => c.statut === 'CLOSED').slice(0, 300)) {
REPORT_DETAILS_MOCK.set(c.id, payoutRowsForCourse(c));
}
// // Pre-built rows map for in-memory updates
// export const REPORT_DETAILS_MOCK = new Map<string, CourseReportDetailRow[]>();
// for (const c of COURSES_MOCK.filter((c) => c.statut === 'CLOSED').slice(0, 300)) {
// REPORT_DETAILS_MOCK.set(c.id, payoutRowsForCourse(c));
// }

View File

@@ -213,7 +213,7 @@ export class AgentLimitService {
sortDir: 'asc',
} as any).pipe(
switchMap((result) => {
const limits = result.data;
const limits = result.content;
const previousDefault = limits.find((l) => l.isDefault && l.id !== newDefaultLimitId);
const operations: Observable<any>[] = [];

View File

@@ -451,7 +451,7 @@ export class AgentService {
sortDir: 'asc',
} as any).pipe(
switchMap((result) => {
const agents = result.data;
const agents = result.content;
if (agents.length === 0) {
return of(true);
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,16 +7,18 @@ 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 { ServicesUtils } from './services-utils';
const USE_SERVER = true;
const API_BASE = '/api/v1/hippodromes';
const API_BASE = '/api/hippodromes';
@Injectable({ providedIn: 'root' })
export class HippodromeService {
private apiUrl = environment.apiBaseUrl + API_BASE;
private store = signal<Hippodrome[]>([]);
constructor(private http: HttpClient, private paginatedHttp: PaginatedHttpService) {}
constructor(private http: HttpClient, private paginatedHttp: PaginatedHttpService, private servicesUtils:ServicesUtils) {}
// Helper method to get ngrok bypass headers
private getNgrokHeaders(): Record<string, string> {
@@ -30,326 +32,13 @@ export class HippodromeService {
// LISTE — supporte client & serveur
list(
params: ListParams,
usePaginationEndpoint: boolean = false
usePaginationEndpoint: boolean = true
): Observable<PagedResult<Hippodrome>> {
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(
switchMap((hippodromes) => {
// Fetch all reunions and courses to calculate counts
return forkJoin({
reunions: this.http
.get<any[]>(`${environment.apiBaseUrl}/api/v1/reunions`, {
headers: this.getNgrokHeaders(),
})
.pipe(
catchError(() => of([])),
map((data) => ({ data: data || [], meta: { total: (data || []).length } }))
),
courses: this.http
.get<any[]>(`${environment.apiBaseUrl}/api/v1/courses`, {
headers: this.getNgrokHeaders(),
})
.pipe(
catchError(() => of([])),
map((data) => ({ data: data || [], meta: { total: (data || []).length } }))
),
}).pipe(
map(({ reunions, courses }) => {
// Count reunions per hippodrome
const reunionCountMap = new Map<string, number>();
reunions.data.forEach((reunion: any) => {
const hippodromeId = String(reunion.hippodromeId || reunion.hippodrome?.id);
if (hippodromeId && hippodromeId !== 'undefined' && hippodromeId !== 'null') {
reunionCountMap.set(hippodromeId, (reunionCountMap.get(hippodromeId) || 0) + 1);
}
});
// Create a map of reunionId -> hippodromeId from reunions
const reunionToHippodromeMap = new Map<string, string>();
reunions.data.forEach((reunion: any) => {
const reunionId = String(reunion.id);
const hippodromeId = String(reunion.hippodromeId || reunion.hippodrome?.id);
if (
reunionId &&
reunionId !== 'undefined' &&
reunionId !== 'null' &&
hippodromeId &&
hippodromeId !== 'undefined' &&
hippodromeId !== 'null'
) {
reunionToHippodromeMap.set(reunionId, hippodromeId);
}
});
// Count courses per hippodrome using the reunion -> hippodrome mapping
const courseCountMap = new Map<string, number>();
courses.data.forEach((course: any) => {
const reunionId = String(course.reunionId || course.reunion?.id);
if (reunionId && reunionId !== 'undefined' && reunionId !== 'null') {
const hippodromeId = reunionToHippodromeMap.get(reunionId);
if (hippodromeId) {
courseCountMap.set(hippodromeId, (courseCountMap.get(hippodromeId) || 0) + 1);
}
}
});
// Add counts to hippodromes
const hippodromesWithCounts = hippodromes.map((h) => ({
...h,
reunionCount: reunionCountMap.get(String(h.id)) ?? 0,
courseCount: courseCountMap.get(String(h.id)) ?? 0,
}));
// Apply client-side sorting and pagination
let filtered = this.applyClientFilters(hippodromesWithCounts, {
...params,
search: '', // Already filtered by search endpoint
});
const total = filtered.length;
const start = (params.page - 1) * params.perPage;
const pageData = filtered.slice(start, start + params.perPage);
const uniqueCountries = new Set(filtered.map((h) => h.pays)).size;
const uniqueCities = new Set(filtered.map((h) => h.ville)).size;
const averageByCountry = filtered.length
? Math.round(filtered.length / uniqueCountries)
: 0;
const totalReunions = filtered.reduce((acc, h) => acc + (h.reunionCount ?? 0), 0);
const totalCourses = filtered.reduce((acc, h) => acc + (h.courseCount ?? 0), 0);
return normalizePage<Hippodrome>(
{
data: pageData,
meta: {
total,
uniqueCountries,
uniqueCities,
averageByCountry,
totalReunions,
totalCourses,
},
},
params.page,
params.perPage
);
})
);
}),
catchError((err) => {
console.error('Error searching hippodromes:', err);
return of(
normalizePage<Hippodrome>(
{
data: [],
meta: {
total: 0,
uniqueCountries: 0,
uniqueCities: 0,
averageByCountry: 0,
totalReunions: 0,
totalCourses: 0,
},
},
params.page,
params.perPage
)
);
})
);
}
if (usePaginationEndpoint) {
return this.paginatedHttp
.fetch<Hippodrome>(this.apiUrl, params, {
zeroBasedPageIndex: false,
buildSort: (key, dir) => (key && dir ? ['sort', `${key},${dir}`] : null),
mapClientSortKey: (k) => {
const alias: Record<string, string> = {
name: 'nom',
city: 'ville',
country: 'pays',
};
return k ? alias[k] ?? k : undefined;
},
})
.pipe(
catchError((err) => {
console.error('Error fetching hippodromes:', err);
return of(
normalizePage<Hippodrome>(
{
data: [],
meta: {
total: 0,
uniqueCountries: 0,
uniqueCities: 0,
averageByCountry: 0,
totalReunions: 0,
totalCourses: 0,
},
},
params.page,
params.perPage
)
);
})
);
} else {
// Fetch all data and apply client-side pagination
return this.http
.get<Hippodrome[]>(`${this.apiUrl}/actifs`, {
headers: this.getNgrokHeaders(),
})
.pipe(
switchMap((allData) => {
// Fetch all reunions and courses directly from API to calculate counts
// We fetch directly to avoid circular dependency with ReunionService and CourseService
return forkJoin({
reunions: this.http
.get<any[]>(`${environment.apiBaseUrl}/api/v1/reunions`, {
headers: this.getNgrokHeaders(),
})
.pipe(
catchError(() => of([])),
map((data) => ({ data, meta: { total: data.length } }))
),
courses: this.http
.get<any[]>(`${environment.apiBaseUrl}/api/v1/courses`, {
headers: this.getNgrokHeaders(),
})
.pipe(
catchError(() => of([])),
map((data) => ({ data, meta: { total: data.length } }))
),
}).pipe(
map(({ reunions, courses }) => {
// Count reunions per hippodrome
const reunionCountMap = new Map<string, number>();
reunions.data.forEach((reunion: any) => {
const hippodromeId = String(reunion.hippodromeId || reunion.hippodrome?.id);
if (hippodromeId && hippodromeId !== 'undefined' && hippodromeId !== 'null') {
reunionCountMap.set(
hippodromeId,
(reunionCountMap.get(hippodromeId) || 0) + 1
);
}
});
// Create a map of reunionId -> hippodromeId from reunions
const reunionToHippodromeMap = new Map<string, string>();
reunions.data.forEach((reunion: any) => {
const reunionId = String(reunion.id);
const hippodromeId = String(reunion.hippodromeId || reunion.hippodrome?.id);
if (
reunionId &&
reunionId !== 'undefined' &&
reunionId !== 'null' &&
hippodromeId &&
hippodromeId !== 'undefined' &&
hippodromeId !== 'null'
) {
reunionToHippodromeMap.set(reunionId, hippodromeId);
}
});
// Count courses per hippodrome using the reunion -> hippodrome mapping
const courseCountMap = new Map<string, number>();
courses.data.forEach((course: any) => {
const reunionId = String(course.reunionId || course.reunion?.id);
if (reunionId && reunionId !== 'undefined' && reunionId !== 'null') {
const hippodromeId = reunionToHippodromeMap.get(reunionId);
if (hippodromeId) {
courseCountMap.set(
hippodromeId,
(courseCountMap.get(hippodromeId) || 0) + 1
);
}
}
});
// Add counts to hippodromes
const hippodromesWithCounts = allData.map((h) => ({
...h,
reunionCount: reunionCountMap.get(String(h.id)) ?? 0,
courseCount: courseCountMap.get(String(h.id)) ?? 0,
}));
// Apply client-side filtering, sorting, and pagination
let filtered = this.applyClientFilters(hippodromesWithCounts, params);
const total = filtered.length;
const start = (params.page - 1) * params.perPage;
const pageData = filtered.slice(start, start + params.perPage);
const uniqueCountries = new Set(filtered.map((h) => h.pays)).size;
const uniqueCities = new Set(filtered.map((h) => h.ville)).size;
const averageByCountry = filtered.length
? Math.round(filtered.length / uniqueCountries)
: 0;
const totalReunions = filtered.reduce((acc, h) => acc + (h.reunionCount ?? 0), 0);
const totalCourses = filtered.reduce((acc, h) => acc + (h.courseCount ?? 0), 0);
return normalizePage<Hippodrome>(
{
data: pageData,
meta: {
total,
uniqueCountries,
uniqueCities,
averageByCountry,
totalReunions,
totalCourses,
},
},
params.page,
params.perPage
);
})
);
}),
catchError((err) => {
console.error('Error fetching hippodromes:', err);
return of(
normalizePage<Hippodrome>(
{
data: [],
meta: {
total: 0,
uniqueCountries: 0,
uniqueCities: 0,
averageByCountry: 0,
totalReunions: 0,
totalCourses: 0,
},
},
params.page,
params.perPage
)
);
})
);
}
}
// Mock mode disabled - return empty result
return of(
normalizePage<Hippodrome>(
{
data: [],
meta: {
total: 0,
uniqueCountries: 0,
uniqueCities: 0,
averageByCountry: 0,
totalReunions: 0,
totalCourses: 0,
},
},
params.page,
params.perPage
)
);
const hippodromeList = this.http.get<PagedResult<Hippodrome>>(this.apiUrl, {
headers: this.getNgrokHeaders(),
params: this.servicesUtils.getParamsFromModel(params)
})
return hippodromeList;
}
private applyClientFilters(data: Hippodrome[], params: ListParams): Hippodrome[] {

View File

@@ -1,76 +1,76 @@
import { Injectable, signal } from '@angular/core';
import { Observable, of } from 'rxjs';
import { CourseReportDetail, CourseReportDetailRow, CourseReportSummary } from '../interfaces/report';
import { REPORT_SUMMARIES_MOCK, REPORT_DETAILS_MOCK } from '../mocks/report.mocks';
import { normalizePage } from '@shared/paging/normalize-page';
import { ListParams, PagedResult, SortDir } from '@shared/paging/paging';
// import { Injectable, signal } from '@angular/core';
// import { Observable, of } from 'rxjs';
// import { CourseReportDetail, CourseReportDetailRow, CourseReportSummary } from '../interfaces/report';
// import { REPORT_SUMMARIES_MOCK, REPORT_DETAILS_MOCK } from '../mocks/report.mocks';
// import { normalizePage } from '@shared/paging/normalize-page';
// import { ListParams, PagedResult, SortDir } from '@shared/paging/paging';
@Injectable({ providedIn: 'root' })
export class ReportService {
private summaries = signal<CourseReportSummary[]>([...REPORT_SUMMARIES_MOCK]);
// @Injectable({ providedIn: 'root' })
// export class ReportService {
// private summaries = signal<CourseReportSummary[]>([...REPORT_SUMMARIES_MOCK]);
list(params: ListParams): Observable<PagedResult<CourseReportSummary>> {
let data = [...this.summaries()];
const q = (params.search ?? '').toLowerCase();
if (q) {
data = data.filter((r) =>
[
r.course.nom,
r.course.type,
r.course.reunion?.hippodrome?.nom,
String(r.course.numero),
]
.filter(Boolean)
.map((s) => String(s).toLowerCase())
.some((s) => s.includes(q))
);
}
if (params.sortKey && params.sortDir) {
const { sortKey, sortDir } = params as { sortKey: string; sortDir: SortDir };
const get = (o: any, k: string) => k.split('.').reduce((a, b) => a?.[b], o);
data = [...data].sort((a, b) => String(get(a, sortKey) ?? '').localeCompare(String(get(b, sortKey) ?? ''), 'fr', { numeric: true }) * (sortDir === 'asc' ? 1 : -1));
}
const start = (params.page - 1) * params.perPage;
const pageData = data.slice(start, start + params.perPage);
return of(normalizePage<CourseReportSummary>({ data: pageData, meta: { total: data.length } }, params.page, params.perPage));
}
// list(params: ListParams): Observable<PagedResult<CourseReportSummary>> {
// let data = [...this.summaries()];
// const q = (params.search ?? '').toLowerCase();
// if (q) {
// data = data.filter((r) =>
// [
// r.course.nom,
// r.course.type,
// r.course.reunion?.hippodrome?.nom,
// String(r.course.numero),
// ]
// .filter(Boolean)
// .map((s) => String(s).toLowerCase())
// .some((s) => s.includes(q))
// );
// }
// if (params.sortKey && params.sortDir) {
// const { sortKey, sortDir } = params as { sortKey: string; sortDir: SortDir };
// const get = (o: any, k: string) => k.split('.').reduce((a, b) => a?.[b], o);
// data = [...data].sort((a, b) => String(get(a, sortKey) ?? '').localeCompare(String(get(b, sortKey) ?? ''), 'fr', { numeric: true }) * (sortDir === 'asc' ? 1 : -1));
// }
// const start = (params.page - 1) * params.perPage;
// const pageData = data.slice(start, start + params.perPage);
// return of(normalizePage<CourseReportSummary>({ data: pageData, meta: { total: data.length } }, params.page, params.perPage));
// }
getDetail(courseId: string): Observable<CourseReportDetail | undefined> {
const summary = this.summaries().find((s) => s.id === courseId);
if (!summary) return of(undefined);
const rows = REPORT_DETAILS_MOCK.get(courseId) ?? [];
return of({ summary, rows });
}
// getDetail(courseId: string): Observable<CourseReportDetail | undefined> {
// const summary = this.summaries().find((s) => s.id === courseId);
// if (!summary) return of(undefined);
// const rows = REPORT_DETAILS_MOCK.get(courseId) ?? [];
// return of({ summary, rows });
// }
// === Actions ===
validate(courseId: string): Observable<CourseReportSummary | undefined> {
let updated: CourseReportSummary | undefined;
this.summaries.set(
this.summaries().map((s) => (s.id === courseId ? ((updated = { ...s, statut: 'Validé', confirmed: false }), updated) : s))
);
return of(updated);
}
// // === Actions ===
// validate(courseId: string): Observable<CourseReportSummary | undefined> {
// let updated: CourseReportSummary | undefined;
// this.summaries.set(
// this.summaries().map((s) => (s.id === courseId ? ((updated = { ...s, statut: 'Validé', confirmed: false }), updated) : s))
// );
// return of(updated);
// }
confirm(courseId: string): Observable<CourseReportSummary | undefined> {
let updated: CourseReportSummary | undefined;
this.summaries.set(
this.summaries().map((s) => (s.id === courseId ? ((updated = { ...s, statut: 'Validé', confirmed: true }), updated) : s))
);
return of(updated);
}
// confirm(courseId: string): Observable<CourseReportSummary | undefined> {
// let updated: CourseReportSummary | undefined;
// this.summaries.set(
// this.summaries().map((s) => (s.id === courseId ? ((updated = { ...s, statut: 'Validé', confirmed: true }), updated) : s))
// );
// return of(updated);
// }
resetStatus(courseId: string): Observable<CourseReportSummary | undefined> {
let updated: CourseReportSummary | undefined;
this.summaries.set(
this.summaries().map((s) => (s.id === courseId ? ((updated = { ...s, statut: 'Non Validé', confirmed: false }), updated) : s))
);
return of(updated);
}
// resetStatus(courseId: string): Observable<CourseReportSummary | undefined> {
// let updated: CourseReportSummary | undefined;
// this.summaries.set(
// this.summaries().map((s) => (s.id === courseId ? ((updated = { ...s, statut: 'Non Validé', confirmed: false }), updated) : s))
// );
// return of(updated);
// }
modifyRows(courseId: string, rows: CourseReportDetailRow[]): Observable<boolean> {
REPORT_DETAILS_MOCK.set(courseId, rows);
return of(true);
}
}
// modifyRows(courseId: string, rows: CourseReportDetailRow[]): Observable<boolean> {
// REPORT_DETAILS_MOCK.set(courseId, rows);
// return of(true);
// }
// }

View File

@@ -25,7 +25,7 @@ interface ReunionApiResponse {
}
const USE_SERVER = true;
const API_BASE = '/api/v1/reunions';
const API_BASE = '/api/reunions';
@Injectable({ providedIn: 'root' })
export class ReunionService {
@@ -49,7 +49,7 @@ export class ReunionService {
list(
params: ListParams,
usePaginationEndpoint: boolean = false
usePaginationEndpoint: boolean = true
): Observable<PagedResult<Reunion>> {
if (USE_SERVER) {
if (usePaginationEndpoint) {
@@ -61,30 +61,28 @@ export class ReunionService {
.pipe(
switchMap((pagedResult) => {
// Handle empty data case
if (!pagedResult.data || pagedResult.data.length === 0) {
if (!pagedResult.content || pagedResult.content.length === 0) {
return of({
...pagedResult,
data: [],
meta: {
...pagedResult.meta,
uniqueHippodromes: 0,
content: [],
pageable: {
...pagedResult.pageable,
},
});
}
});
}
// Extract unique hippodrome IDs from the paginated data
const uniqueHippodromeIds = [
...new Set(pagedResult.data.map((r) => String(r.hippodromeId))),
...new Set(pagedResult.content.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,
content: [],
pageable: {
...pagedResult.pageable,
},
});
}
@@ -93,12 +91,12 @@ export class ReunionService {
const hippodromeRequests = uniqueHippodromeIds.map((id) =>
this.hippodromeService
.getById(id)
.pipe(catchError(() => of<Hippodrome | undefined>(undefined)))
.pipe(catchError(() => of<Hippodrome>()))
);
// Fetch courses to calculate counts per reunion
const coursesRequest = this.http
.get<any[]>(`${environment.apiBaseUrl}/api/v1/courses`, {
.get<any[]>(`${environment.apiBaseUrl}/api/courses`, {
headers: this.getNgrokHeaders(),
})
.pipe(
@@ -133,7 +131,7 @@ export class ReunionService {
});
// Transform API responses to Reunion objects
const transformedData: Reunion[] = pagedResult.data
const transformedData: Reunion[] = pagedResult.content
.map((apiReunion) => {
const hippodrome = hippodromeMap.get(String(apiReunion.hippodromeId));
if (!hippodrome) {
@@ -155,19 +153,10 @@ export class ReunionService {
} 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,
},
};
return {
...pagedResult,
content: transformedData,
}
})
);
}),

View File

@@ -0,0 +1,16 @@
import { HttpParams } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { ListParams } from "@shared/paging/paging";
@Injectable({providedIn: 'root'})
export class ServicesUtils{
getParamsFromModel(params:ListParams):HttpParams{
let httpParams = new HttpParams();
Object.entries(params).forEach(([key, value])=>{
if(params != null && params!=undefined){
httpParams.set(key, String(value))
}
})
return httpParams;
}
}