detail on course

This commit is contained in:
OnlyPapy98
2025-12-31 14:53:40 +01:00
parent 87c33f25cf
commit afa5fab55d
17 changed files with 462 additions and 41 deletions

View File

@@ -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),

View File

@@ -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[] = [

View File

@@ -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)); }
}

View File

@@ -0,0 +1,65 @@
@if(detail()){
<div class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-semibold">Détails du gain — Course n° {{ detail()!.course.numero }}</h2>
<div class="flex items-center gap-2">
<a z-button routerLink="/gains" zType="ghost"><i class="icon-arrow-left mr-2"></i>Retour</a>
</div>
</div>
<z-card class="p-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 text-sm">
<div>
<span class="font-medium">Date:</span>
{{ detail()!.course.reunionDate | date : 'dd/MM/yyyy' }}
</div>
<div><span class="font-medium">Nom:</span> {{ detail()!.course.nom }}</div>
<div><span class="font-medium">Montant cagnotte:</span> {{ detail()!.montantCagnotte | number : '1.0-0' : 'fr-FR' }} CFA</div>
<div><span class="font-medium">Montant à rembourser:</span> {{ detail()!.montantARembourser | number : '1.0-0' : 'fr-FR' }} CFA</div>
</div>
</z-card>
@if(detail()!.formules && detail()!.formules.length > 0) {
<div class="space-y-4">
@for (f of detail()!.formules; track f.id || $index) {
<z-card class="p-3">
<div class="flex items-center justify-between mb-2">
<div class="text-sm font-medium">Formule: {{ f.gains }} — {{ f.typeFormule }}</div>
<div class="text-sm text-muted">Type pari: {{ f.typePari }}</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-2 text-sm mb-3">
<div><span class="font-medium">Masse initiale:</span> {{ f.masseInitiale | number }}</div>
<div><span class="font-medium">Masse après prélèvements:</span> {{ f.masseApresPrelevements | number }}</div>
<div><span class="font-medium">Masse finale:</span> {{ f.masseFinale | number }}</div>
<div><span class="font-medium">Total gagnants:</span> {{ f.totalGagnants }}</div>
</div>
<div class="rounded-md border overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-accent">
<tr>
<th class="text-left p-2">Libellé</th>
<th class="text-left p-2">Rapport</th>
<th class="text-left p-2">Nombre gagnants</th>
<th class="text-left p-2">Masse partagée</th>
</tr>
</thead>
<tbody>
@for (r of f.rapportsDetails; track r.id || $index) {
<tr class="border-t">
<td class="p-2">{{ r.libelle }}</td>
<td class="p-2">{{ r.rapport }}</td>
<td class="p-2">{{ r.nombreGagnants }}</td>
<td class="p-2">{{ r.massePartageeRang | number }}</td>
</tr>
}
</tbody>
</table>
</div>
</z-card>
}
</div>
}
</div>
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GainDetails } from './gain-details';
describe('GainDetails', () => {
let component: GainDetails;
let fixture: ComponentFixture<GainDetails>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GainDetails]
})
.compileComponents();
fixture = TestBed.createComponent(GainDetails);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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<ResultatCagnotte | undefined>(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']);
}
}

View File

View File

@@ -0,0 +1,34 @@
<div class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-semibold">Gains — Cagnotte</h2>
<z-button zType="default" (click)="fetchPage()">Récupérer les gains</z-button>
</div>
<div class="mt-4">
<app-data-table [data]="rows()" [columns]="cols" [loading]="loading()">
<ng-template #rowActions let-row>
<z-button zType="ghost" zSize="icon" aria-label="Voir le détail" (click)="openReport(row)">
<div class="icon-file-text"></div>
</z-button>
</ng-template>
</app-data-table>
<div class="flex items-center justify-between mt-3">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<label class="text-sm">Lignes par page</label>
<select class="border rounded px-2 py-1" [value]="perPage()" (change)="onPerPageChangeEvent($event)">
<option [value]="5">5</option>
<option [value]="10">10</option>
<option [value]="25">25</option>
<option [value]="50">50</option>
</select>
</div>
<div class="text-sm text-muted">{{ totalElements() }} résultats</div>
</div>
<div>
<z-pagination [zPageIndex]="page()" [zTotal]="totalPages()" (zPageIndexChange)="onPageChange($event)"></z-pagination>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Gains } from './gains';
describe('Gains', () => {
let component: Gains;
let fixture: ComponentFixture<Gains>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Gains]
})
.compileComponents();
fixture = TestBed.createComponent(Gains);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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<ResultatCagnotte[]>([]);
loading = signal(false);
// Pagination
page = signal<number>(1);
perPage = signal<number>(10);
totalPages = signal<number>(1);
totalElements = signal<number>(0);
cols: TableColumn<ResultatCagnotte>[] = [
{ 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<ListParams>) {
this.loading.set(true);
const p: ListParams = { page: this.page(), size: this.perPage(), ...(params || {}) };
this.gainService.list(p).subscribe({
next: (res: PagedResult<ResultatCagnotte>) => {
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);
}
}
}

View File

@@ -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<ResultatCourse, "id"> = {
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({