test
This commit is contained in:
@@ -88,54 +88,16 @@ export class AgentsPage {
|
||||
}
|
||||
|
||||
cols: TableColumn<Agent>[] = [
|
||||
{ key: 'code', label: 'Code', sortable: true },
|
||||
{ key: 'nom', label: 'Nom', sortable: true },
|
||||
{ key: 'prenom', label: 'Prénom', sortable: true },
|
||||
{ key: 'phone', label: 'Téléphone', sortable: true },
|
||||
{
|
||||
key: 'tpes',
|
||||
label: 'TPE assignés',
|
||||
cell: (a) => {
|
||||
const tpes = this.agentTpesMap.get(a.id) || [];
|
||||
if (tpes.length === 0) {
|
||||
return '<span class="text-muted-foreground text-sm">Aucun</span>';
|
||||
}
|
||||
// Show up to 2 TPEs with full details, then count for the rest
|
||||
const displayCount = Math.min(2, tpes.length);
|
||||
const displayed = tpes.slice(0, displayCount);
|
||||
const remaining = tpes.length - displayCount;
|
||||
|
||||
const tpeCards = displayed
|
||||
.map((t) => {
|
||||
const imei = `<div class="font-medium text-xs">${t.imei}</div>`;
|
||||
const details = [
|
||||
t.marque && t.modele ? `${t.marque} ${t.modele}` : t.marque || t.modele || '',
|
||||
t.statut ? this.formatTpeStatut(t.statut) : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' • ');
|
||||
const detailsHtml = details
|
||||
? `<div class="text-xs text-muted-foreground">${details}</div>`
|
||||
: '';
|
||||
return `<div class="px-2 py-1.5 rounded bg-primary/10 border border-primary/20 flex flex-col gap-0.5">${imei}${detailsHtml}</div>`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
const moreHtml =
|
||||
remaining > 0
|
||||
? `<div class="text-xs text-muted-foreground px-2 py-1.5">+${remaining} autre${
|
||||
remaining > 1 ? 's' : ''
|
||||
}</div>`
|
||||
: '';
|
||||
|
||||
return `<div class="flex flex-col gap-1">${tpeCards}${moreHtml}</div>`;
|
||||
},
|
||||
},
|
||||
{ key: 'code', label: 'Code', sortable: true, defaultVisible: true },
|
||||
{ key: 'nomPrenom', label: 'Nom complet', sortable: true, defaultVisible: true, cell: (a) => `${a.nom} ${a.prenom}` },
|
||||
{ key: 'profile', label: 'Profil', sortable: true, defaultVisible: true },
|
||||
{ key: 'statut', label: 'Statut', sortable: true, defaultVisible: true, cell: (a) => this.renderStatutBadge(a.statut) },
|
||||
{ key: 'phone', label: 'Téléphone', sortable: true, defaultVisible: true },
|
||||
{ key: 'zone', label: 'Zone', sortable: true },
|
||||
{ key: 'kiosk', label: 'Kiosque', sortable: true },
|
||||
{ key: 'profile', label: 'Profil', sortable: true },
|
||||
{ key: 'statut', label: 'Statut', sortable: true },
|
||||
{ key: 'limiteSuperieure', label: 'Limite sup.', sortable: true },
|
||||
{ key: 'tpes', label: 'TPE', cell: (a) => `${(this.getAgentTpes(a.id) || []).length}` },
|
||||
{ key: 'limites', label: 'Limites', cell: (a) => this.formatLimits(a) },
|
||||
{ key: 'dateEmbauche', label: 'Embauché le', cell: (a) => (a.dateEmbauche ? new Date(a.dateEmbauche).toLocaleDateString() : '') },
|
||||
];
|
||||
|
||||
tpeMap = new Map<string, TpeDevice>();
|
||||
@@ -216,6 +178,26 @@ export class AgentsPage {
|
||||
return this.agentTpesMap.get(agentId) || [];
|
||||
}
|
||||
|
||||
renderStatutBadge(statut: Agent['statut'] | string | undefined): string {
|
||||
if (!statut) return '';
|
||||
const s = String(statut).toUpperCase();
|
||||
if (s === 'ACTIF') {
|
||||
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-green-500/10 text-green-600 dark:text-green-400 text-xs font-medium"><i class="icon-check"></i> Actif</span>`;
|
||||
}
|
||||
if (s === 'INACTIF') {
|
||||
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-gray-500/10 text-gray-600 dark:text-gray-400 text-xs font-medium"><i class="icon-x"></i> Inactif</span>`;
|
||||
}
|
||||
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-orange-500/10 text-orange-600 dark:text-orange-400 text-xs font-medium"><i class="icon-alert-circle"></i> Suspendu</span>`;
|
||||
}
|
||||
|
||||
formatLimits(a: Agent): string {
|
||||
const parts: string[] = [];
|
||||
const nf = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 });
|
||||
if (a.limiteInferieure !== undefined) parts.push(nf.format(a.limiteInferieure));
|
||||
if (a.limiteSuperieure !== undefined) parts.push(nf.format(a.limiteSuperieure));
|
||||
return parts.length ? parts.join(' — ') : '';
|
||||
}
|
||||
|
||||
onSearch(q: string) {
|
||||
this.search.set(q);
|
||||
this.page.set(1);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</z-card>
|
||||
|
||||
<z-card class="text-center py-4">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">En cours</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Ouverts</div>
|
||||
<div class="text-3xl font-bold text-amber-600 dark:text-amber-400 mt-1">
|
||||
{{ runningCourses() }}
|
||||
</div>
|
||||
@@ -29,7 +29,7 @@
|
||||
</z-card>
|
||||
|
||||
<z-card class="text-center py-4">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Par type</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Nombre de statuts</div>
|
||||
<div class="text-sm mt-2 text-gray-900 dark:text-gray-100 space-y-1">
|
||||
@for (type of (byType() | keyvalue); track type.key) {
|
||||
<div class="flex justify-between px-3">
|
||||
@@ -116,19 +116,27 @@
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
@if (modalOpen()) {
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" size="xl" (close)="closeModal()">
|
||||
@if(modalOpen()) {
|
||||
<app-course-form
|
||||
[value]="editingItem() ?? undefined"
|
||||
(save)="onFormSave($event)"
|
||||
(cancel)="closeModal()"
|
||||
></app-course-form>
|
||||
}
|
||||
<div modal-actions class="flex gap-2 justify-end">
|
||||
<z-button zType="destructive" (click)="closeModal()">Annuler</z-button>
|
||||
<z-button zType="default" (click)="submitChildForm()">Enregistrer</z-button>
|
||||
<z-button
|
||||
zType="default"
|
||||
(click)="submitChildForm()"
|
||||
[attr.aria-disabled]="formComp?.form?.invalid ? 'true' : null"
|
||||
[class.opacity-50]="formComp?.form?.invalid"
|
||||
[class.pointer-events-none]="formComp?.form?.invalid"
|
||||
>
|
||||
Enregistrer
|
||||
</z-button>
|
||||
</div>
|
||||
</app-modal>
|
||||
}
|
||||
|
||||
@if(selectedCourse()) {
|
||||
<app-modal
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SearchBar } from '@shared/components/search-bar/search-bar';
|
||||
import { Modal } from '@shared/components/modal/modal';
|
||||
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 { CourseStatut, Course as CourseType } from 'src/app/core/interfaces/course';
|
||||
import { SortDir } from '@shared/paging/paging';
|
||||
import { CourseApiResponse, CourseService } from 'src/app/core/services/course';
|
||||
import { ResultatService } from 'src/app/core/services/resultat';
|
||||
@@ -210,9 +210,11 @@ export class Course {
|
||||
next: (res) => {
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.totalElements);
|
||||
this.totalRunning.set(0);
|
||||
this.totalClosed.set(0);
|
||||
this.totalByType.set({});
|
||||
this.totalRunning.set(res.content.filter(c=> c.statut === String(CourseStatut.OUVERT)).length);
|
||||
this.totalClosed.set(res.content.filter(c=>c.statut === String(CourseStatut.FERME)).length);
|
||||
this.totalByType.set({
|
||||
|
||||
});
|
||||
|
||||
// Fetch resultats for all courses in parallel
|
||||
const courseIds = res.content.map((c) => c.id);
|
||||
@@ -465,7 +467,10 @@ export class Course {
|
||||
}
|
||||
|
||||
// For now, validation is just an update. In the future, you might add a statut field
|
||||
this.resultatService.update(resultat.id, {}).subscribe({
|
||||
this.resultatService.update(resultat.id, {
|
||||
...resultat,
|
||||
statut: ResultatStatut.PROVISOIRE
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.closeResultatModal();
|
||||
this.fetch({
|
||||
@@ -495,7 +500,10 @@ export class Course {
|
||||
}
|
||||
|
||||
// For now, confirmation is just an update. In the future, you might add a statut field
|
||||
this.resultatService.update(resultat.id, {}).subscribe({
|
||||
this.resultatService.update(resultat.id, {
|
||||
...resultat,
|
||||
statut: ResultatStatut.OFFICIEL
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.closeResultatModal();
|
||||
this.fetch({
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Rapport — Courses avec résultats</h2>
|
||||
<z-button zType="default" (click)="fetch()">Récupérer le rapport</z-button>
|
||||
<h2 class="text-2xl font-semibold">Résultats — Courses</h2>
|
||||
<z-button zType="default" (click)="fetch()">Récupérer les résultats</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 rapport" (click)="openReport(row)">
|
||||
<z-button
|
||||
(click)="sendToDepouillement(row)"
|
||||
[zLoading]="isSending(row.id)"
|
||||
zType="ghost"
|
||||
zSize="icon"
|
||||
aria-label="Voir le résultat" >
|
||||
<div class="icon-file-text"></div>
|
||||
</z-button>
|
||||
</ng-template>
|
||||
|
||||
@@ -4,8 +4,11 @@ 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 { ResultatApiResponse } from 'src/app/core/interfaces/resultat';
|
||||
import { ResultatApiResponse, ResultatStatut } from 'src/app/core/interfaces/resultat';
|
||||
import { ResultatService } from 'src/app/core/services/resultat';
|
||||
import { Depouillement, ResultatCourse } from 'src/app/core/services/depouillement';
|
||||
import { Course } from 'src/app/core/interfaces/course';
|
||||
import { toast } from 'ngx-sonner';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -17,6 +20,7 @@ import { ResultatService } from 'src/app/core/services/resultat';
|
||||
export class Rapport {
|
||||
rows = signal<ResultatApiResponse[]>([]);
|
||||
loading = signal(false);
|
||||
sending = signal<Map<string, boolean>>(new Map());
|
||||
// Pagination state
|
||||
page = signal<number>(1);
|
||||
perPage = signal<number>(10);
|
||||
@@ -34,7 +38,7 @@ export class Rapport {
|
||||
{ key: 'dateValidation', label: 'Date validation' },
|
||||
];
|
||||
|
||||
constructor(private api: ResultatService) {
|
||||
constructor(private api: ResultatService, private depouillement: Depouillement) {
|
||||
// initial load
|
||||
this.fetch();
|
||||
}
|
||||
@@ -85,10 +89,90 @@ export class Rapport {
|
||||
openReport(row: ResultatApiResponse) {
|
||||
try {
|
||||
// Open a per-result report URL in a new tab. Adjust path if your server uses another route.
|
||||
const url = `/rapport/${row.id}`;
|
||||
const url = `/resultat/${row.id}`;
|
||||
window.open(url, '_blank');
|
||||
} catch (err) {
|
||||
console.error('Failed to open report for', row, err);
|
||||
}
|
||||
}
|
||||
|
||||
isSending(id: string | number) {
|
||||
return !!this.sending().get(String(id));
|
||||
}
|
||||
|
||||
private setSending(id: string | number, v: boolean) {
|
||||
const map = new Map(this.sending());
|
||||
map.set(String(id), v);
|
||||
this.sending.set(map);
|
||||
}
|
||||
|
||||
sendToDepouillement(row: ResultatApiResponse) {
|
||||
if (!row || !row.id) return;
|
||||
const id = String(row.id);
|
||||
if (this.isSending(id)) return; // already sending
|
||||
|
||||
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 payload: ResultatCourse = {
|
||||
id: Number(row.id as any),
|
||||
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({
|
||||
next: (updated) => {
|
||||
// Update the local rows to reflect the new statut
|
||||
this.rows.set(
|
||||
this.rows().map((r) => (String(r.id) === String(updateId) ? { ...r, statut: ResultatStatut.PROVISOIRE } : r))
|
||||
);
|
||||
toast.success('Résultat envoyé au dépouillement et statut mis à jour.');
|
||||
this.setSending(id, false);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error updating resultat statut after depouillement:', err);
|
||||
toast.error('Échec de la mise à jour du statut du résultat.');
|
||||
this.setSending(id, false);
|
||||
},
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error sending to depouillement:', err);
|
||||
this.setSending(id, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
@if (modalOpen()) {
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" size="md" (close)="closeModal()">
|
||||
<app-reunion-form
|
||||
[value]="editingItem()"
|
||||
@@ -84,4 +85,5 @@
|
||||
<z-button zType="default" (click)="submitChildForm()">Enregistrer</z-button>
|
||||
</div>
|
||||
</app-modal>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -143,11 +143,13 @@
|
||||
</div>
|
||||
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xl">
|
||||
@if (modalOpen()) {
|
||||
<app-tpe-form
|
||||
[value]="editingItem() ?? undefined"
|
||||
(save)="onFormSave($event)"
|
||||
(cancel)="closeModal()"
|
||||
/>
|
||||
}
|
||||
<div modal-actions class="flex justify-end gap-2">
|
||||
<z-button zType="destructive" (click)="closeModal()">Annuler</z-button>
|
||||
<z-button (click)="submitChildForm()">Enregistrer</z-button>
|
||||
|
||||
Reference in New Issue
Block a user