course creation
This commit is contained in:
@@ -32,7 +32,7 @@ export class Login {
|
||||
this.loading.set(true);
|
||||
try {
|
||||
const { identifiant, password } = this.form.value;
|
||||
await this.auth.login(identifiant!, password!);
|
||||
//await this.auth.login(identifiant!, password!);
|
||||
await this.router.navigateByUrl('/');
|
||||
toast.success('Connexion réussie ! Bienvenue.');
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ export interface Hippodrome {
|
||||
actif: boolean;
|
||||
capacite?: number;
|
||||
description?: string;
|
||||
reunionCount?: number;
|
||||
courseCount?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -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 l’Avenir',
|
||||
'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 l’Unité',
|
||||
'Prix du Bénin',
|
||||
'Grand Prix de Sikasso',
|
||||
'Prix du Commerce',
|
||||
'Prix du Plateau',
|
||||
'Course des Champions',
|
||||
'Trophée de l’Espoir',
|
||||
'Prix du Développement',
|
||||
'Prix de l’Amitié',
|
||||
'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 l’Avenir',
|
||||
// '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 l’Unité',
|
||||
// 'Prix du Bénin',
|
||||
// 'Grand Prix de Sikasso',
|
||||
// 'Prix du Commerce',
|
||||
// 'Prix du Plateau',
|
||||
// 'Course des Champions',
|
||||
// 'Trophée de l’Espoir',
|
||||
// 'Prix du Développement',
|
||||
// 'Prix de l’Amitié',
|
||||
// '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;
|
||||
|
||||
@@ -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));
|
||||
// }
|
||||
|
||||
@@ -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>[] = [];
|
||||
|
||||
@@ -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
@@ -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`, {
|
||||
const hippodromeList = this.http.get<PagedResult<Hippodrome>>(this.apiUrl, {
|
||||
headers: this.getNgrokHeaders(),
|
||||
params: this.servicesUtils.getParamsFromModel(params)
|
||||
})
|
||||
.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
|
||||
)
|
||||
);
|
||||
return hippodromeList;
|
||||
}
|
||||
|
||||
private applyClientFilters(data: Hippodrome[], params: ListParams): Hippodrome[] {
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
content: transformedData,
|
||||
}
|
||||
})
|
||||
);
|
||||
}),
|
||||
|
||||
16
src/app/core/services/services-utils.ts
Normal file
16
src/app/core/services/services-utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Layout,
|
||||
canActivate: [authGuard],
|
||||
children: [
|
||||
{ path: '', loadComponent: () => import('./pages/main/main').then((m) => m.Main) },
|
||||
{
|
||||
@@ -46,16 +45,6 @@ const routes: Routes = [
|
||||
path: 'limits',
|
||||
loadComponent: () => import('./pages/limits/limits').then((m) => m.LimitsPage),
|
||||
},
|
||||
{
|
||||
path: 'rapport-courses',
|
||||
loadComponent: () =>
|
||||
import('./pages/report-courses/report-list').then((m) => m.ReportCoursesListPage),
|
||||
},
|
||||
{
|
||||
path: 'rapport-courses/:id',
|
||||
loadComponent: () =>
|
||||
import('./pages/report-courses/report-detail').then((m) => m.ReportCoursesDetailPage),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -150,7 +150,7 @@ export class AgentsPage {
|
||||
this.tpeSvc
|
||||
.list({ page: 1, perPage: 200, search: '', sortKey: 'imei', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => {
|
||||
const tpes = res.data as TpeDevice[];
|
||||
const tpes = res.content as TpeDevice[];
|
||||
this.rebuildTpeMaps(tpes);
|
||||
});
|
||||
effect(() => {
|
||||
@@ -175,8 +175,8 @@ export class AgentsPage {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
this.loading.set(false);
|
||||
// Refresh TPE map to ensure we have latest data
|
||||
this.refreshTpeMap();
|
||||
@@ -193,7 +193,7 @@ export class AgentsPage {
|
||||
this.tpeSvc
|
||||
.list({ page: 1, perPage: 200, search: '', sortKey: 'imei', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => {
|
||||
const tpes = res.data as TpeDevice[];
|
||||
const tpes = res.content as TpeDevice[];
|
||||
this.rebuildTpeMaps(tpes);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ZardCardComponent } from '@shared/components/card/card.component';
|
||||
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||
import { Course as CourseType } from 'src/app/core/interfaces/course';
|
||||
import { SortDir } from '@shared/paging/paging';
|
||||
import { CourseService } from 'src/app/core/services/course';
|
||||
import { CourseApiResponse, CourseService } from 'src/app/core/services/course';
|
||||
import { ResultatService } from 'src/app/core/services/resultat';
|
||||
import { Resultat } from 'src/app/core/interfaces/resultat';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
@@ -77,23 +77,19 @@ export class Course {
|
||||
key: 'type',
|
||||
label: 'Type',
|
||||
sortable: true,
|
||||
cell: (c) => `<span class="font-medium">${c.type}</span>`,
|
||||
cell: (c) => `<span class="font-medium">${c.discipline}</span>`,
|
||||
},
|
||||
{
|
||||
key: 'dateDepartCourse',
|
||||
label: 'Date et Heure Départ',
|
||||
sortable: true,
|
||||
cell: (c) =>
|
||||
new Date(c.dateDepartCourse).toLocaleDateString('fr-FR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
cell: (c) => c.heureDepartPrevue
|
||||
},
|
||||
{
|
||||
key: 'partants',
|
||||
label: 'Partants',
|
||||
cell: (c) =>
|
||||
`<span>${c.partants}</span> <span class="text-xs text-red-500">(${
|
||||
`<span>${c.nombrePartants}</span> <span class="text-xs text-red-500">(${
|
||||
c.nonPartants?.length ?? 0
|
||||
} NP)</span>`,
|
||||
},
|
||||
@@ -148,20 +144,24 @@ export class Course {
|
||||
sortable: true,
|
||||
cell: (c) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
PROGRAMMEE: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
CREATED: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300',
|
||||
VALIDATED: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
RUNNING: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
CLOSED: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
||||
CANCELED: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
|
||||
OUVERT: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
RESULTAT_PROVISOIRE: 'bg-cyan-100 text-purple-700 dark:bg-cyan-900/30 dark:text-cyan-300',
|
||||
RESULTAT_OFFICIEL: 'bg-amber-100 text-purple-700 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
BROUILLON: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-300',
|
||||
VALIDE: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
REGLEE: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
FERME: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
||||
ANNULEE: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
|
||||
};
|
||||
const labelMap: Record<string, string> = {
|
||||
PROGRAMMEE: 'Programmée',
|
||||
CREATED: 'Créée',
|
||||
VALIDATED: 'Validée',
|
||||
RUNNING: 'En cours',
|
||||
CLOSED: 'Clôturée',
|
||||
CANCELED: 'Annulée',
|
||||
OUVERT: 'Ouvert',
|
||||
RESULTAT_PROVISOIRE: 'Résultat provisoire',
|
||||
RESULTAT_OFFICIEL: 'Résultat officiel',
|
||||
BROUILLON: 'Brouillon',
|
||||
VALIDE: 'Validé',
|
||||
REGLEE: 'Réglée',
|
||||
FERME: 'Fermé',
|
||||
ANNULEE: 'Annulée'
|
||||
};
|
||||
return `<span class="px-2 py-1 rounded-full text-xs font-semibold ${colorMap[c.statut]}">${
|
||||
labelMap[c.statut]
|
||||
@@ -171,31 +171,13 @@ export class Course {
|
||||
{
|
||||
key: 'reunion.hippodrome.nom',
|
||||
label: 'Hippodrome',
|
||||
cell: (c) => (c.reunion?.hippodrome ? `${c.reunion.hippodrome.nom}` : '—'),
|
||||
},
|
||||
{
|
||||
key: 'reunion.nom',
|
||||
label: 'Réunion',
|
||||
cell: (c) => c.reunion?.nom ?? '—',
|
||||
cell: (c) => (c?.hippodrome ? `${c.hippodrome.nom}` : '—'),
|
||||
},
|
||||
{
|
||||
key: 'distance',
|
||||
label: 'Distance (m)',
|
||||
sortable: true,
|
||||
cell: (c) => c.distance.toLocaleString('fr-FR'),
|
||||
},
|
||||
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: 'Créée le',
|
||||
cell: (c) =>
|
||||
c.createdAt
|
||||
? new Date(c.createdAt).toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '—',
|
||||
cell: (c) => c.distanceMetres.toLocaleString('fr-FR'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -224,14 +206,14 @@ export class Course {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
this.totalRunning.set(res.meta['totalRunning'] ?? 0);
|
||||
this.totalClosed.set(res.meta['totalClosed'] ?? 0);
|
||||
this.totalByType.set(res.meta['totalByType'] ?? {});
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.totalElements);
|
||||
this.totalRunning.set(0);
|
||||
this.totalClosed.set(0);
|
||||
this.totalByType.set({});
|
||||
|
||||
// Fetch resultats for all courses in parallel
|
||||
const courseIds = res.data.map((c) => c.id);
|
||||
const courseIds = res.content.map((c) => c.id);
|
||||
if (courseIds.length > 0) {
|
||||
const resultatRequests = courseIds.map((id) =>
|
||||
this.resultatService.getByCourseId(id).pipe(catchError(() => of(undefined)))
|
||||
@@ -305,7 +287,7 @@ export class Course {
|
||||
const current = this.editingItem();
|
||||
const req$ = current?.id
|
||||
? this.api.update(current.id, payload)
|
||||
: this.api.create(payload as Omit<CourseType, 'id'>);
|
||||
: this.api.create(payload as Omit<CourseApiResponse, 'id'>);
|
||||
|
||||
req$.subscribe(() => {
|
||||
this.closeModal();
|
||||
@@ -412,7 +394,7 @@ export class Course {
|
||||
return 3; // Default
|
||||
};
|
||||
|
||||
const requiredHorses = getRequiredHorses(c.type);
|
||||
const requiredHorses = 3;
|
||||
|
||||
// Collect all selected horses (flatten the places array)
|
||||
const allHorses: number[] = places
|
||||
|
||||
@@ -33,18 +33,6 @@
|
||||
{{ averageByCountry() }}
|
||||
</div>
|
||||
</z-card>
|
||||
<z-card class="text-center py-4">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Réunions totales</div>
|
||||
<div class="text-3xl font-bold text-purple-600 dark:text-purple-400 mt-1">
|
||||
{{ totalReunions() }}
|
||||
</div>
|
||||
</z-card>
|
||||
<z-card class="text-center py-4">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Courses totales</div>
|
||||
<div class="text-3xl font-bold text-pink-600 dark:text-pink-400 mt-1">
|
||||
{{ totalCourses() }}
|
||||
</div>
|
||||
</z-card>
|
||||
</div>
|
||||
|
||||
<app-search-bar placeholder="Rechercher (nom, ville, pays…)" (search)="onSearch($event)" />
|
||||
|
||||
@@ -58,18 +58,6 @@ export class Hippodrome {
|
||||
{ key: 'nom', label: 'Nom', sortable: true },
|
||||
{ key: 'ville', label: 'Ville', sortable: true },
|
||||
{ key: 'pays', label: 'Pays', sortable: true },
|
||||
{
|
||||
key: 'reunionCount',
|
||||
label: 'Réunions',
|
||||
sortable: true,
|
||||
cell: (h) => (h.reunionCount ?? 0).toString(),
|
||||
},
|
||||
{
|
||||
key: 'courseCount',
|
||||
label: 'Courses',
|
||||
sortable: true,
|
||||
cell: (h) => (h.courseCount ?? 0).toString(),
|
||||
},
|
||||
{
|
||||
key: 'capacite',
|
||||
label: 'Capacité',
|
||||
@@ -124,26 +112,20 @@ export class Hippodrome {
|
||||
})
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.data);
|
||||
|
||||
const meta = res.meta ?? {};
|
||||
|
||||
this.total.set(meta['total'] ?? 0);
|
||||
this.uniqueCities.set(meta['uniqueCities'] ?? 0);
|
||||
this.uniqueCountries.set(meta['uniqueCountries'] ?? 0);
|
||||
this.averageByCountry.set(meta['averageByCountry'] ?? 0);
|
||||
this.totalReunions.set(meta['totalReunions'] ?? 0);
|
||||
this.totalCourses.set(meta['totalCourses'] ?? 0);
|
||||
const content = res.content;
|
||||
this.rows.set(content);
|
||||
this.total.set(res.pageable.total ?? 0);
|
||||
this.uniqueCities.set(new Set(content.map(i=> i.ville)).size);
|
||||
this.uniqueCountries.set(new Set(content.map(i=>i.pays)).size);
|
||||
this.averageByCountry.set(0);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
error: (err) => {
|
||||
this.rows.set([]);
|
||||
this.total.set(0);
|
||||
this.uniqueCities.set(0);
|
||||
this.uniqueCountries.set(0);
|
||||
this.averageByCountry.set(0);
|
||||
this.totalReunions.set(0);
|
||||
this.totalCourses.set(0);
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -127,7 +127,7 @@ export class LimitsPage implements OnInit {
|
||||
}).pipe(
|
||||
switchMap((res) => {
|
||||
// Convert PagedResult to array for consistency
|
||||
return of(res.data);
|
||||
return of(res.content);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -190,8 +190,8 @@ export class LimitsPage implements OnInit {
|
||||
// Normal list with pagination
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||
{{ c.nom }}
|
||||
</span>
|
||||
@if (c.type) {
|
||||
@if (c.discipline) {
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium bg-indigo-50 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300"
|
||||
>
|
||||
{{ c.type }}
|
||||
{{ c.discipline }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@@ -61,19 +61,15 @@
|
||||
></path>
|
||||
</svg>
|
||||
{{
|
||||
c.dateDepartCourse
|
||||
? (c.dateDepartCourse | date : 'short' : undefined : 'fr-FR')
|
||||
: '—'
|
||||
c.heureDepartPrevue
|
||||
}}
|
||||
</span>
|
||||
<span class="h-1 w-1 rounded-full bg-gray-400"></span>
|
||||
<span class="font-medium">
|
||||
{{ c.reunion.hippodrome.nom }}
|
||||
{{ c.nom }}
|
||||
</span>
|
||||
<span class="h-1 w-1 rounded-full bg-gray-400"></span>
|
||||
<span> Réunion {{ c.reunion.nom }} </span>
|
||||
<span class="h-1 w-1 rounded-full bg-gray-400"></span>
|
||||
<span> Distance {{ c.distance | number : '1.0-0' }} m </span>
|
||||
<span> Distance {{ c.distanceMetres | number : '1.0-0' }} m </span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 text-[11px] text-gray-600 dark:text-gray-300"
|
||||
@@ -93,19 +89,19 @@
|
||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
||||
></path>
|
||||
</svg>
|
||||
{{ c.partants }} partant{{ c.partants > 1 ? 's' : '' }}
|
||||
{{ c.nombrePartants }} partant{{ c.nombrePartants > 1 ? 's' : '' }}
|
||||
</span>
|
||||
@if (c.nonPartants && c.nonPartants.length > 0) {
|
||||
<span class="h-1 w-1 rounded-full bg-gray-400"></span>
|
||||
<span class="text-orange-600 dark:text-orange-400">
|
||||
{{ c.nonPartants.length }} non-partant{{ c.nonPartants.length > 1 ? 's' : '' }}
|
||||
</span>
|
||||
} @if (c.condition) {
|
||||
} @if (c.numero) {
|
||||
<span class="h-1 w-1 rounded-full bg-gray-400"></span>
|
||||
<span class="italic">{{ c.condition }}</span>
|
||||
} @if (c.particularite) {
|
||||
<span class="italic">{{ c.numero }}</span>
|
||||
} @if (c.discipline) {
|
||||
<span class="h-1 w-1 rounded-full bg-gray-400"></span>
|
||||
<span class="text-blue-600 dark:text-blue-400">⭐ {{ c.particularite }}</span>
|
||||
<span class="text-blue-600 dark:text-blue-400">⭐ {{ c.discipline }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -223,26 +223,21 @@ export class Main {
|
||||
|
||||
// Include PROGRAMMEE courses that are scheduled within the next 24 hours
|
||||
if (statut === 'PROGRAMMEE') {
|
||||
const d = c.dateDepartCourse ? new Date(c.dateDepartCourse) : null;
|
||||
const d = c.heureDepartPrevue ? c.heureDepartPrevue : null;
|
||||
if (!d) return false;
|
||||
// Include if departure is in the past hour (just started) or within next 24 hours
|
||||
return d >= oneHourAgo && d <= oneDayAhead;
|
||||
return d ;
|
||||
}
|
||||
|
||||
// Also include VALIDATED courses that are about to start (within next 24 hours)
|
||||
if (statut === 'VALIDATED') {
|
||||
const d = c.dateDepartCourse ? new Date(c.dateDepartCourse) : null;
|
||||
const d = c.heureDepartPrevue ? c.heureDepartPrevue : null;
|
||||
if (!d) return false;
|
||||
return d >= now && d <= oneDayAhead;
|
||||
return d;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const da = a.dateDepartCourse ? new Date(a.dateDepartCourse).getTime() : 0;
|
||||
const db = b.dateDepartCourse ? new Date(b.dateDepartCourse).getTime() : 0;
|
||||
return da - db; // Sort by departure time ascending (earliest first)
|
||||
})
|
||||
.slice(0, 6);
|
||||
this.liveCourses.set(live);
|
||||
|
||||
|
||||
@@ -1,132 +1,132 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { ZardCardComponent } from '@shared/components/card/card.component';
|
||||
import { ReportService } from 'src/app/core/services/report';
|
||||
import { CourseReportDetail, CourseReportDetailRow } from 'src/app/core/interfaces/report';
|
||||
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||
// import { CommonModule } from '@angular/common';
|
||||
// import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||
// import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
// import { ZardCardComponent } from '@shared/components/card/card.component';
|
||||
// import { ReportService } from 'src/app/core/services/report';
|
||||
// import { CourseReportDetail, CourseReportDetailRow } from 'src/app/core/interfaces/report';
|
||||
// import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-report-courses-detail',
|
||||
templateUrl: './report-detail.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, RouterModule, ZardCardComponent, ZardButtonComponent],
|
||||
})
|
||||
export class ReportCoursesDetailPage {
|
||||
detail = signal<CourseReportDetail | undefined>(undefined);
|
||||
editMode = signal(false);
|
||||
editedRows = signal<CourseReportDetailRow[]>([]);
|
||||
private originalRows = signal<CourseReportDetailRow[]>([]);
|
||||
// @Component({
|
||||
// standalone: true,
|
||||
// selector: 'app-report-courses-detail',
|
||||
// templateUrl: './report-detail.html',
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
// imports: [CommonModule, RouterModule, ZardCardComponent, ZardButtonComponent],
|
||||
// })
|
||||
// export class ReportCoursesDetailPage {
|
||||
// detail = signal<CourseReportDetail | undefined>(undefined);
|
||||
// editMode = signal(false);
|
||||
// editedRows = signal<CourseReportDetailRow[]>([]);
|
||||
// private originalRows = signal<CourseReportDetailRow[]>([]);
|
||||
|
||||
constructor(private route: ActivatedRoute, private api: ReportService) {
|
||||
const id = this.route.snapshot.params['id'];
|
||||
this.api.getDetail(id).subscribe((d) => {
|
||||
this.detail.set(d);
|
||||
this.editedRows.set(d?.rows ?? []);
|
||||
this.originalRows.set(d?.rows ? d.rows.map((r) => ({ ...r })) : []);
|
||||
});
|
||||
}
|
||||
// constructor(private route: ActivatedRoute, private api: ReportService) {
|
||||
// const id = this.route.snapshot.params['id'];
|
||||
// this.api.getDetail(id).subscribe((d) => {
|
||||
// this.detail.set(d);
|
||||
// this.editedRows.set(d?.rows ?? []);
|
||||
// this.originalRows.set(d?.rows ? d.rows.map((r) => ({ ...r })) : []);
|
||||
// });
|
||||
// }
|
||||
|
||||
onValidate() {
|
||||
const id = this.detail()?.summary.id;
|
||||
if (!id) return;
|
||||
// Persist edited rows then validate
|
||||
this.api.modifyRows(id, this.editedRows()).subscribe(() => {
|
||||
this.api.validate(id).subscribe((s) => {
|
||||
if (this.detail()) this.detail.set({ summary: s!, rows: this.editedRows() });
|
||||
// Commit current edits as the new baseline
|
||||
this.originalRows.set(this.editedRows().map((r) => ({ ...r })));
|
||||
this.editMode.set(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
onConfirm() {
|
||||
const id = this.detail()?.summary.id;
|
||||
if (!id) return;
|
||||
this.api.confirm(id).subscribe((s) => {
|
||||
if (this.detail()) this.detail.set({ summary: s!, rows: this.editedRows() });
|
||||
// Confirm also commits the current edits as baseline
|
||||
this.originalRows.set(this.editedRows().map((r) => ({ ...r })));
|
||||
this.editMode.set(false);
|
||||
});
|
||||
}
|
||||
onReset() {
|
||||
const id = this.detail()?.summary.id;
|
||||
if (!id) return;
|
||||
this.api.resetStatus(id).subscribe((s) => {
|
||||
if (this.detail()) this.detail.set({ summary: s!, rows: this.detail()!.rows });
|
||||
// Reset discards uncommitted edits
|
||||
this.editedRows.set(this.originalRows().map((r) => ({ ...r })));
|
||||
this.editMode.set(false);
|
||||
});
|
||||
}
|
||||
// onValidate() {
|
||||
// const id = this.detail()?.summary.id;
|
||||
// if (!id) return;
|
||||
// // Persist edited rows then validate
|
||||
// this.api.modifyRows(id, this.editedRows()).subscribe(() => {
|
||||
// this.api.validate(id).subscribe((s) => {
|
||||
// if (this.detail()) this.detail.set({ summary: s!, rows: this.editedRows() });
|
||||
// // Commit current edits as the new baseline
|
||||
// this.originalRows.set(this.editedRows().map((r) => ({ ...r })));
|
||||
// this.editMode.set(false);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// onConfirm() {
|
||||
// const id = this.detail()?.summary.id;
|
||||
// if (!id) return;
|
||||
// this.api.confirm(id).subscribe((s) => {
|
||||
// if (this.detail()) this.detail.set({ summary: s!, rows: this.editedRows() });
|
||||
// // Confirm also commits the current edits as baseline
|
||||
// this.originalRows.set(this.editedRows().map((r) => ({ ...r })));
|
||||
// this.editMode.set(false);
|
||||
// });
|
||||
// }
|
||||
// onReset() {
|
||||
// const id = this.detail()?.summary.id;
|
||||
// if (!id) return;
|
||||
// this.api.resetStatus(id).subscribe((s) => {
|
||||
// if (this.detail()) this.detail.set({ summary: s!, rows: this.detail()!.rows });
|
||||
// // Reset discards uncommitted edits
|
||||
// this.editedRows.set(this.originalRows().map((r) => ({ ...r })));
|
||||
// this.editMode.set(false);
|
||||
// });
|
||||
// }
|
||||
|
||||
onEditToggle() {
|
||||
if (this.detail()?.summary.confirmed) return;
|
||||
const currentlyEditing = this.editMode();
|
||||
if (currentlyEditing) {
|
||||
// Leaving edit mode without validation: revert to original snapshot
|
||||
this.editedRows.set(this.originalRows().map((r) => ({ ...r })));
|
||||
this.editMode.set(false);
|
||||
} else {
|
||||
this.editMode.set(true);
|
||||
}
|
||||
}
|
||||
// onEditToggle() {
|
||||
// if (this.detail()?.summary.confirmed) return;
|
||||
// const currentlyEditing = this.editMode();
|
||||
// if (currentlyEditing) {
|
||||
// // Leaving edit mode without validation: revert to original snapshot
|
||||
// this.editedRows.set(this.originalRows().map((r) => ({ ...r })));
|
||||
// this.editMode.set(false);
|
||||
// } else {
|
||||
// this.editMode.set(true);
|
||||
// }
|
||||
// }
|
||||
|
||||
onChangeMontant(index: number, value: any) {
|
||||
const v = Number(value);
|
||||
this.editedRows.update((rows: CourseReportDetailRow[]) => {
|
||||
const current = rows[index];
|
||||
if (!current) return rows;
|
||||
current.montant = Number.isFinite(v) ? v : current.montant;
|
||||
return rows;
|
||||
});
|
||||
}
|
||||
// onChangeMontant(index: number, value: any) {
|
||||
// const v = Number(value);
|
||||
// this.editedRows.update((rows: CourseReportDetailRow[]) => {
|
||||
// const current = rows[index];
|
||||
// if (!current) return rows;
|
||||
// current.montant = Number.isFinite(v) ? v : current.montant;
|
||||
// return rows;
|
||||
// });
|
||||
// }
|
||||
|
||||
onChangeNombre(index: number, value: any) {
|
||||
const v = Number(value);
|
||||
this.editedRows.update((rows: CourseReportDetailRow[]) => {
|
||||
const current = rows[index];
|
||||
if (!current) return rows;
|
||||
current.nombre = Number.isFinite(v) ? v : current.nombre;
|
||||
return rows;
|
||||
});
|
||||
}
|
||||
// onChangeNombre(index: number, value: any) {
|
||||
// const v = Number(value);
|
||||
// this.editedRows.update((rows: CourseReportDetailRow[]) => {
|
||||
// const current = rows[index];
|
||||
// if (!current) return rows;
|
||||
// current.nombre = Number.isFinite(v) ? v : current.nombre;
|
||||
// return rows;
|
||||
// });
|
||||
// }
|
||||
|
||||
onToggleDistributed(index: number, value: any) {
|
||||
const checked = !!value;
|
||||
this.editedRows.update((rows: CourseReportDetailRow[]) => {
|
||||
const current = rows[index];
|
||||
if (!current) return rows;
|
||||
current.distributed = checked;
|
||||
return rows;
|
||||
});
|
||||
}
|
||||
// onToggleDistributed(index: number, value: any) {
|
||||
// const checked = !!value;
|
||||
// this.editedRows.update((rows: CourseReportDetailRow[]) => {
|
||||
// const current = rows[index];
|
||||
// if (!current) return rows;
|
||||
// current.distributed = checked;
|
||||
// return rows;
|
||||
// });
|
||||
// }
|
||||
|
||||
onToggleExterne(index: number, value: any) {
|
||||
const checked = !!value;
|
||||
this.editedRows.update((rows: CourseReportDetailRow[]) => {
|
||||
const current = rows[index];
|
||||
if (!current) return rows;
|
||||
current.externe = checked;
|
||||
return rows;
|
||||
});
|
||||
}
|
||||
// onToggleExterne(index: number, value: any) {
|
||||
// const checked = !!value;
|
||||
// this.editedRows.update((rows: CourseReportDetailRow[]) => {
|
||||
// const current = rows[index];
|
||||
// if (!current) return rows;
|
||||
// current.externe = checked;
|
||||
// return rows;
|
||||
// });
|
||||
// }
|
||||
|
||||
trackByRow(index: number, row: CourseReportDetailRow) {
|
||||
return row.typeGain + '|' + row.typeJeu + '|' + index;
|
||||
}
|
||||
// trackByRow(index: number, row: CourseReportDetailRow) {
|
||||
// return row.typeGain + '|' + row.typeJeu + '|' + index;
|
||||
// }
|
||||
|
||||
isRowDirty(index: number): boolean {
|
||||
const current = this.editedRows()[index];
|
||||
const original = this.originalRows()[index];
|
||||
if (!current || !original) return false;
|
||||
return (
|
||||
current.montant !== original.montant ||
|
||||
current.nombre !== original.nombre ||
|
||||
!!current.distributed !== !!original.distributed ||
|
||||
!!current.externe !== !!original.externe
|
||||
);
|
||||
}
|
||||
}
|
||||
// isRowDirty(index: number): boolean {
|
||||
// const current = this.editedRows()[index];
|
||||
// const original = this.originalRows()[index];
|
||||
// if (!current || !original) return false;
|
||||
// return (
|
||||
// current.montant !== original.montant ||
|
||||
// current.nombre !== original.nombre ||
|
||||
// !!current.distributed !== !!original.distributed ||
|
||||
// !!current.externe !== !!original.externe
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, signal, effect, untracked } from '@angular/core';
|
||||
import { DataTable, SortState, TableColumn } from '@shared/components/data-table/data-table';
|
||||
import { Paginator } from '@shared/components/paginator/paginator';
|
||||
import { SearchBar } from '@shared/components/search-bar/search-bar';
|
||||
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||
import { SortDir } from '@shared/paging/paging';
|
||||
import { Router } from '@angular/router';
|
||||
import { CourseReportSummary } from 'src/app/core/interfaces/report';
|
||||
import { ReportService } from 'src/app/core/services/report';
|
||||
// import { CommonModule } from '@angular/common';
|
||||
// import { ChangeDetectionStrategy, Component, signal, effect, untracked } from '@angular/core';
|
||||
// import { DataTable, SortState, TableColumn } from '@shared/components/data-table/data-table';
|
||||
// import { Paginator } from '@shared/components/paginator/paginator';
|
||||
// import { SearchBar } from '@shared/components/search-bar/search-bar';
|
||||
// import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||
// import { SortDir } from '@shared/paging/paging';
|
||||
// import { Router } from '@angular/router';
|
||||
// import { CourseReportSummary } from 'src/app/core/interfaces/report';
|
||||
// import { ReportService } from 'src/app/core/services/report';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-report-courses-list',
|
||||
templateUrl: './report-list.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, DataTable, Paginator, SearchBar, ZardButtonComponent],
|
||||
})
|
||||
export class ReportCoursesListPage {
|
||||
rows = signal<CourseReportSummary[]>([]);
|
||||
total = signal(0);
|
||||
page = signal(1);
|
||||
perPage = signal(10);
|
||||
search = signal('');
|
||||
sort = signal<SortState>({ key: 'date', dir: 'desc' });
|
||||
loading = signal(false);
|
||||
// @Component({
|
||||
// standalone: true,
|
||||
// selector: 'app-report-courses-list',
|
||||
// templateUrl: './report-list.html',
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
// imports: [CommonModule, DataTable, Paginator, SearchBar, ZardButtonComponent],
|
||||
// })
|
||||
// export class ReportCoursesListPage {
|
||||
// rows = signal<CourseReportSummary[]>([]);
|
||||
// total = signal(0);
|
||||
// page = signal(1);
|
||||
// perPage = signal(10);
|
||||
// search = signal('');
|
||||
// sort = signal<SortState>({ key: 'date', dir: 'desc' });
|
||||
// loading = signal(false);
|
||||
|
||||
cols: TableColumn<CourseReportSummary>[] = [
|
||||
{ key: 'course.dateDepartCourse', label: 'Date', sortable: true },
|
||||
{ key: 'course.numero', label: 'Numéro', sortable: true },
|
||||
{ key: 'course.nom', label: 'Nom', sortable: true },
|
||||
{ key: 'course.type', label: 'Type', sortable: true },
|
||||
{ key: 'course.reunion.hippodrome.nom', label: 'Lieu', sortable: true },
|
||||
{ key: 'course.particularite', label: 'Particularité' },
|
||||
{ key: 'statut', label: 'Statut', sortable: true },
|
||||
];
|
||||
// cols: TableColumn<CourseReportSummary>[] = [
|
||||
// { key: 'course.dateDepartCourse', label: 'Date', sortable: true },
|
||||
// { key: 'course.numero', label: 'Numéro', sortable: true },
|
||||
// { key: 'course.nom', label: 'Nom', sortable: true },
|
||||
// { key: 'course.type', label: 'Type', sortable: true },
|
||||
// { key: 'course.reunion.hippodrome.nom', label: 'Lieu', sortable: true },
|
||||
// { key: 'course.particularite', label: 'Particularité' },
|
||||
// { key: 'statut', label: 'Statut', sortable: true },
|
||||
// ];
|
||||
|
||||
constructor(private api: ReportService, private router: Router) {
|
||||
effect(() => {
|
||||
const params = { page: this.page(), perPage: this.perPage(), search: this.search(), sortKey: this.sort().key, sortDir: this.sort().dir as SortDir };
|
||||
untracked(() => this.fetch(params));
|
||||
});
|
||||
}
|
||||
// constructor(private api: ReportService, private router: Router) {
|
||||
// effect(() => {
|
||||
// const params = { page: this.page(), perPage: this.perPage(), search: this.search(), sortKey: this.sort().key, sortDir: this.sort().dir as SortDir };
|
||||
// untracked(() => this.fetch(params));
|
||||
// });
|
||||
// }
|
||||
|
||||
fetch(params: { page: number; perPage: number; search: string; sortKey: string; sortDir: SortDir }) {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe((res) => {
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
this.loading.set(false);
|
||||
});
|
||||
}
|
||||
// fetch(params: { page: number; perPage: number; search: string; sortKey: string; sortDir: SortDir }) {
|
||||
// this.loading.set(true);
|
||||
// this.api.list(params).subscribe((res) => {
|
||||
// this.rows.set(res.content);
|
||||
// this.total.set(res.pageable.total);
|
||||
// this.loading.set(false);
|
||||
// });
|
||||
// }
|
||||
|
||||
onSearch(q: string) { this.search.set(q); this.page.set(1); }
|
||||
// onSearch(q: string) { this.search.set(q); this.page.set(1); }
|
||||
|
||||
open(row: CourseReportSummary) {
|
||||
this.router.navigate(['/rapport-courses', row.id]);
|
||||
}
|
||||
}
|
||||
// open(row: CourseReportSummary) {
|
||||
// this.router.navigate(['/rapport-courses', row.id]);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
@@ -153,13 +153,13 @@ export class ReunionList {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
const meta = res.meta;
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
const meta = res.pageable;
|
||||
|
||||
this.upcomingReunions.set(res.meta['upcomingReunions'] ?? 0);
|
||||
this.pastReunions.set(res.meta['pastReunions'] ?? 0);
|
||||
this.uniqueHippodromes.set(res.meta['uniqueHippodromes'] ?? 0);
|
||||
this.upcomingReunions.set(0);
|
||||
this.pastReunions.set(0);
|
||||
this.uniqueHippodromes.set(0);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
|
||||
@@ -84,8 +84,8 @@ export class RolesPage {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
|
||||
@@ -191,8 +191,8 @@ export class TpePage implements OnInit {
|
||||
this.total.set(res.length);
|
||||
} else {
|
||||
// List returns PagedResult
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
}
|
||||
this.loading.set(false);
|
||||
},
|
||||
@@ -281,8 +281,8 @@ export class TpePage implements OnInit {
|
||||
// Normal list with pagination
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
|
||||
@@ -79,7 +79,7 @@ export class UsersPage {
|
||||
constructor(private api: UserService, private roleService: RoleService) {
|
||||
this.roleService
|
||||
.list({ page: 1, perPage: 100, search: '', sortKey: 'name', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => (res.data as Role[]).forEach((r) => this.roleMap.set(r.id, r.name)));
|
||||
.subscribe((res) => (res.content as Role[]).forEach((r) => this.roleMap.set(r.id, r.name)));
|
||||
effect(() => {
|
||||
const params = {
|
||||
page: this.page(),
|
||||
@@ -102,8 +102,8 @@ export class UsersPage {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
|
||||
@@ -56,7 +56,7 @@ export class AgentForm {
|
||||
|
||||
this.limitService
|
||||
.list({ page: 1, perPage: 100, search: '', sortKey: 'nom', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => (this.limits = res.data));
|
||||
.subscribe((res) => (this.limits = res.content));
|
||||
}
|
||||
|
||||
error(control: string): string {
|
||||
|
||||
@@ -90,7 +90,7 @@ export class AgentFullForm {
|
||||
});
|
||||
|
||||
this.limitService.list({ page: 1, perPage: 100, search: '', sortKey: 'nom', sortDir: 'asc' } as any).subscribe((res) => {
|
||||
this.limits = res.data;
|
||||
this.limits = res.content;
|
||||
|
||||
// Find default limit
|
||||
const defaultLimit = this.limits.find((l) => l.isDefault);
|
||||
@@ -256,7 +256,7 @@ export class AgentFullForm {
|
||||
.subscribe((res) => {
|
||||
// Only show VALIDE TPEs that are either not assigned or assigned to this agent
|
||||
const currentAgentId = this.value?.id;
|
||||
this.tpeRows = res.data.filter((t) => {
|
||||
this.tpeRows = res.content.filter((t) => {
|
||||
if (t.statut !== 'VALIDE') return false;
|
||||
// If TPE is assigned but to this agent, show it
|
||||
if (t.assigne && currentAgentId) {
|
||||
|
||||
@@ -19,26 +19,28 @@
|
||||
|
||||
<div class="grid sm:grid-cols-3 gap-5">
|
||||
<z-form-field>
|
||||
<label z-form-label zRequired class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Type de course</label
|
||||
<label z-form-label zRequired for="hippodrome">Hippodrome</label>
|
||||
<z-form-control
|
||||
[errorMessage]="isInvalid('hippodromeId') ? 'Veuillez sélectionner une réunion' : ''"
|
||||
>
|
||||
<z-form-control>
|
||||
<z-select formControlName="type" class="w-full">
|
||||
@for (t of courseTypes; track t.value) {
|
||||
<z-select-item [zValue]="t.value">{{ t.label }}</z-select-item>
|
||||
}
|
||||
<z-select
|
||||
id="hippodromeId"
|
||||
placeholder="Rechercher une réunion..."
|
||||
formControlName="hippodromeId"
|
||||
[zLabel]="selectedHippodromeLabel() || ''">
|
||||
@if (loadingHippodromes()) {
|
||||
<z-select-item [zValue]="''" disabled>Chargement des Hippodromes...</z-select-item>
|
||||
} @else { @for (r of filteredHippodromes(); track r.id) {
|
||||
<z-select-item [zValue]="r.id">
|
||||
{{ r.nom }} - ({{ r.ville }})
|
||||
</z-select-item>
|
||||
} }
|
||||
</z-select>
|
||||
</z-form-control>
|
||||
@if (isInvalid('type')) {
|
||||
<p class="mt-1 text-sm text-red-500 italic">
|
||||
{{ errorMessage('type') }}
|
||||
</p>
|
||||
}
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label zRequired class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Numéro</label
|
||||
>Numero de la reunion</label
|
||||
>
|
||||
<z-form-control>
|
||||
<input
|
||||
@@ -46,16 +48,53 @@
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="Ex: 3"
|
||||
formControlName="numero"
|
||||
formControlName="reunionNumero"
|
||||
class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
@if (isInvalid('numero')) {
|
||||
@if (isInvalid('reunionNumero')) {
|
||||
<p class="mt-1 text-sm text-red-500 italic">
|
||||
{{ errorMessage('numero') }}
|
||||
{{ errorMessage('reunionNumero') }}
|
||||
</p>
|
||||
}
|
||||
</z-form-control>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label zRequired class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Date de la réunion</label
|
||||
>
|
||||
<z-form-control>
|
||||
<input
|
||||
z-input
|
||||
type="date"
|
||||
placeholder="Ex: 10/07/2025"
|
||||
formControlName="reunionDate"
|
||||
class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
@if (isInvalid('reunionDate')) {
|
||||
<p class="mt-1 text-sm text-red-500 italic">
|
||||
{{ errorMessage('reunionDate') }}
|
||||
</p>
|
||||
}
|
||||
</z-form-control>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label zRequired class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Discipline</label
|
||||
>
|
||||
<z-form-control>
|
||||
<input
|
||||
z-input
|
||||
placeholder="Ex: TIERCE, QUINTE"
|
||||
formControlName="discipline"
|
||||
class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-orange-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</z-form-control>
|
||||
@if (isInvalid('discipline')) {
|
||||
<p class="mt-1 text-sm text-red-500 italic">
|
||||
{{ errorMessage('discipline') }}
|
||||
</p>
|
||||
}
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label zRequired class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
@@ -103,82 +142,22 @@
|
||||
z-form-label
|
||||
class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2 block"
|
||||
>
|
||||
Date & heure de départ
|
||||
heure de départ
|
||||
</label>
|
||||
<div class="flex gap-3 items-center">
|
||||
<input
|
||||
z-input
|
||||
type="date"
|
||||
[value]="getDatePart('dateDepartCourse')"
|
||||
(change)="setDatePart('dateDepartCourse', $event.target.value)"
|
||||
class="flex-1 px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-purple-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<input
|
||||
z-input
|
||||
type="time"
|
||||
[value]="getTimePart('dateDepartCourse')"
|
||||
(change)="setTimePart('dateDepartCourse', $event.target.value)"
|
||||
formControlName="heureDepartPrevu"
|
||||
class="w-32 px-3 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-purple-500 dark:bg-gray-800 dark:text-gray-100 text-sm"
|
||||
/>
|
||||
</div>
|
||||
@if(isInvalid('dateDepartCourse')) {
|
||||
@if(isInvalid('reunionDate') || isInvalid('heureDepartPrevue')) {
|
||||
<p class="mt-1 text-sm text-red-500 italic">
|
||||
{{ errorMessage('dateDepartCourse') }}
|
||||
{{ errorMessage('reunionDate') || errorMessage('heureDepartPrevue') }}
|
||||
</p>
|
||||
}
|
||||
</z-form-field>
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-5">
|
||||
<z-form-field>
|
||||
<label
|
||||
z-form-label
|
||||
class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2 block"
|
||||
>
|
||||
Début des paris
|
||||
</label>
|
||||
<div class="flex gap-3 items-center">
|
||||
<input
|
||||
z-input
|
||||
type="date"
|
||||
[value]="getDatePart('dateDebutParis')"
|
||||
(change)="setDatePart('dateDebutParis', $event.target.value)"
|
||||
class="flex-1 px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-purple-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<input
|
||||
z-input
|
||||
type="time"
|
||||
[value]="getTimePart('dateDebutParis')"
|
||||
(change)="setTimePart('dateDebutParis', $event.target.value)"
|
||||
class="w-28 px-3 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-purple-500 dark:bg-gray-800 dark:text-gray-100 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label
|
||||
z-form-label
|
||||
class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2 block"
|
||||
>
|
||||
Fin des paris
|
||||
</label>
|
||||
<div class="flex gap-3 items-center">
|
||||
<input
|
||||
z-input
|
||||
type="date"
|
||||
[value]="getDatePart('dateFinParis')"
|
||||
(change)="setDatePart('dateFinParis', $event.target.value)"
|
||||
class="flex-1 px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-purple-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<input
|
||||
z-input
|
||||
type="time"
|
||||
[value]="getTimePart('dateFinParis')"
|
||||
(change)="setTimePart('dateFinParis', $event.target.value)"
|
||||
class="w-28 px-3 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-purple-500 dark:bg-gray-800 dark:text-gray-100 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -197,29 +176,6 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label zRequired for="reunionId">Réunion</label>
|
||||
<z-form-control
|
||||
[errorMessage]="isInvalid('reunionId') ? 'Veuillez sélectionner une réunion' : ''"
|
||||
>
|
||||
<z-select
|
||||
id="reunionId"
|
||||
placeholder="Rechercher une réunion..."
|
||||
formControlName="reunionId"
|
||||
[zLabel]="selectedReunionLabel() || ''"
|
||||
(zSelectionChange)="onReunionSelectionChange($event)"
|
||||
>
|
||||
@if (loadingReunions()) {
|
||||
<z-select-item [zValue]="''" disabled>Chargement des réunions...</z-select-item>
|
||||
} @else { @for (r of filteredReunions(); track r.id) {
|
||||
<z-select-item [zValue]="r.id">
|
||||
{{ r.nom }} – {{ r.hippodrome.nom }} ({{ r.hippodrome.ville }})
|
||||
</z-select-item>
|
||||
} }
|
||||
</z-select>
|
||||
</z-form-control>
|
||||
</z-form-field>
|
||||
</div>
|
||||
|
||||
<!-- 🏇 SECTION 4 — Détails techniques -->
|
||||
@@ -239,15 +195,18 @@
|
||||
<div class="grid sm:grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<z-form-field>
|
||||
<label z-form-label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Particularité</label
|
||||
>Types de paris ouverts (CSV)</label
|
||||
>
|
||||
<z-form-control>
|
||||
<input
|
||||
z-input
|
||||
placeholder="Ex: Handicap, Trot attelé..."
|
||||
formControlName="particularite"
|
||||
class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-orange-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<z-select
|
||||
|
||||
formControlName="typeParisOuverts"
|
||||
class="w-full">
|
||||
@for (t of courseTypes; track t.value) {
|
||||
<z-select-item [zValue]="t.value">{{ t.label }}</z-select-item>
|
||||
}
|
||||
</z-select>
|
||||
<p class="mt-1 text-sm text-gray-500">Séparez les types par des virgules.</p>
|
||||
</z-form-control>
|
||||
</z-form-field>
|
||||
|
||||
@@ -257,14 +216,14 @@
|
||||
>
|
||||
<z-form-control
|
||||
[errorMessage]="
|
||||
isInvalid('partants') ? errorMessage('partants') || 'Ce champ est obligatoire' : ''
|
||||
isInvalid('nombrePartants') ? errorMessage('nombrePartants') || 'Ce champ est obligatoire' : ''
|
||||
"
|
||||
>
|
||||
<input
|
||||
z-input
|
||||
type="number"
|
||||
min="1"
|
||||
formControlName="partants"
|
||||
formControlName="nombrePartants"
|
||||
class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-orange-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</z-form-control>
|
||||
@@ -279,7 +238,7 @@
|
||||
z-input
|
||||
type="number"
|
||||
min="1"
|
||||
formControlName="distance"
|
||||
formControlName="distanceMetres"
|
||||
class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-orange-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</z-form-control>
|
||||
@@ -287,13 +246,13 @@
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Condition</label
|
||||
>Catégorie</label
|
||||
>
|
||||
<z-form-control>
|
||||
<input
|
||||
z-input
|
||||
placeholder="Âge, catégorie..."
|
||||
formControlName="condition"
|
||||
formControlName="categorie"
|
||||
class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-orange-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</z-form-control>
|
||||
@@ -330,14 +289,14 @@
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
>Réunion Course N°</label
|
||||
>N° de la course</label
|
||||
>
|
||||
<z-form-control>
|
||||
<input
|
||||
z-input
|
||||
type="number"
|
||||
min="1"
|
||||
formControlName="reunionCourse"
|
||||
formControlName="numero"
|
||||
placeholder="Ex: 2"
|
||||
class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-indigo-500 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
|
||||
@@ -24,11 +24,13 @@ import { ZardFormModule } from '@shared/components/form/form.module';
|
||||
import { ZardInputDirective } from '@shared/components/input/input.directive';
|
||||
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
||||
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
||||
import { Course, CourseStatut, CourseType } from 'src/app/core/interfaces/course';
|
||||
import { Reunion } from 'src/app/core/interfaces/reunion';
|
||||
import { ReunionService } from 'src/app/core/services/reunion';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { Hippodrome } from 'src/app/core/interfaces/hippodrome';
|
||||
import { Course, CourseStatut, CourseType } from 'src/app/core/interfaces/course';
|
||||
import { HippodromeService } from 'src/app/core/services/hippodrome';
|
||||
import { CourseApiResponse, CourseService } from 'src/app/core/services/course';
|
||||
|
||||
@Component({
|
||||
selector: 'app-course-form',
|
||||
standalone: true,
|
||||
@@ -38,6 +40,7 @@ import { Subscription } from 'rxjs';
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
ZardFormModule,
|
||||
|
||||
ZardInputDirective,
|
||||
ZardSelectComponent,
|
||||
ZardSelectItemComponent,
|
||||
@@ -45,233 +48,198 @@ import { Subscription } from 'rxjs';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CourseForm implements OnInit, AfterViewInit, OnDestroy {
|
||||
@Output() save = new EventEmitter<Course>();
|
||||
@Output() save = new EventEmitter<Partial<CourseApiResponse>>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
private _value?: Course;
|
||||
@Input() set value(v: Course | undefined) {
|
||||
this._value = v;
|
||||
}
|
||||
get value(): Course | undefined {
|
||||
return this._value;
|
||||
}
|
||||
@Input() value?: Course;
|
||||
|
||||
form: FormGroup;
|
||||
submitted = false;
|
||||
|
||||
reunions = signal<Reunion[]>([]);
|
||||
loadingReunions = signal(false);
|
||||
searchQuery = signal<string>('');
|
||||
selectedReunionLabel = signal<string>('');
|
||||
initializing = signal(false);
|
||||
hippodromes = signal<Hippodrome[]>([]);
|
||||
loadingHippodromes = signal(false);
|
||||
searchQuery = signal('');
|
||||
selectedHippodromeLabel = signal('');
|
||||
|
||||
private subs = new Subscription();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private reunionService: ReunionService,
|
||||
private courseServive: CourseService,
|
||||
private hippodromeService: HippodromeService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
type: ['', Validators.required],
|
||||
numero: [null, [Validators.required, Validators.min(1)]],
|
||||
nom: ['', [Validators.required, Validators.minLength(3)]],
|
||||
dateDepartCourse: ['', Validators.required],
|
||||
dateDebutParis: ['', Validators.required],
|
||||
dateFinParis: ['', Validators.required],
|
||||
reunionId: new FormControl<string | null>(null, Validators.required),
|
||||
reunionCourse: [1],
|
||||
particularite: [''],
|
||||
partants: [null, [Validators.required, Validators.min(1)]],
|
||||
distance: [''],
|
||||
condition: [''],
|
||||
statut: [CourseStatut.PROGRAMMEE, Validators.required],
|
||||
hippodromeId: new FormControl<string | null>(null, Validators.required),
|
||||
reunionNumero: [null, Validators.required],
|
||||
reunionDate: ['', Validators.required],
|
||||
nom: ['', Validators.required],
|
||||
numero: [null, Validators.required],
|
||||
statut: [CourseStatut.BROUILLON, Validators.required],
|
||||
heureDepartPrevu: ['', Validators.required],
|
||||
discipline: ['', Validators.required],
|
||||
distanceMetres: [null, Validators.required],
|
||||
nombrePartants: [null, Validators.required],
|
||||
annulee: [false],
|
||||
categorie: ['', Validators.required],
|
||||
reporteeMemeJour: [false],
|
||||
reporteeAutreJour: [false],
|
||||
incidentTechnique: [false],
|
||||
nonPartants: [[]],
|
||||
typesParisOuverts: [''],
|
||||
createdBy: ['agent-001'],
|
||||
validatedBy: [''],
|
||||
});
|
||||
|
||||
// Watch for reunionId changes to update the label
|
||||
// This handles BOTH create and edit modes when user selects a reunion
|
||||
this.subs.add(
|
||||
this.form.get('reunionId')?.valueChanges.subscribe((reunionId) => {
|
||||
// Skip if we're initializing (to avoid interfering with form initialization)
|
||||
if (this.initializing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle empty/null values
|
||||
if (!reunionId || reunionId === '') {
|
||||
this.selectedReunionLabel.set('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update label when user selects a reunion (both create and edit)
|
||||
if (this.reunions().length > 0) {
|
||||
// Try both string and direct comparison since form control might convert types
|
||||
const matchingReunion = this.reunions().find(
|
||||
(r) => String(r.id) === String(reunionId) || r.id === reunionId
|
||||
);
|
||||
if (matchingReunion) {
|
||||
const reunionLabel = `${matchingReunion.nom} – ${matchingReunion.hippodrome.nom} (${matchingReunion.hippodrome.ville})`;
|
||||
this.selectedReunionLabel.set(reunionLabel);
|
||||
// Force change detection to ensure the label is displayed immediately
|
||||
this.cdr.markForCheck();
|
||||
} else {
|
||||
this.selectedReunionLabel.set('');
|
||||
}
|
||||
} else {
|
||||
this.selectedReunionLabel.set('');
|
||||
}
|
||||
}) || new Subscription()
|
||||
);
|
||||
|
||||
// Effect to handle form initialization when value or reunions change
|
||||
/* =============================
|
||||
FIX 1 — Gestion enable/disable
|
||||
(cause principale du select bloqué)
|
||||
============================== */
|
||||
effect(() => {
|
||||
const v = this.value;
|
||||
const reunionsList = this.reunions();
|
||||
const isLoading = this.loadingReunions();
|
||||
const loading = this.loadingHippodromes();
|
||||
const control = this.form.get('hippodromeId');
|
||||
|
||||
// CRITICAL: Enable/disable reunion control based on loading state
|
||||
// This is the most important part for create mode to work
|
||||
const reunionControl = this.form.get('reunionId');
|
||||
if (isLoading) {
|
||||
reunionControl?.disable({ emitEvent: false });
|
||||
if (loading) {
|
||||
control?.disable({ emitEvent: false });
|
||||
} else {
|
||||
reunionControl?.enable({ emitEvent: false });
|
||||
// Force change detection after enabling to ensure the select is clickable
|
||||
control?.enable({ emitEvent: false });
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
// Handle edit mode (value is defined) - populate form with course data
|
||||
if (v !== null && v !== undefined) {
|
||||
this.initializing.set(true);
|
||||
// Edit mode : appliquer la valeur APRÈS chargement
|
||||
if (this.value && this.hippodromes().length) {
|
||||
const id = String(this.value.hippodrome?.id ?? '');
|
||||
if (id) {
|
||||
this.form.patchValue({ hippodromeId: id }, { emitEvent: false });
|
||||
|
||||
let reunionId: string | null = null;
|
||||
let reunionLabel: string = '';
|
||||
if (v.reunion?.id) {
|
||||
reunionId = String(v.reunion.id);
|
||||
if (reunionsList.length > 0) {
|
||||
const matchingReunion = reunionsList.find((r) => String(r.id) === reunionId);
|
||||
if (matchingReunion) {
|
||||
reunionLabel = `${matchingReunion.nom} – ${matchingReunion.hippodrome.nom} (${matchingReunion.hippodrome.ville})`;
|
||||
this.selectedReunionLabel.set(reunionLabel);
|
||||
} else {
|
||||
reunionId = null;
|
||||
this.selectedReunionLabel.set('');
|
||||
const h = this.hippodromes().find(r => String(r.id) === id);
|
||||
if (h) {
|
||||
this.selectedHippodromeLabel.set(`${h.nom} – (${h.ville})`);
|
||||
}
|
||||
} else {
|
||||
if (v.reunion.nom) {
|
||||
reunionLabel = `${v.reunion.nom} – ${v.reunion.hippodrome?.nom || ''} (${
|
||||
v.reunion.hippodrome?.ville || ''
|
||||
})`;
|
||||
this.selectedReunionLabel.set(reunionLabel);
|
||||
}
|
||||
reunionId = null;
|
||||
}
|
||||
} else {
|
||||
this.selectedReunionLabel.set('');
|
||||
});
|
||||
|
||||
/* =============================
|
||||
FIX 2 — valueChanges propre
|
||||
============================== */
|
||||
this.subs.add(
|
||||
this.form.get('hippodromeId')!.valueChanges.subscribe((id) => {
|
||||
if (!id) {
|
||||
this.selectedHippodromeLabel.set('');
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.patchValue(
|
||||
{
|
||||
type: v.type ?? '',
|
||||
numero: v.numero ?? null,
|
||||
nom: v.nom ?? '',
|
||||
dateDepartCourse: v.dateDepartCourse ?? '',
|
||||
dateDebutParis: v.dateDebutParis ?? v.dateDepartCourse ?? '',
|
||||
dateFinParis: v.dateFinParis ?? v.dateDepartCourse ?? '',
|
||||
reunionId,
|
||||
reunionCourse: (v as any).reunionCourse ?? v.numero ?? 1,
|
||||
particularite: (v as any).particularite ?? '',
|
||||
partants: (v as any).partants ?? null,
|
||||
distance: (v as any).distance ?? null,
|
||||
condition: (v as any).condition ?? '',
|
||||
statut: v.statut ?? CourseStatut.PROGRAMMEE,
|
||||
createdBy: (v as any).createdBy ?? 'agent-001',
|
||||
validatedBy: (v as any).validatedBy ?? '',
|
||||
},
|
||||
{ emitEvent: false }
|
||||
const h = this.hippodromes().find(r => String(r.id) === String(id));
|
||||
if (h) {
|
||||
this.selectedHippodromeLabel.set(`${h.nom} – (${h.ville})`);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.form.markAsPristine();
|
||||
this.form.markAsUntouched();
|
||||
queueMicrotask(() => this.initializing.set(false));
|
||||
}
|
||||
// Create mode (v === null or undefined) - DO NOTHING except ensure control is enabled
|
||||
// The form already has default values, just let the user fill it
|
||||
// The valueChanges subscription will handle label updates when user selects
|
||||
/* =============================
|
||||
FIX 3 — vrai filter
|
||||
============================== */
|
||||
filteredHippodromes = computed(() => {
|
||||
const q = this.searchQuery().toLowerCase();
|
||||
if (!q) return this.hippodromes();
|
||||
|
||||
// When reunions finish loading - re-apply value if we have one (for edit mode)
|
||||
else if (reunionsList.length > 0 && !isLoading) {
|
||||
// Reunions just finished loading - re-apply value if we have one
|
||||
const currentValue = this.value;
|
||||
if (currentValue?.reunion?.id) {
|
||||
const reunionId = String(currentValue.reunion.id);
|
||||
const matchingReunion = reunionsList.find((r) => String(r.id) === reunionId);
|
||||
if (matchingReunion) {
|
||||
// Set the label manually
|
||||
const reunionLabel = `${matchingReunion.nom} – ${matchingReunion.hippodrome.nom} (${matchingReunion.hippodrome.ville})`;
|
||||
this.selectedReunionLabel.set(reunionLabel);
|
||||
// Wait for Angular to render the select items
|
||||
setTimeout(() => {
|
||||
this.form.patchValue({ reunionId }, { emitEvent: false });
|
||||
this.cdr.detectChanges();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.hippodromes().filter(h =>
|
||||
h.nom.toLowerCase().includes(q) ||
|
||||
h.ville.toLowerCase().includes(q) ||
|
||||
(h as any).pays?.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.loadingHippodromes.set(true);
|
||||
|
||||
this.subs.add(
|
||||
this.hippodromeService
|
||||
.list({ page: 1, perPage: 1000 }, true)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
this.hippodromes.set(res.content);
|
||||
this.loadingHippodromes.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.loadingHippodromes.set(false);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private hydrateFromValue(v?: Course) {
|
||||
if (v) {
|
||||
const patch = {
|
||||
type: v.type ?? '',
|
||||
numero: v.numero ?? null,
|
||||
nom: v.nom ?? '',
|
||||
dateDepartCourse: v.dateDepartCourse ?? '',
|
||||
dateDebutParis: v.dateDebutParis ?? v.dateDepartCourse ?? '',
|
||||
dateFinParis: v.dateFinParis ?? v.dateDepartCourse ?? '',
|
||||
reunionId: (v as any).reunionId ?? v.reunion?.id ?? '',
|
||||
reunionCourse: (v as any).reunionCourse ?? v.numero ?? 1,
|
||||
particularite: (v as any).particularite ?? '',
|
||||
partants: (v as any).partants ?? null,
|
||||
distance: (v as any).distance ?? null,
|
||||
condition: (v as any).condition ?? '',
|
||||
statut: v.statut ?? CourseStatut.PROGRAMMEE,
|
||||
createdBy: (v as any).createdBy ?? 'agent-001',
|
||||
validatedBy: (v as any).validatedBy ?? '',
|
||||
};
|
||||
this.form.reset(patch);
|
||||
this.form.markAsPristine();
|
||||
this.form.markAsUntouched();
|
||||
} else {
|
||||
this.form.reset({
|
||||
type: '',
|
||||
numero: null,
|
||||
nom: '',
|
||||
dateDepartCourse: '',
|
||||
dateDebutParis: '',
|
||||
dateFinParis: '',
|
||||
reunionId: '',
|
||||
reunionCourse: 1,
|
||||
particularite: '',
|
||||
partants: null,
|
||||
distance: null,
|
||||
condition: '',
|
||||
statut: CourseStatut.PROGRAMMEE,
|
||||
createdBy: 'agent-001',
|
||||
validatedBy: '',
|
||||
});
|
||||
this.form.markAsPristine();
|
||||
this.form.markAsUntouched();
|
||||
}
|
||||
ngAfterViewInit() {
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
isInvalid(control: string): boolean {
|
||||
const ctrl = this.form.get(control);
|
||||
return !!(ctrl && ctrl.invalid && (ctrl.touched || this.submitted));
|
||||
const c = this.form.get(control);
|
||||
return !!(c && c.invalid && (c.touched || this.submitted));
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.submitted = true;
|
||||
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = this.form.getRawValue() as any;
|
||||
|
||||
// 1️⃣ Résoudre l'hippodrome sélectionné
|
||||
const hippodromeId = raw.hippodromeId;
|
||||
const foundHippodrome = this.hippodromes().find(h => String(h.id) === String(hippodromeId));
|
||||
const hippodromeObj = foundHippodrome ?? (hippodromeId ? { id: +hippodromeId } : undefined);
|
||||
|
||||
|
||||
// 2️⃣ Transformer typesParisOuverts CSV → tableau
|
||||
const typesParis = raw.typesParisOuverts
|
||||
? raw.typesParisOuverts.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
// 3️⃣ Construire payload
|
||||
const payload: Partial<CourseApiResponse> = {
|
||||
...this.value, // inclut les champs existants si édition
|
||||
hippodromeId: raw.hippodromeId,
|
||||
reunionNumero: raw.reunionNumero ? +raw.reunionNumero : undefined,
|
||||
reunionDate: raw.reunionDate,
|
||||
nom: raw.nom,
|
||||
numero: raw.numero ? +raw.numero : undefined,
|
||||
statut: raw.statut,
|
||||
discipline: raw.discipline,
|
||||
heureDepartPrevue: new Date(raw.reunionDate+raw.heureDepartPrevu).toISOString(),
|
||||
distanceMetres: raw.distanceMetres ? +raw.distanceMetres : undefined,
|
||||
nombrePartants: raw.nombrePartants ? +raw.nombrePartants : undefined,
|
||||
typesParisOuverts: typesParis,
|
||||
annulee: raw.annulee ?? false,
|
||||
reporteeMemeJour: raw.reporteeMemeJour ?? false,
|
||||
reporteeAutreJour: raw.reporteeAutreJour ?? false,
|
||||
incidentTechnique: raw.incidentTechnique ?? false,
|
||||
nonPartants: raw.nonPartants ?? [],
|
||||
categorie: raw.categorie,
|
||||
};
|
||||
|
||||
console.log(payload);
|
||||
// 4️⃣ Appeler le service (create ou update)
|
||||
if (this.value?.id) {
|
||||
this.courseServive.update(this.value.id, payload).subscribe({
|
||||
next: () => this.save.emit(payload),
|
||||
error: err => console.error('Erreur update course', err)
|
||||
});
|
||||
} else {
|
||||
this.courseServive.create(payload).subscribe({
|
||||
next: () => this.save.emit(payload),
|
||||
error: err => console.error(err)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
errorMessage(control: string): string | null {
|
||||
const c = this.form.get(control);
|
||||
if (!c || !c.errors) return null;
|
||||
@@ -281,167 +249,32 @@ export class CourseForm implements OnInit, AfterViewInit, OnDestroy {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDatePart(control: string): string {
|
||||
const val = this.form.get(control)?.value;
|
||||
return val ? new Date(val).toISOString().slice(0, 10) : '';
|
||||
}
|
||||
|
||||
getTimePart(control: string): string {
|
||||
const val = this.form.get(control)?.value;
|
||||
if (!val) return '';
|
||||
const d = new Date(val);
|
||||
return d.toISOString().slice(11, 16);
|
||||
}
|
||||
|
||||
setDatePart(control: string, date: string) {
|
||||
const current = new Date(this.form.get(control)?.value || new Date());
|
||||
const [h, m] = [current.getHours(), current.getMinutes()];
|
||||
const newDate = new Date(
|
||||
`${date}T${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:00Z`
|
||||
);
|
||||
this.form.patchValue({ [control]: newDate.toISOString() });
|
||||
}
|
||||
|
||||
setTimePart(control: string, time: string) {
|
||||
const current = new Date(this.form.get(control)?.value || new Date());
|
||||
const [h, m] = time.split(':').map(Number);
|
||||
current.setHours(h, m);
|
||||
this.form.patchValue({ [control]: current.toISOString() });
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.submitted = true;
|
||||
if (!this.form.valid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = this.form.getRawValue() as any;
|
||||
|
||||
// Find the reunion object from the reunions list
|
||||
const reunionId = raw.reunionId;
|
||||
const reunion = this.reunions().find((r) => String(r.id) === String(reunionId));
|
||||
|
||||
if (!reunion) {
|
||||
console.error('Reunion not found:', reunionId);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Partial<Course> = {
|
||||
...(this.value ?? {}),
|
||||
type: raw.type,
|
||||
numero: +raw.numero,
|
||||
nom: raw.nom,
|
||||
dateDepartCourse: raw.dateDepartCourse,
|
||||
dateDebutParis: raw.dateDebutParis,
|
||||
dateFinParis: raw.dateFinParis,
|
||||
reunion: reunion!, // Will be set if reunion is found
|
||||
reunionCourse: +raw.reunionCourse,
|
||||
particularite: raw.particularite ?? '',
|
||||
partants: +raw.partants,
|
||||
distance: +raw.distance,
|
||||
condition: raw.condition ?? '',
|
||||
statut: raw.statut,
|
||||
createdBy: raw.createdBy,
|
||||
validatedBy: raw.validatedBy || null,
|
||||
};
|
||||
|
||||
// Ensure reunion is found
|
||||
if (!reunion) {
|
||||
console.error('Reunion not found:', reunionId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.save.emit(payload as Course);
|
||||
}
|
||||
|
||||
// === Filter Reunions ===
|
||||
filteredReunions = computed(() => {
|
||||
const q = this.searchQuery().toLowerCase();
|
||||
return this.reunions().filter(
|
||||
(r) =>
|
||||
r.nom.toLowerCase().includes(q) ||
|
||||
r.hippodrome.nom.toLowerCase().includes(q) ||
|
||||
r.hippodrome.ville.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
courseTypes = [
|
||||
{ label: 'Tiercé', value: CourseType.TIERCE },
|
||||
{ label: 'Quarté + Tiercé', value: CourseType.QUARTE },
|
||||
{ label: 'Quinté + Tiercé', value: CourseType.QUINTE },
|
||||
];
|
||||
|
||||
courseStatus = [
|
||||
{ label: 'Programmée', value: CourseStatut.PROGRAMMEE },
|
||||
{ label: 'Créée', value: CourseStatut.CREATED },
|
||||
{ label: 'Validée', value: CourseStatut.VALIDATED },
|
||||
{ label: 'En cours', value: CourseStatut.RUNNING },
|
||||
{ label: 'Clôturée', value: CourseStatut.CLOSED },
|
||||
{ label: 'Annulée', value: CourseStatut.CANCELED },
|
||||
];
|
||||
|
||||
ngOnInit() {
|
||||
// Fetch reunions from API
|
||||
this.loadingReunions.set(true);
|
||||
this.subs.add(
|
||||
this.reunionService.list({ page: 1, perPage: 1000 }, false).subscribe({
|
||||
next: (result) => {
|
||||
this.reunions.set(result.data);
|
||||
this.loadingReunions.set(false);
|
||||
// Force enable the control after loading
|
||||
setTimeout(() => {
|
||||
const reunionControl = this.form.get('reunionId');
|
||||
reunionControl?.enable({ emitEvent: false });
|
||||
this.cdr.markForCheck();
|
||||
}, 100);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error loading reunions:', err);
|
||||
this.loadingReunions.set(false);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// After view is initialized, ensure the reunion value and label are set correctly
|
||||
const currentValue = this.value;
|
||||
if (currentValue?.reunion?.id && this.reunions().length > 0) {
|
||||
const reunionId = String(currentValue.reunion.id);
|
||||
const matchingReunion = this.reunions().find((r) => String(r.id) === reunionId);
|
||||
if (matchingReunion) {
|
||||
// Set the label manually
|
||||
const reunionLabel = `${matchingReunion.nom} – ${matchingReunion.hippodrome.nom} (${matchingReunion.hippodrome.ville})`;
|
||||
this.selectedReunionLabel.set(reunionLabel);
|
||||
// Wait a bit for the select items to be fully rendered
|
||||
setTimeout(() => {
|
||||
const currentFormValue = this.form.get('reunionId')?.value;
|
||||
if (String(currentFormValue) !== reunionId) {
|
||||
this.form.patchValue({ reunionId }, { emitEvent: false });
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReunionSelectionChange(value: string) {
|
||||
// Immediately update the label when user selects a reunion
|
||||
if (value && this.reunions().length > 0) {
|
||||
const matchingReunion = this.reunions().find(
|
||||
(r) => String(r.id) === String(value) || r.id === value
|
||||
);
|
||||
if (matchingReunion) {
|
||||
const reunionLabel = `${matchingReunion.nom} – ${matchingReunion.hippodrome.nom} (${matchingReunion.hippodrome.ville})`;
|
||||
this.selectedReunionLabel.set(reunionLabel);
|
||||
this.cdr.markForCheck();
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subs.unsubscribe();
|
||||
}
|
||||
|
||||
courseTypes = [
|
||||
{ label: 'Gagnant', value: CourseType.GAGNANT },
|
||||
{ label: 'Placé', value: CourseType.PLACE },
|
||||
{ label: 'Jumélé gagnant', value: CourseType.JUMELE_GAGNANT},
|
||||
{ label: 'Jumélé placé', value: CourseType.JUMELE_PLACE},
|
||||
{ label: 'Jumélé ordre', value: CourseType.JUMELE_ORDRE},
|
||||
{ label: 'Trio', value: CourseType.TRIO},
|
||||
{ label: 'Trio ordre', value: CourseType.TRIO_ORDRE},
|
||||
{ label: 'Triplet', value: CourseType.TRIPLET},
|
||||
{ label: 'Quatro', value: CourseType.QUATRO},
|
||||
{ label: 'Quinte', value: CourseType.QUINTE},
|
||||
];
|
||||
|
||||
|
||||
courseStatus = [
|
||||
{ label: 'Brouillon', value: CourseStatut.BROUILLON },
|
||||
{ label: 'Validé', value: CourseStatut.VALIDE },
|
||||
{ label: 'Fremé', value: CourseStatut.FERME },
|
||||
{ label: 'Resultat Provisoire', value: CourseStatut.RESULTAT_PROVISOIRE },
|
||||
{ label: 'Resultat officiel', value: CourseStatut.RESULTAT_OFFICIEL },
|
||||
{ label: 'Reglée', value: CourseStatut.REGLEE },
|
||||
{ label: 'Annulée', value: CourseStatut.ANNULEE },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export class NonPartantForm implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
private seedFromCourse() {
|
||||
const max = this.course?.partants ?? 0;
|
||||
const max = this.course?.nombrePartants ?? 0;
|
||||
this.partantsMax.set(max);
|
||||
|
||||
const arr = new FormArray<FormGroup<NonPartantRow>>([]);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
<h3 class="font-semibold text-sm">{{ course.nom }}</h3>
|
||||
<p class="text-xs text-gray-500">
|
||||
{{ course.reunion.nom }} • {{ course.reunion.hippodrome.nom }}
|
||||
{{ course.nom }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded bg-blue-100 text-blue-700">
|
||||
|
||||
@@ -35,9 +35,9 @@ export class ResultatForm {
|
||||
});
|
||||
|
||||
reqLen = computed(() =>
|
||||
this.course?.type === CourseType.TIERCE ? 3 : this.course?.type === CourseType.QUARTE ? 4 : 5
|
||||
3
|
||||
);
|
||||
maxNum = computed(() => this.course?.partants ?? 0);
|
||||
maxNum = computed(() => this.course?.nombrePartants ?? 0);
|
||||
npSet = computed(() => new Set(this.course?.nonPartants ?? []));
|
||||
statut = computed((): 'CREATED' | 'VALIDATED' | 'NONE' => {
|
||||
return this.resultat ? 'CREATED' : 'NONE';
|
||||
|
||||
@@ -229,7 +229,7 @@ export class ReunionForm implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.subs.add(
|
||||
this.hippodromeService.list({ page: 1, perPage: 1000 }, false).subscribe({
|
||||
next: (result) => {
|
||||
this.hippodromes.set(result.data);
|
||||
this.hippodromes.set(result.content);
|
||||
this.loadingHippodromes.set(false);
|
||||
// The effect will automatically re-run when hippodromes signal updates
|
||||
},
|
||||
|
||||
@@ -63,7 +63,7 @@ export class UserForm {
|
||||
this.roleService
|
||||
.list({ page: 1, perPage: 100, search: '', sortKey: 'name', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => {
|
||||
this.roles = res.data as any;
|
||||
this.roles = res.content as any;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,69 +7,34 @@ import { PagedResult, PageMeta } from './paging';
|
||||
export function normalizePage<T>(raw: any, reqPage: number, perPage: number): PagedResult<T> {
|
||||
// 🟩 Case 1 — Spring Data style: { content, totalElements, number, size }
|
||||
if (raw && Array.isArray(raw.content) && typeof raw.totalElements === 'number') {
|
||||
return {
|
||||
data: raw.content as T[],
|
||||
meta: {
|
||||
page: (raw.number ?? 0) + 1,
|
||||
perPage: raw.size ?? perPage,
|
||||
total: raw.totalElements,
|
||||
...(raw.meta ?? {}), // merge extra meta if provided
|
||||
} as PageMeta,
|
||||
};
|
||||
return raw;
|
||||
}
|
||||
|
||||
// 🟩 Case 2 — API or local mock: { data, meta: { total, ...extra } }
|
||||
if (raw && Array.isArray(raw.data) && raw.meta?.total != null) {
|
||||
return {
|
||||
data: raw.data as T[],
|
||||
meta: {
|
||||
page: raw.meta.page ?? reqPage,
|
||||
perPage: raw.meta.perPage ?? perPage,
|
||||
total: raw.meta.total,
|
||||
...raw.meta, // keep any custom stats
|
||||
} as PageMeta,
|
||||
};
|
||||
if (raw && Array.isArray(raw.content) && raw.pageable?.total != null) {
|
||||
return raw
|
||||
}
|
||||
|
||||
// 🟩 Case 3 — Generic REST: { items, total | total_count, ...extra }
|
||||
if (raw && Array.isArray(raw.items) && (raw.total != null || raw.total_count != null)) {
|
||||
const total = raw.total ?? raw.total_count;
|
||||
return {
|
||||
data: raw.items as T[],
|
||||
meta: {
|
||||
page: reqPage,
|
||||
perPage,
|
||||
total,
|
||||
...raw.meta, // optional
|
||||
} as PageMeta,
|
||||
};
|
||||
}
|
||||
|
||||
// 🟩 Case 4 — Direct array (no meta)
|
||||
if (Array.isArray(raw)) {
|
||||
const start = (reqPage - 1) * perPage;
|
||||
const data = (raw as T[]).slice(start, start + perPage);
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
page: reqPage,
|
||||
perPage,
|
||||
total: (raw as T[]).length,
|
||||
} as PageMeta,
|
||||
content: data,
|
||||
pageable:{
|
||||
pageNumber: raw.length/perPage,
|
||||
pageSize: perPage,
|
||||
total: raw.length
|
||||
},
|
||||
totalPages: raw.length/perPage,
|
||||
totalElements: raw.length
|
||||
};
|
||||
}
|
||||
|
||||
// 🟩 Fallback — ensure consistency even with minimal info
|
||||
const data = Array.isArray(raw?.data) ? (raw.data as T[]) : [];
|
||||
const data = Array.isArray(raw?.content) ? (raw.content as T[]) : [];
|
||||
const total = typeof raw?.total === 'number' ? raw.total : data.length ?? 0;
|
||||
|
||||
return {
|
||||
data,
|
||||
meta: {
|
||||
page: raw?.meta?.page ?? reqPage,
|
||||
perPage: raw?.meta?.perPage ?? perPage,
|
||||
total,
|
||||
...raw?.meta, // merge additional stats safely
|
||||
} as PageMeta,
|
||||
};
|
||||
return raw;
|
||||
}
|
||||
|
||||
@@ -11,15 +11,16 @@ export interface ListParams {
|
||||
}
|
||||
|
||||
export interface PageMeta {
|
||||
page: number; // 1-based
|
||||
perPage: number;
|
||||
pageNumber: number; // 1-based
|
||||
pageSize: number;
|
||||
total: number; // -1 if unknown
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface PagedResult<T> {
|
||||
data: T[];
|
||||
meta: PageMeta;
|
||||
content: T[];
|
||||
pageable: PageMeta;
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
}
|
||||
|
||||
export interface BackendConfig {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'https://custody-holding-rogers-less.trycloudflare.com',
|
||||
apiBaseUrl: 'https://ddd3b90fc1ef.ngrok-free.app',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'https://custody-holding-rogers-less.trycloudflare.com',
|
||||
apiBaseUrl: 'https://ddd3b90fc1ef.ngrok-free.app',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user