diff --git a/src/app/core/interfaces/gain.ts b/src/app/core/interfaces/gain.ts new file mode 100644 index 0000000..7733784 --- /dev/null +++ b/src/app/core/interfaces/gain.ts @@ -0,0 +1,41 @@ +import { Course, CourseType } from "./course"; + +export type TypeFormule = + | 'UNITAIRE' + | 'CHAMP_X' + | 'CHAMP_TOTAL' + | 'FORMULE_COMPLETE'; + + + export interface RapportDetail { + id: number; + libelle: string; + rapport: number; + nombreGagnants: number; + massePartageeRang: number; + gainsFormule: string; +} + + +export interface Formule { + id: number; + gains: string; + typePari: CourseType; + typeFormule: TypeFormule; + masseInitiale: number; + masseApresPrelevements: number; + masseFinale: number; + totalPari: number; + totalGagnants: number; + rapportsDetails: RapportDetail[]; +} + + +export interface ResultatCagnotte { + id: number; + course: Course; + montantCagnotte: number; + montantARembourser: number; + dateCalcul: string; + formules: Formule[]; +} diff --git a/src/app/core/services/course.ts b/src/app/core/services/course.ts index 9ab3dee..0b70341 100644 --- a/src/app/core/services/course.ts +++ b/src/app/core/services/course.ts @@ -157,7 +157,6 @@ export class CourseService { } getById(id: string): Observable { - if (USE_SERVER) { return this.http .get(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }) .pipe( @@ -165,9 +164,6 @@ export class CourseService { // Fetch the reunion (non-partants are already included in the API response) return this.hippodromeService.getById(String(apiCourse.hippodromeId)).pipe( map((hippodrome) => { - if (!hippodrome) { - return undefined; - } return { id: String(apiCourse.id), hippodrome: hippodrome ?? undefined, @@ -196,8 +192,6 @@ export class CourseService { return of(undefined); }) ); - } - return of(undefined); } // getByReunionId(reunionId: string): Observable { diff --git a/src/app/core/services/depouillement.ts b/src/app/core/services/depouillement.ts index 3b55458..c0ff0ee 100644 --- a/src/app/core/services/depouillement.ts +++ b/src/app/core/services/depouillement.ts @@ -8,15 +8,10 @@ import { environment } from 'src/environments/environment.development'; export interface ResultatCourse { id: number; - course: Course; + course: Partial; statut: ResultatStatut; ordreArrivee: string; - datePublication?: string; // ISO string - dateValidation?: string; // ISO string - dateAnnulation?: string; // ISO string - notes?: string; - createdAt?: string; - updatedAt?: string; + datePublication?: string; } const API_BASE = '/api/v1/depouillement'; diff --git a/src/app/core/services/gain.spec.ts b/src/app/core/services/gain.spec.ts new file mode 100644 index 0000000..5df8b14 --- /dev/null +++ b/src/app/core/services/gain.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { Gain } from './gain'; + +describe('Gain', () => { + let service: Gain; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(Gain); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/core/services/gain.ts b/src/app/core/services/gain.ts new file mode 100644 index 0000000..841c3ef --- /dev/null +++ b/src/app/core/services/gain.ts @@ -0,0 +1,107 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, of } from 'rxjs'; +import { map, catchError, switchMap } from 'rxjs/operators'; +import { ResultatCagnotte, Formule } from '../interfaces/gain'; +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/gains'; + +export interface ResultatCagnotteApi extends ResultatCagnotte {} + +@Injectable({ + providedIn: 'root', +}) +export class Gain { + private apiUrl = environment.depouillementBaseUrl + API_BASE; + + constructor(private http: HttpClient, private pager: PaginatedHttpService, private servicesUtil: ServicesUtils) {} + + private getNgrokHeaders(): Record { + const isNgrok = + environment.apiBaseUrl.includes('ngrok-free.app') || + environment.apiBaseUrl.includes('ngrok.io') || + environment.apiBaseUrl.includes('ngrok'); + return isNgrok ? { 'ngrok-skip-browser-warning': 'true' } : {}; + } + + // List (paginated) ResultatCagnotte + list(params: ListParams): Observable> { + if (USE_SERVER) { + const url = this.apiUrl; + return this.pager.fetch(url, params).pipe( + map((res) => { + const content = (res.content ?? []).map((api) => api as ResultatCagnotte); + return { + pageable: res.pageable, + totalPages: res.totalPages, + totalElements: res.totalElements, + content, + } as PagedResult; + }), + catchError((err) => { + console.error('Error fetching gains list:', err); + return of({ content: [], pageable: { pageNumber: 1, pageSize: 0, total: 0 }, totalPages: 1, totalElements: 0 } as PagedResult); + }) + ); + } + + return of({ content: [], pageable: { pageNumber: 1, pageSize: 0, total: 0 }, totalPages: 1, totalElements: 0 } as PagedResult); + } + + getById(id: string): Observable { + if (USE_SERVER) { + return this.http.get(`${this.apiUrl}/rapport/${id}`, { headers: this.getNgrokHeaders() }).pipe( + map((api) => api as ResultatCagnotte), + catchError((err) => { + console.error(`Error fetching gain ${id}:`, err); + return of(undefined); + }) + ); + } + return of(undefined); + } + + create(payload: Partial): Observable { + if (USE_SERVER) { + return this.http.post(this.apiUrl, payload, { headers: this.getNgrokHeaders() }).pipe( + map((api) => api as ResultatCagnotte), + catchError((err) => { + console.error('Error creating gain:', err); + throw err; + }) + ); + } + throw new Error('Server mode required'); + } + + update(id: string, payload: Partial): Observable { + if (USE_SERVER) { + return this.http.put(`${this.apiUrl}/${id}`, payload, { headers: this.getNgrokHeaders() }).pipe( + map((api) => api as ResultatCagnotte), + catchError((err) => { + console.error(`Error updating gain ${id}:`, err); + return of(undefined); + }) + ); + } + return of(undefined); + } + + delete(id: string): Observable { + if (USE_SERVER) { + return this.http.delete(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }).pipe( + catchError((err) => { + console.error(`Error deleting gain ${id}:`, err); + throw err; + }) + ); + } + return of(void 0); + } +} diff --git a/src/app/dashboard/dashboard-routing-module.ts b/src/app/dashboard/dashboard-routing-module.ts index b0e18fd..3b61cba 100644 --- a/src/app/dashboard/dashboard-routing-module.ts +++ b/src/app/dashboard/dashboard-routing-module.ts @@ -25,6 +25,14 @@ const routes: Routes = [ path: 'resultat', loadComponent: () => import('./pages/rapport/rapport').then((m) => m.Rapport), }, + { + path: 'gains', + loadComponent: () => import('./pages/gains/gains').then((m) => m.Gains), + }, + { + path: 'gains/:id', + loadComponent: () => import('./pages/gain-details/gain-details').then((m) => m.GainDetails), + }, { path: 'users', loadComponent: () => import('./pages/users/users').then((m) => m.UsersPage), diff --git a/src/app/dashboard/layout/layout.ts b/src/app/dashboard/layout/layout.ts index 1539fc2..9308c25 100644 --- a/src/app/dashboard/layout/layout.ts +++ b/src/app/dashboard/layout/layout.ts @@ -47,6 +47,7 @@ export class Layout { { icon: '📅', label: 'Reunions', link: '/reunions' }, { icon: '🏇', label: 'Courses', link: '/courses' }, { icon: 'icon-chart-bar', label: 'Résultats des courses', link: '/resultat' }, + { icon: '💰', label: 'Gains (cagnotte)', link: '/gains' }, ]; workspaceMenuItems: MenuItem[] = [ diff --git a/src/app/dashboard/pages/gain-details/gain-details.css b/src/app/dashboard/pages/gain-details/gain-details.css new file mode 100644 index 0000000..e3818e7 --- /dev/null +++ b/src/app/dashboard/pages/gain-details/gain-details.css @@ -0,0 +1,16 @@ +/* Cohesive styles for gain-details */ +.space-y-4 > * + * { margin-top: 1rem; } +.text-muted { color: var(--muted-foreground, #6b7280); } +.bg-accent { background-color: var(--accent, #f3f4f6); } +.p-2 { padding: 0.5rem; } +.p-3 { padding: 0.75rem; } +.p-4 { padding: 1rem; } +.rounded-md { border-radius: 0.375rem; } +.border { border: 1px solid var(--border, #e5e7eb); } +.font-medium { font-weight: 600; } + +/* Responsive tweaks */ +@media (min-width: 768px) { + .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } +} + diff --git a/src/app/dashboard/pages/gain-details/gain-details.html b/src/app/dashboard/pages/gain-details/gain-details.html new file mode 100644 index 0000000..5509791 --- /dev/null +++ b/src/app/dashboard/pages/gain-details/gain-details.html @@ -0,0 +1,65 @@ +@if(detail()){ +
+
+

Détails du gain — Course n° {{ detail()!.course.numero }}

+
+ Retour +
+
+ + +
+
+ Date: + {{ detail()!.course.reunionDate | date : 'dd/MM/yyyy' }} +
+
Nom: {{ detail()!.course.nom }}
+
Montant cagnotte: {{ detail()!.montantCagnotte | number : '1.0-0' : 'fr-FR' }} CFA
+
Montant Ă  rembourser: {{ detail()!.montantARembourser | number : '1.0-0' : 'fr-FR' }} CFA
+
+
+ + @if(detail()!.formules && detail()!.formules.length > 0) { +
+ @for (f of detail()!.formules; track f.id || $index) { + +
+
Formule: {{ f.gains }} — {{ f.typeFormule }}
+
Type pari: {{ f.typePari }}
+
+ +
+
Masse initiale: {{ f.masseInitiale | number }}
+
Masse après prélèvements: {{ f.masseApresPrelevements | number }}
+
Masse finale: {{ f.masseFinale | number }}
+
Total gagnants: {{ f.totalGagnants }}
+
+ +
+ + + + + + + + + + + @for (r of f.rapportsDetails; track r.id || $index) { + + + + + + + } + +
LibelléRapportNombre gagnantsMasse partagée
{{ r.libelle }}{{ r.rapport }}{{ r.nombreGagnants }}{{ r.massePartageeRang | number }}
+
+
+ } +
+ } +
+} diff --git a/src/app/dashboard/pages/gain-details/gain-details.spec.ts b/src/app/dashboard/pages/gain-details/gain-details.spec.ts new file mode 100644 index 0000000..19f2d77 --- /dev/null +++ b/src/app/dashboard/pages/gain-details/gain-details.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GainDetails } from './gain-details'; + +describe('GainDetails', () => { + let component: GainDetails; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [GainDetails] + }) + .compileComponents(); + + fixture = TestBed.createComponent(GainDetails); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dashboard/pages/gain-details/gain-details.ts b/src/app/dashboard/pages/gain-details/gain-details.ts new file mode 100644 index 0000000..f7dc1f3 --- /dev/null +++ b/src/app/dashboard/pages/gain-details/gain-details.ts @@ -0,0 +1,37 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { ActivatedRoute, RouterModule, Router } from '@angular/router'; +import { ZardCardComponent } from '@shared/components/card/card.component'; +import { ZardButtonComponent } from '@shared/components/button/button.component'; +import { Gain } from 'src/app/core/services/gain'; +import { ResultatCagnotte } from 'src/app/core/interfaces/gain'; + +@Component({ + standalone: true, + selector: 'app-gain-details', + templateUrl: './gain-details.html', + styleUrl: './gain-details.css', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, RouterModule, ZardCardComponent, ZardButtonComponent], +}) +export class GainDetails { + detail = signal(undefined); + + constructor(private route: ActivatedRoute, private api: Gain, private router: Router) { + const id = this.route.snapshot.params['id']; + if (!id) { + // nothing to show, go back + this.router.navigate(['/gains']); + return; + } + + this.api.getById(String(id)).subscribe((d) => { + console.log(d); + this.detail.set(d); + }); + } + + goBack() { + this.router.navigate(['/gains']); + } +} diff --git a/src/app/dashboard/pages/gains/gains.css b/src/app/dashboard/pages/gains/gains.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dashboard/pages/gains/gains.html b/src/app/dashboard/pages/gains/gains.html new file mode 100644 index 0000000..bf3b318 --- /dev/null +++ b/src/app/dashboard/pages/gains/gains.html @@ -0,0 +1,34 @@ +
+
+

Gains — Cagnotte

+ Récupérer les gains +
+ +
+ + + +
+
+
+
+ +
+
+
+ + +
+
{{ totalElements() }} résultats
+
+
+ +
+
+
+
diff --git a/src/app/dashboard/pages/gains/gains.spec.ts b/src/app/dashboard/pages/gains/gains.spec.ts new file mode 100644 index 0000000..28c933d --- /dev/null +++ b/src/app/dashboard/pages/gains/gains.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Gains } from './gains'; + +describe('Gains', () => { + let component: Gains; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Gains] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Gains); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dashboard/pages/gains/gains.ts b/src/app/dashboard/pages/gains/gains.ts new file mode 100644 index 0000000..13fe1e3 --- /dev/null +++ b/src/app/dashboard/pages/gains/gains.ts @@ -0,0 +1,84 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { DataTable, TableColumn } from '@shared/components/data-table/data-table'; +import { ZardButtonComponent } from '@shared/components/button/button.component'; +import { ZardPaginationModule } from '@shared/components/pagination/pagination.module'; +import { ListParams, PagedResult } from '@shared/paging/paging'; +import { Gain } from 'src/app/core/services/gain'; +import { ResultatCagnotte } from 'src/app/core/interfaces/gain'; + +@Component({ + standalone: true, + selector: 'app-gains', + templateUrl: './gains.html', + styleUrl: './gains.css', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, DataTable, ZardButtonComponent, ZardPaginationModule], +}) +export class Gains { + rows = signal([]); + loading = signal(false); + // Pagination + page = signal(1); + perPage = signal(10); + totalPages = signal(1); + totalElements = signal(0); + + cols: TableColumn[] = [ + { key: 'course.nom', label: 'Course' , cell: (r) => r.course?.nom ?? '—'}, + { key: 'montantCagnotte', label: 'Montant cagnotte', cell: (r) => `${r.montantCagnotte.toLocaleString('fr-FR')} CFA` }, + { key: 'montantARembourser', label: 'Montant à rembourser', cell: (r) => `${r.montantARembourser.toLocaleString('fr-FR')} CFA` }, + { key: 'dateCalcul', label: 'Date calcul', cell: (r) => r.dateCalcul ?? '—' }, + ]; + + constructor(private gainService: Gain, private router: Router) { + this.fetchPage(); + } + + fetchPage(params?: Partial) { + this.loading.set(true); + const p: ListParams = { page: this.page(), size: this.perPage(), ...(params || {}) }; + this.gainService.list(p).subscribe({ + next: (res: PagedResult) => { + this.rows.set(res.content || []); + this.totalPages.set(res.totalPages ?? 1); + this.totalElements.set(res.totalElements ?? (res.content?.length ?? 0)); + if ((res as any).pageable?.pageNumber) this.page.set((res as any).pageable.pageNumber); + this.loading.set(false); + }, + error: (err) => { + console.error('Error fetching gains:', err); + this.rows.set([]); + this.loading.set(false); + } + }); + } + + onPageChange(next: number) { + this.page.set(next); + this.fetchPage(); + } + + onPerPageChange(size: number) { + this.perPage.set(size); + this.page.set(1); + this.fetchPage(); + } + + // Template-friendly wrapper to safely parse the select event value + onPerPageChangeEvent(e: Event) { + const v = (e.target as HTMLSelectElement)?.value; + const size = Number(v) || 10; + this.onPerPageChange(size); + } + + openReport(row: ResultatCagnotte) { + try { + // Navigate to detail page + this.router.navigate(['/gains', String(row.course.id)]); + } catch (err) { + console.error('Failed to open gain details for', row, err); + } + } +} diff --git a/src/app/dashboard/pages/rapport/rapport.ts b/src/app/dashboard/pages/rapport/rapport.ts index b72cb75..f365e3b 100644 --- a/src/app/dashboard/pages/rapport/rapport.ts +++ b/src/app/dashboard/pages/rapport/rapport.ts @@ -114,43 +114,19 @@ export class Rapport { this.setSending(id, true); // Build a minimal ResultatCourse payload using available fields. - const course: Course = { - id: String((row as any).courseId ?? ''), - hippodrome: undefined, - reunionNumero: Number((row as any).reunionNumero ?? 0), - reunionDate: '', - nom: row.courseNom ?? '', - numero: Number(row.courseNumero ?? 0), - heureDepartPrevue: '', - discipline: '', - distanceMetres: 0, - categorie: '', - nombrePartants: 0, - statut: '', - annulee: false, - reporteeMemeJour: false, - reporteeAutreJour: false, - incidentTechnique: false, - nonPartants: [], - typesParisOuverts: [], + const course = { + id: String((row as any).courseId ?? '') }; - const payload: ResultatCourse = { - id: Number(row.id as any), + const payload: Omit = { course, statut: (row.statut as any) ?? (0 as any), ordreArrivee: String(row.ordreArrivee ?? ''), datePublication: row.datePublication ?? row.createdAt, - dateValidation: row.dateValidation, - dateAnnulation: row.dateAnnulation, - notes: '', - createdAt: row.createdAt, - updatedAt: row.updatedAt, }; this.depouillement.sendResultat(payload).subscribe({ next: (res) => { - console.debug('Depouillement sent:', res); // After successful depouillement, update the resultat statut to PROVISOIRE const updateId = String((res && (res as any).id) ?? row.id); this.api.update(updateId, { statut: ResultatStatut.PROVISOIRE }).subscribe({ diff --git a/src/app/shared/forms/course-form/course-form.ts b/src/app/shared/forms/course-form/course-form.ts index 91036c4..0a74f39 100644 --- a/src/app/shared/forms/course-form/course-form.ts +++ b/src/app/shared/forms/course-form/course-form.ts @@ -334,7 +334,8 @@ onSubmit() { if (updated) this.save.emit(updated); else console.error('Update returned empty result'); }, - error: (err) => console.error('Error updating course:', err), + error: (err) => { + console.error('Error updating course:', err)}, }); } else { this.courseServive.create(payload).subscribe({