Compare commits
3 Commits
ed79cae77d
...
afa5fab55d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa5fab55d | ||
|
|
87c33f25cf | ||
|
|
f21a5fd4e6 |
41
src/app/core/interfaces/gain.ts
Normal file
41
src/app/core/interfaces/gain.ts
Normal file
@@ -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[];
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export interface Resultat {
|
||||
totalMises: number;
|
||||
masseAPartager: number;
|
||||
prelevementsLegaux: number;
|
||||
statut: ResultatStatut;
|
||||
montantRembourse: number;
|
||||
montantCagnotte: number;
|
||||
adeadHeat: boolean;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { normalizePage } from '@shared/paging/normalize-page';
|
||||
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||
|
||||
const USE_SERVER = true;
|
||||
const API_BASE = '/api/v1/agents';
|
||||
const API_BASE = '/api/agents';
|
||||
|
||||
// Interface to match the API response structure for TPE (nested in Agent)
|
||||
// Note: When TPE is nested in Agent's tpes array, the agent field might be omitted or be a reference
|
||||
@@ -270,7 +270,6 @@ export class AgentService {
|
||||
|
||||
// GET /api/v1/agents - List all
|
||||
list(params?: ListParams): Observable<PagedResult<Agent>> {
|
||||
if (USE_SERVER) {
|
||||
let httpParams = new HttpParams();
|
||||
if (params) {
|
||||
if (params.page) httpParams = httpParams.set('page', params.page.toString());
|
||||
@@ -281,39 +280,30 @@ export class AgentService {
|
||||
}
|
||||
|
||||
return this.http
|
||||
.get<AgentApiResponse[]>(this.apiUrl, {
|
||||
.get<PagedResult<AgentApiResponse>>(this.apiUrl, {
|
||||
params: httpParams,
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((list) => {
|
||||
const agents = list.map((apiAgent) => {
|
||||
map((res) => {
|
||||
const agents = res.content.map((apiAgent) => {
|
||||
const transformed = this.transformAgent(apiAgent);
|
||||
return transformed;
|
||||
});
|
||||
// If pagination params provided, return paginated result
|
||||
if (params) {
|
||||
return normalizePage<Agent>(
|
||||
{ data: agents, meta: { total: agents.length } },
|
||||
params.page || 1,
|
||||
params.size || 10
|
||||
);
|
||||
const resAgent = {
|
||||
...res,
|
||||
content: agents
|
||||
}
|
||||
// Otherwise return all as single page
|
||||
return normalizePage<Agent>(
|
||||
{ data: agents, meta: { total: agents.length } },
|
||||
1,
|
||||
agents.length
|
||||
);
|
||||
return resAgent;
|
||||
}),
|
||||
catchError((err) => {
|
||||
console.error('Error fetching agents:', err);
|
||||
return of(normalizePage<Agent>({ data: [], meta: { total: 0 } }, 1, 10));
|
||||
return of(normalizePage<Agent>({ content: [], meta: { total: 0 } }, 1, 10));
|
||||
})
|
||||
);
|
||||
}
|
||||
return of(normalizePage<Agent>({ data: [], meta: { total: 0 } }, 1, 10));
|
||||
}
|
||||
|
||||
// POST /api/v1/agents - Create
|
||||
create(payload: Omit<Agent, 'id' | 'createdAt' | 'updatedAt'>): Observable<Agent> {
|
||||
|
||||
@@ -37,6 +37,10 @@ export interface CourseApiResponse {
|
||||
typesParisOuverts: Array<string>;
|
||||
}
|
||||
|
||||
export interface NonApiRequest {
|
||||
nonPartants: String[]
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CourseService {
|
||||
private apiUrl = environment.apiBaseUrl + API_BASE;
|
||||
@@ -153,7 +157,6 @@ export class CourseService {
|
||||
}
|
||||
|
||||
getById(id: string): Observable<Course | undefined> {
|
||||
if (USE_SERVER) {
|
||||
return this.http
|
||||
.get<CourseApiResponse>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() })
|
||||
.pipe(
|
||||
@@ -161,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,
|
||||
@@ -193,8 +193,6 @@ export class CourseService {
|
||||
})
|
||||
);
|
||||
}
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
// getByReunionId(reunionId: string): Observable<Course[]> {
|
||||
// if (USE_SERVER) {
|
||||
@@ -454,34 +452,20 @@ export class CourseService {
|
||||
}
|
||||
|
||||
addNonPartant(courseId: string, npList: string[]) {
|
||||
const payload = {
|
||||
nonPartants: [...npList]
|
||||
}
|
||||
console.warn('addNonPartant is deprecated. Use setNonPartants instead.');
|
||||
return this.setNonPartants(courseId, npList);
|
||||
return this.setNonPartants(courseId, payload);
|
||||
}
|
||||
|
||||
setNonPartants(courseId: string, npList: string[]): Observable<Course | undefined> {
|
||||
if (USE_SERVER) {
|
||||
// Use PUT endpoint to replace the entire list
|
||||
setNonPartants(courseId: string, npList: NonApiRequest): Observable<CourseApiResponse | undefined> {
|
||||
return this.nonPartantService.replaceNonPartants(courseId, npList).pipe(
|
||||
switchMap((updatedNonPartants) => {
|
||||
// Fetch the updated course to return it
|
||||
return this.getById(courseId).pipe(
|
||||
map((course) => {
|
||||
if (course) {
|
||||
return {
|
||||
...course,
|
||||
nonPartants: updatedNonPartants,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
}),
|
||||
map((updatedNonPartants) => updatedNonPartants),
|
||||
catchError((err) => {
|
||||
console.error(`Error setting nonPartants for course ${courseId}:`, err);
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
}
|
||||
throw new Error('Server mode is required');
|
||||
}
|
||||
}
|
||||
|
||||
40
src/app/core/services/depouillement.spec.ts
Normal file
40
src/app/core/services/depouillement.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { Depouillement, ResultatCourse } from './depouillement';
|
||||
import { environment } from 'src/environments/environment.development';
|
||||
|
||||
describe('Depouillement', () => {
|
||||
let service: Depouillement;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({ imports: [HttpClientTestingModule] });
|
||||
service = TestBed.inject(Depouillement);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => httpMock.verify());
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should POST resultat to depouillement endpoint', () => {
|
||||
const payload: ResultatCourse = {
|
||||
id: 12,
|
||||
course: { id: '1', hippodrome: undefined, reunionNumero: 0, reunionDate: '', nom: 'C1', numero: 1, heureDepartPrevue: '', discipline: '', distanceMetres: 0, categorie: '', nombrePartants: 0, statut: '', annulee: false, reporteeMemeJour: false, reporteeAutreJour: false, incidentTechnique: false, nonPartants: [], typesParisOuverts: [] },
|
||||
statut: 0 as any,
|
||||
ordreArrivee: '1,2,3',
|
||||
} as ResultatCourse;
|
||||
|
||||
service.sendResultat(payload).subscribe((res) => {
|
||||
expect(res).toBeTruthy();
|
||||
expect(res.id).toEqual(payload.id);
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne(environment.apiBaseUrl + '/api/depouillement');
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual(payload);
|
||||
req.flush(payload);
|
||||
});
|
||||
});
|
||||
48
src/app/core/services/depouillement.ts
Normal file
48
src/app/core/services/depouillement.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { Course } from '../interfaces/course';
|
||||
import { ResultatStatut } from '../interfaces/resultat';
|
||||
import { environment } from 'src/environments/environment.development';
|
||||
|
||||
export interface ResultatCourse {
|
||||
id: number;
|
||||
course: Partial<Course>;
|
||||
statut: ResultatStatut;
|
||||
ordreArrivee: string;
|
||||
datePublication?: string;
|
||||
}
|
||||
|
||||
const API_BASE = '/api/v1/depouillement';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class Depouillement {
|
||||
private apiUrl = environment.depouillementBaseUrl + API_BASE;
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
private getNgrokHeaders(): Record<string, string> {
|
||||
const isNgrok =
|
||||
environment.apiBaseUrl.includes('ngrok-free.app') ||
|
||||
environment.apiBaseUrl.includes('ngrok.io') ||
|
||||
environment.apiBaseUrl.includes('ngrok');
|
||||
return isNgrok ? { 'ngrok-skip-browser-warning': 'true' } : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a resultat to the dépouillement endpoint.
|
||||
* The backend expects a payload shaped like ResultatCourse.
|
||||
*/
|
||||
sendResultat(resultat: Omit<ResultatCourse, "id">): Observable<{}> {
|
||||
return this.http.post<ResultatCourse>(this.apiUrl, resultat, { headers: this.getNgrokHeaders() }).pipe(
|
||||
map((res) => res as ResultatCourse),
|
||||
catchError((err) => {
|
||||
console.error('Error sending resultat to depouillement:', err);
|
||||
return of(resultat); // return original payload on error to allow UI to proceed gracefully
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/app/core/services/gain.spec.ts
Normal file
16
src/app/core/services/gain.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
107
src/app/core/services/gain.ts
Normal file
107
src/app/core/services/gain.ts
Normal file
@@ -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<string, string> {
|
||||
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<PagedResult<ResultatCagnotte>> {
|
||||
if (USE_SERVER) {
|
||||
const url = this.apiUrl;
|
||||
return this.pager.fetch<ResultatCagnotteApi>(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<ResultatCagnotte>;
|
||||
}),
|
||||
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<ResultatCagnotte>);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return of({ content: [], pageable: { pageNumber: 1, pageSize: 0, total: 0 }, totalPages: 1, totalElements: 0 } as PagedResult<ResultatCagnotte>);
|
||||
}
|
||||
|
||||
getById(id: string): Observable<ResultatCagnotte | undefined> {
|
||||
if (USE_SERVER) {
|
||||
return this.http.get<ResultatCagnotteApi>(`${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<ResultatCagnotteApi>): Observable<ResultatCagnotte> {
|
||||
if (USE_SERVER) {
|
||||
return this.http.post<ResultatCagnotteApi>(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<ResultatCagnotteApi>): Observable<ResultatCagnotte | undefined> {
|
||||
if (USE_SERVER) {
|
||||
return this.http.put<ResultatCagnotteApi>(`${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<void> {
|
||||
if (USE_SERVER) {
|
||||
return this.http.delete<void>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }).pipe(
|
||||
catchError((err) => {
|
||||
console.error(`Error deleting gain ${id}:`, err);
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
return of(void 0);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment.development';
|
||||
import { CourseApiResponse, NonApiRequest } from './course';
|
||||
|
||||
const USE_SERVER = true;
|
||||
|
||||
@@ -20,21 +21,22 @@ export class NonPartantService {
|
||||
}
|
||||
|
||||
// PUT /api/v1/courses/{courseId}/non-partants - Replace the list of non-partants for a course
|
||||
replaceNonPartants(courseId: string, nonPartants: string[]): Observable<string[]> {
|
||||
if (USE_SERVER) {
|
||||
const courseApiUrl = environment.apiBaseUrl + '/api/v1/courses';
|
||||
replaceNonPartants(courseId: string, nonPartants: NonApiRequest): Observable<CourseApiResponse | undefined> {
|
||||
const courseApiUrl = environment.apiBaseUrl + '/api/courses';
|
||||
return this.http
|
||||
.put<string[]>(`${courseApiUrl}/${courseId}/non-partants`, nonPartants, {
|
||||
.patch<CourseApiResponse>(`${courseApiUrl}/${courseId}/non-partants`, nonPartants, {
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((list) => list.map((np) => String(np))),
|
||||
map((res) => {
|
||||
|
||||
return res
|
||||
}),
|
||||
catchError((err) => {
|
||||
console.log(err);
|
||||
console.error(`Error replacing non-partants for course ${courseId}:`, err);
|
||||
return of([]);
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
}
|
||||
return of([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,28 +137,43 @@ export class ResultatService {
|
||||
})
|
||||
.pipe(
|
||||
switchMap((raw) => {
|
||||
// Some courses don't have a resultat yet.
|
||||
// In that case the API returns 200 with a body like:
|
||||
// { "message": "Aucun résultat disponible pour cette course" }
|
||||
// We interpret this as "no resultat" and return undefined.
|
||||
if (
|
||||
raw &&
|
||||
typeof raw === 'object' &&
|
||||
'message' in raw &&
|
||||
!('id' in raw) &&
|
||||
!('ordreArrivee' in raw)
|
||||
) {
|
||||
// Debug raw response shape to help detect API changes
|
||||
console.debug(`ResultatService.getByCourseId(${courseId}) raw:`, raw);
|
||||
// Handle common variants of server responses:
|
||||
// - { message: '...' } -> no resultat
|
||||
// - { id: ..., ordreArrivee: '...' } -> single resultat
|
||||
// - [ { ... } ] -> array of resultats (pick first)
|
||||
// - { content: [...] } -> take first content
|
||||
|
||||
if (!raw) return of<Resultat | undefined>(undefined);
|
||||
|
||||
// message-only response -> no resultat
|
||||
if (typeof raw === 'object' && 'message' in raw && !('id' in raw) && !('ordreArrivee' in raw)) {
|
||||
return of<Resultat | undefined>(undefined);
|
||||
}
|
||||
|
||||
const apiResultat = raw as ResultatApiResponse;
|
||||
let apiResultat: ResultatApiResponse | undefined;
|
||||
|
||||
if (Array.isArray(raw)) {
|
||||
apiResultat = raw.length > 0 ? (raw[0] as ResultatApiResponse) : undefined;
|
||||
} else if (raw && typeof raw === 'object') {
|
||||
if ('id' in raw || 'ordreArrivee' in raw) {
|
||||
apiResultat = raw as ResultatApiResponse;
|
||||
} else if ('content' in raw && Array.isArray(raw.content) && raw.content.length > 0) {
|
||||
apiResultat = raw.content[0] as ResultatApiResponse;
|
||||
} else if ('data' in raw && raw.data && typeof raw.data === 'object') {
|
||||
apiResultat = raw.data as ResultatApiResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (!apiResultat) return of<Resultat | undefined>(undefined);
|
||||
|
||||
return this.courseService.getById(courseId).pipe(
|
||||
map((course) => {
|
||||
if (!course) {
|
||||
return undefined;
|
||||
}
|
||||
return this.transformApiResponse(apiResultat, course);
|
||||
return this.transformApiResponse(apiResultat!, course);
|
||||
})
|
||||
);
|
||||
}),
|
||||
@@ -264,6 +279,7 @@ export class ResultatService {
|
||||
course,
|
||||
// API now returns 'ordreArrivee' as CSV/string; normalize to number[]
|
||||
ordreArrivee: apiResultat.ordreArrivee,
|
||||
statut: apiResultat.statut,
|
||||
// dead-heat not provided by new API shape — default to empty
|
||||
chevauxDeadHeat: [],
|
||||
// Financial fields may not be present in new API; default to 0
|
||||
|
||||
@@ -9,64 +9,72 @@ import { normalizePage } from '@shared/paging/normalize-page';
|
||||
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||
|
||||
const USE_SERVER = true;
|
||||
const API_BASE = '/api/v1/tpes';
|
||||
const API_BASE = '/api/terminaux';
|
||||
|
||||
// Interface to match the API response structure for Agent (nested in TPE)
|
||||
interface AgentApiResponse {
|
||||
id: number;
|
||||
code: string;
|
||||
profile: string;
|
||||
principalCode?: string;
|
||||
caisseProfile?: string;
|
||||
statut: string;
|
||||
zone?: string;
|
||||
kiosk?: string;
|
||||
fonction?: string;
|
||||
dateEmbauche?: string;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
autresNoms?: string;
|
||||
dateNaissance?: string;
|
||||
lieuNaissance?: string;
|
||||
ville?: string;
|
||||
adresse?: string;
|
||||
autoriserAides?: boolean;
|
||||
phone: string;
|
||||
pin?: string;
|
||||
limiteInferieure?: number;
|
||||
limiteSuperieure?: number;
|
||||
limiteParTransaction?: number;
|
||||
limiteMinAirtime?: number;
|
||||
limiteMaxAirtime?: number;
|
||||
maxPeripheriques?: number;
|
||||
limitId?: number;
|
||||
nationalite?: string;
|
||||
cni?: string;
|
||||
cniDelivreeLe?: string;
|
||||
cniDelivreeA?: string;
|
||||
residence?: string;
|
||||
autreAdresse1?: string;
|
||||
statutMarital?: string;
|
||||
epoux?: string;
|
||||
autreTelephone?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
createdBy?: string;
|
||||
id: number,
|
||||
code: String,
|
||||
profil: String,
|
||||
principalCode: String,
|
||||
caisseProfile: String,
|
||||
statut: AgentStatus,
|
||||
zone: String,
|
||||
kiosk: String,
|
||||
fonction: String,
|
||||
dateEmbauche: String,
|
||||
nom: String,
|
||||
"prenom": String,
|
||||
"autresNoms": String,
|
||||
"dateNaissance": String,
|
||||
"lieuNaissance": String,
|
||||
"ville": String,
|
||||
"adresse": String,
|
||||
"autoriserAides": boolean,
|
||||
"phone": String,
|
||||
"limiteInferieure": number,
|
||||
"limiteSuperieure": number,
|
||||
"limiteParTransaction": number,
|
||||
"limiteMinAirtime": number,
|
||||
"limiteMaxAirtime": number,
|
||||
"maxPeripheriques": number,
|
||||
"limitId": String,
|
||||
"nationalite": String,
|
||||
"cni": String,
|
||||
"cniDelivreeLe": String,
|
||||
"cniDelivreeA": String,
|
||||
"residence": String,
|
||||
"autreAdresse1": String,
|
||||
"statutMarital": String,
|
||||
"epoux": String,
|
||||
"autreTelephone": String,
|
||||
"createdAt": String,
|
||||
"updatedAt": String,
|
||||
"createdBy": String,
|
||||
"terminauxIds": [
|
||||
number[]
|
||||
]
|
||||
}
|
||||
|
||||
// Interface to match the API response structure
|
||||
interface TpeApiResponse {
|
||||
id: number;
|
||||
imei: string;
|
||||
serial: string;
|
||||
type: string;
|
||||
marque: string;
|
||||
modele: string;
|
||||
statut: string; // API uses uppercase: VALIDE, INVALIDE, EN_PANNE, BLOQUE
|
||||
agent?: AgentApiResponse;
|
||||
assigne: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
id: number,
|
||||
numeroSerie: String,
|
||||
pointDeVenteId: number,
|
||||
statut: TpeStatus,
|
||||
derniereConnexion: String,
|
||||
versionLogicielle: String,
|
||||
typeTerminal: String,
|
||||
plateforme: String,
|
||||
modeleAppareil: String,
|
||||
systemeExploitation: String,
|
||||
versionOs: String,
|
||||
adresseIp: String,
|
||||
adresseMac: String,
|
||||
agentConnecteId: number,
|
||||
derniereConnexionAgent: String,
|
||||
derniereDeconnexionAgent: String,
|
||||
journalSession: String
|
||||
}
|
||||
|
||||
// Stats interfaces
|
||||
@@ -114,29 +122,29 @@ export class TpeService {
|
||||
return statut; // Already uppercase, no transformation needed
|
||||
}
|
||||
|
||||
// Transform API Agent response to Agent
|
||||
// Transform API Agent response to Agent (lightweight mapping)
|
||||
private transformAgent(apiAgent: AgentApiResponse): Agent {
|
||||
return {
|
||||
id: String(apiAgent.id),
|
||||
code: apiAgent.code,
|
||||
profile: apiAgent.profile,
|
||||
principalCode: apiAgent.principalCode,
|
||||
caisseProfile: apiAgent.caisseProfile,
|
||||
code: String((apiAgent as any).code || ''),
|
||||
profile: String((apiAgent as any).profile || ''),
|
||||
principalCode: (apiAgent as any).principalCode ? String((apiAgent as any).principalCode) : undefined,
|
||||
caisseProfile: (apiAgent as any).caisseProfile ? String((apiAgent as any).caisseProfile) : undefined,
|
||||
statut: apiAgent.statut as AgentStatus,
|
||||
zone: apiAgent.zone,
|
||||
kiosk: apiAgent.kiosk,
|
||||
fonction: apiAgent.fonction,
|
||||
dateEmbauche: apiAgent.dateEmbauche,
|
||||
nom: apiAgent.nom,
|
||||
prenom: apiAgent.prenom,
|
||||
autresNoms: apiAgent.autresNoms,
|
||||
dateNaissance: apiAgent.dateNaissance,
|
||||
lieuNaissance: apiAgent.lieuNaissance,
|
||||
ville: apiAgent.ville,
|
||||
adresse: apiAgent.adresse,
|
||||
autoriserAides: apiAgent.autoriserAides,
|
||||
phone: apiAgent.phone,
|
||||
pin: apiAgent.pin,
|
||||
zone: (apiAgent as any).zone ? String((apiAgent as any).zone) : undefined,
|
||||
kiosk: (apiAgent as any).kiosk ? String((apiAgent as any).kiosk) : undefined,
|
||||
fonction: (apiAgent as any).fonction ? String((apiAgent as any).fonction) : undefined,
|
||||
dateEmbauche: apiAgent.dateEmbauche ? String(apiAgent.dateEmbauche) : undefined,
|
||||
nom: String(apiAgent.nom || ''),
|
||||
prenom: String(apiAgent.prenom || ''),
|
||||
autresNoms: apiAgent.autresNoms ? String(apiAgent.autresNoms) : undefined,
|
||||
dateNaissance: apiAgent.dateNaissance ? String(apiAgent.dateNaissance) : undefined,
|
||||
lieuNaissance: apiAgent.lieuNaissance ? String(apiAgent.lieuNaissance) : undefined,
|
||||
ville: apiAgent.ville ? String(apiAgent.ville) : undefined,
|
||||
adresse: apiAgent.adresse ? String(apiAgent.adresse) : undefined,
|
||||
autoriserAides: Boolean(apiAgent.autoriserAides),
|
||||
phone: String(apiAgent.phone || ''),
|
||||
pin: (apiAgent as any).pin ? String((apiAgent as any).pin) : undefined,
|
||||
limiteInferieure: apiAgent.limiteInferieure,
|
||||
limiteSuperieure: apiAgent.limiteSuperieure,
|
||||
limiteParTransaction: apiAgent.limiteParTransaction,
|
||||
@@ -144,46 +152,61 @@ export class TpeService {
|
||||
limiteMaxAirtime: apiAgent.limiteMaxAirtime,
|
||||
maxPeripheriques: apiAgent.maxPeripheriques,
|
||||
limitId: apiAgent.limitId ? String(apiAgent.limitId) : undefined,
|
||||
nationalite: apiAgent.nationalite,
|
||||
cni: apiAgent.cni,
|
||||
cniDelivreeLe: apiAgent.cniDelivreeLe,
|
||||
cniDelivreeA: apiAgent.cniDelivreeA,
|
||||
residence: apiAgent.residence,
|
||||
autreAdresse1: apiAgent.autreAdresse1,
|
||||
statutMarital: apiAgent.statutMarital,
|
||||
epoux: apiAgent.epoux,
|
||||
autreTelephone: apiAgent.autreTelephone,
|
||||
createdAt: apiAgent.createdAt,
|
||||
updatedAt: apiAgent.updatedAt,
|
||||
createdBy: apiAgent.createdBy,
|
||||
nationalite: apiAgent.nationalite ? String(apiAgent.nationalite) : undefined,
|
||||
cni: apiAgent.cni ? String(apiAgent.cni) : undefined,
|
||||
cniDelivreeLe: apiAgent.cniDelivreeLe ? String(apiAgent.cniDelivreeLe) : undefined,
|
||||
cniDelivreeA: apiAgent.cniDelivreeA ? String(apiAgent.cniDelivreeA) : undefined,
|
||||
residence: apiAgent.residence ? String(apiAgent.residence) : undefined,
|
||||
autreAdresse1: apiAgent.autreAdresse1 ? String(apiAgent.autreAdresse1) : undefined,
|
||||
statutMarital: apiAgent.statutMarital ? String(apiAgent.statutMarital) : undefined,
|
||||
epoux: apiAgent.epoux ? String(apiAgent.epoux) : undefined,
|
||||
autreTelephone: apiAgent.autreTelephone ? String(apiAgent.autreTelephone) : undefined,
|
||||
createdAt: apiAgent.createdAt ? String(apiAgent.createdAt) : undefined,
|
||||
updatedAt: apiAgent.updatedAt ? String(apiAgent.updatedAt) : undefined,
|
||||
createdBy: apiAgent.createdBy ? String(apiAgent.createdBy) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Transform API response to TpeDevice
|
||||
private transformTpe(apiTpe: TpeApiResponse): TpeDevice {
|
||||
// Map API-specific names to our generic interface where possible
|
||||
const serial = (apiTpe as any).numeroSerie || (apiTpe as any).serial || '';
|
||||
const imei = (apiTpe as any).imei || serial || '';
|
||||
const typeRaw = String((apiTpe as any).typeTerminal || '').toUpperCase();
|
||||
const type = typeRaw.includes('POS') ? ('POS' as TpeType) : ('OTHER' as TpeType);
|
||||
const marque = (apiTpe as any).plateforme || (apiTpe as any).marque || '';
|
||||
const modele = (apiTpe as any).modeleAppareil || (apiTpe as any).modele || '';
|
||||
const statut = this.transformStatut(String(apiTpe.statut || 'INVALIDE'));
|
||||
// Agent mapping: sometimes API returns an agent object or only an id
|
||||
let agent: Agent | undefined = undefined;
|
||||
if ((apiTpe as any).agent && typeof (apiTpe as any).agent === 'object' && (apiTpe as any).agent.id) {
|
||||
agent = this.transformAgent((apiTpe as any).agent as AgentApiResponse);
|
||||
}
|
||||
const assigne = Boolean((apiTpe as any).agentConnecteId || (apiTpe as any).assigne);
|
||||
|
||||
return {
|
||||
id: String(apiTpe.id),
|
||||
imei: apiTpe.imei,
|
||||
serial: apiTpe.serial,
|
||||
type: apiTpe.type as TpeType,
|
||||
marque: apiTpe.marque,
|
||||
modele: apiTpe.modele,
|
||||
statut: this.transformStatut(apiTpe.statut),
|
||||
agent: apiTpe.agent ? this.transformAgent(apiTpe.agent) : undefined,
|
||||
assigne: apiTpe.assigne,
|
||||
createdAt: apiTpe.createdAt,
|
||||
updatedAt: apiTpe.updatedAt,
|
||||
imei: String(imei),
|
||||
serial: String(serial),
|
||||
type,
|
||||
marque: String(marque),
|
||||
modele: String(modele),
|
||||
statut,
|
||||
agent,
|
||||
assigne,
|
||||
createdAt: (apiTpe as any).createdAt,
|
||||
updatedAt: (apiTpe as any).updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
// Transform TpeDevice to API payload
|
||||
// Transform TpeDevice to API payload (best-effort)
|
||||
private transformToApiPayload(tpe: Partial<TpeDevice>): any {
|
||||
const payload: any = {};
|
||||
if (tpe.imei !== undefined) payload.imei = tpe.imei;
|
||||
if (tpe.serial !== undefined) payload.serial = tpe.serial;
|
||||
if (tpe.type !== undefined) payload.type = tpe.type;
|
||||
if (tpe.marque !== undefined) payload.marque = tpe.marque;
|
||||
if (tpe.modele !== undefined) payload.modele = tpe.modele;
|
||||
if (tpe.imei !== undefined) payload.numeroSerie = tpe.imei;
|
||||
if (tpe.serial !== undefined) payload.numeroSerie = tpe.serial;
|
||||
if (tpe.type !== undefined) payload.typeTerminal = tpe.type;
|
||||
if (tpe.marque !== undefined) payload.plateforme = tpe.marque;
|
||||
if (tpe.modele !== undefined) payload.modeleAppareil = tpe.modele;
|
||||
if (tpe.statut !== undefined) payload.statut = this.transformStatutToApi(tpe.statut);
|
||||
if (tpe.assigne !== undefined) payload.assigne = tpe.assigne;
|
||||
return payload;
|
||||
@@ -192,10 +215,8 @@ export class TpeService {
|
||||
// GET /api/v1/tpes/{id} - Get by ID
|
||||
getById(id: string): Observable<TpeDevice | undefined> {
|
||||
if (USE_SERVER) {
|
||||
return this.http
|
||||
.get<TpeApiResponse>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() })
|
||||
.pipe(
|
||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
||||
return this.http.get<TpeApiResponse>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }).pipe(
|
||||
map((api) => this.transformTpe(api)),
|
||||
catchError((err) => {
|
||||
console.error(`Error fetching TPE ${id}:`, err);
|
||||
return of(undefined);
|
||||
@@ -205,9 +226,9 @@ export class TpeService {
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
|
||||
// GET /api/v1/tpes - List all
|
||||
list(params?: ListParams): Observable<PagedResult<TpeDevice>> {
|
||||
if (USE_SERVER) {
|
||||
let httpParams = new HttpParams();
|
||||
if (params) {
|
||||
if (params.page) httpParams = httpParams.set('page', params.page.toString());
|
||||
@@ -218,40 +239,24 @@ export class TpeService {
|
||||
}
|
||||
|
||||
return this.http
|
||||
.get<TpeApiResponse[]>(this.apiUrl, {
|
||||
.get<PagedResult<TpeApiResponse>>(this.apiUrl, {
|
||||
params: httpParams,
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((list) => {
|
||||
const tpes = list.map((apiTpe) => this.transformTpe(apiTpe));
|
||||
// If pagination params provided, return paginated result
|
||||
if (params) {
|
||||
return normalizePage<TpeDevice>(
|
||||
{ data: tpes, meta: { total: tpes.length } },
|
||||
params.page || 1,
|
||||
params.size || 10
|
||||
);
|
||||
}
|
||||
// Otherwise return all as single page
|
||||
return normalizePage<TpeDevice>(
|
||||
{ data: tpes, meta: { total: tpes.length } },
|
||||
1,
|
||||
tpes.length
|
||||
);
|
||||
const content = (list.content || []).map((api) => this.transformTpe(api));
|
||||
return { ...list, content } as PagedResult<TpeDevice>;
|
||||
}),
|
||||
catchError((err) => {
|
||||
console.error('Error fetching TPEs:', err);
|
||||
return of(normalizePage<TpeDevice>({ data: [], meta: { total: 0 } }, 1, 10));
|
||||
return of(normalizePage<TpeDevice>({ content: [], meta: { total: 0 } }, 1, 10));
|
||||
})
|
||||
);
|
||||
}
|
||||
return of(normalizePage<TpeDevice>({ data: [], meta: { total: 0 } }, 1, 10));
|
||||
}
|
||||
|
||||
// POST /api/v1/tpes - Create
|
||||
create(payload: Omit<TpeDevice, 'id' | 'createdAt' | 'updatedAt'>): Observable<TpeDevice> {
|
||||
if (USE_SERVER) {
|
||||
create(payload: Partial<TpeDevice>): Observable<TpeDevice> {
|
||||
const apiPayload = this.transformToApiPayload(payload);
|
||||
return this.http
|
||||
.post<TpeApiResponse>(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() })
|
||||
@@ -263,12 +268,9 @@ export class TpeService {
|
||||
})
|
||||
);
|
||||
}
|
||||
throw new Error('Server mode is required');
|
||||
}
|
||||
|
||||
// PUT /api/v1/tpes/{id} - Update
|
||||
update(id: string, payload: Partial<TpeDevice>): Observable<TpeDevice | undefined> {
|
||||
if (USE_SERVER) {
|
||||
const apiPayload = this.transformToApiPayload(payload);
|
||||
return this.http
|
||||
.put<TpeApiResponse>(`${this.apiUrl}/${id}`, apiPayload, {
|
||||
@@ -282,8 +284,6 @@ export class TpeService {
|
||||
})
|
||||
);
|
||||
}
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
// DELETE /api/v1/tpes/{id} - Delete
|
||||
delete(id: string): Observable<boolean> {
|
||||
|
||||
@@ -22,9 +22,17 @@ const routes: Routes = [
|
||||
loadComponent: () => import('./pages/reunion/reunion').then((m) => m.ReunionList),
|
||||
},
|
||||
{
|
||||
path: 'rapport',
|
||||
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),
|
||||
|
||||
@@ -46,7 +46,8 @@ export class Layout {
|
||||
{ icon: '🏟️', label: 'Hippodromes', link: '/hippodromes' },
|
||||
{ icon: '📅', label: 'Reunions', link: '/reunions' },
|
||||
{ icon: '🏇', label: 'Courses', link: '/courses' },
|
||||
{ icon: 'icon-chart-bar', label: 'Rapport des courses', link: '/rapport' },
|
||||
{ icon: 'icon-chart-bar', label: 'Résultats des courses', link: '/resultat' },
|
||||
{ icon: '💰', label: 'Gains (cagnotte)', link: '/gains' },
|
||||
];
|
||||
|
||||
workspaceMenuItems: MenuItem[] = [
|
||||
|
||||
@@ -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 -->
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" size="xl" (close)="closeModal()">
|
||||
@if (modalOpen()) {
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" size="xl" (close)="closeModal()">
|
||||
<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,9 +14,9 @@ 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 { CourseApiResponse, CourseService, NonApiRequest } from 'src/app/core/services/course';
|
||||
import { ResultatService } from 'src/app/core/services/resultat';
|
||||
import { Resultat, ResultatStatut } from 'src/app/core/interfaces/resultat';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
@@ -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);
|
||||
@@ -345,8 +347,13 @@ export class Course {
|
||||
onNonPartantSave(payload: string[]) {
|
||||
const course = this.selectedCourse();
|
||||
if (!course) return;
|
||||
const reqPayload = {
|
||||
nonPartants: [
|
||||
...payload
|
||||
]
|
||||
}
|
||||
|
||||
this.api.setNonPartants(course.id, payload).subscribe({
|
||||
this.api.setNonPartants(course.id, reqPayload).subscribe({
|
||||
next: (updatedCourse) => {
|
||||
if (updatedCourse) {
|
||||
toast.success('Non-partants mis à jour avec succès');
|
||||
@@ -465,7 +472,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 +505,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({
|
||||
|
||||
16
src/app/dashboard/pages/gain-details/gain-details.css
Normal file
16
src/app/dashboard/pages/gain-details/gain-details.css
Normal 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)); }
|
||||
}
|
||||
|
||||
65
src/app/dashboard/pages/gain-details/gain-details.html
Normal file
65
src/app/dashboard/pages/gain-details/gain-details.html
Normal 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>
|
||||
}
|
||||
23
src/app/dashboard/pages/gain-details/gain-details.spec.ts
Normal file
23
src/app/dashboard/pages/gain-details/gain-details.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
37
src/app/dashboard/pages/gain-details/gain-details.ts
Normal file
37
src/app/dashboard/pages/gain-details/gain-details.ts
Normal 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']);
|
||||
}
|
||||
}
|
||||
0
src/app/dashboard/pages/gains/gains.css
Normal file
0
src/app/dashboard/pages/gains/gains.css
Normal file
34
src/app/dashboard/pages/gains/gains.html
Normal file
34
src/app/dashboard/pages/gains/gains.html
Normal 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>
|
||||
23
src/app/dashboard/pages/gains/gains.spec.ts
Normal file
23
src/app/dashboard/pages/gains/gains.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
84
src/app/dashboard/pages/gains/gains.ts
Normal file
84
src/app/dashboard/pages/gains/gains.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,66 @@ 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 = {
|
||||
id: String((row as any).courseId ?? '')
|
||||
};
|
||||
|
||||
const payload: Omit<ResultatCourse, "id"> = {
|
||||
course,
|
||||
statut: (row.statut as any) ?? (0 as any),
|
||||
ordreArrivee: String(row.ordreArrivee ?? ''),
|
||||
datePublication: row.datePublication ?? row.createdAt,
|
||||
};
|
||||
|
||||
this.depouillement.sendResultat(payload).subscribe({
|
||||
next: (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>
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
type="checkbox"
|
||||
[value]="t.value"
|
||||
(change)="onToggleType($event)"
|
||||
[checked]="form.value.typesParisOuverts?.includes(t.value)"
|
||||
[checked]="(form.get('typesParisOuverts')?.value || []).includes(t.value)"
|
||||
/>
|
||||
{{ t.label }}
|
||||
</label>
|
||||
|
||||
@@ -216,6 +216,8 @@ export class CourseForm implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.selectedHippodromeLabel.set('');
|
||||
this.form.markAsPristine();
|
||||
this.form.markAsUntouched();
|
||||
// Ensure UI updates for cleared form
|
||||
this.cdr.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -246,6 +248,9 @@ export class CourseForm implements OnInit, AfterViewInit, OnDestroy {
|
||||
{ emitEvent: false }
|
||||
);
|
||||
|
||||
// Ensure view updates when hydrating values (OnPush component)
|
||||
this.cdr.markForCheck();
|
||||
|
||||
// Set hippodrome label if available
|
||||
if (hippodromeId && this.hippodromes().length > 0) {
|
||||
const h = this.hippodromes().find((r) => String(r.id) === hippodromeId);
|
||||
@@ -274,6 +279,9 @@ export class CourseForm implements OnInit, AfterViewInit, OnDestroy {
|
||||
? [...current, value]
|
||||
: current.filter((v: string) => v !== value)
|
||||
});
|
||||
|
||||
// Trigger change detection so checkbox states update in OnPush mode
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
@@ -292,7 +300,6 @@ onSubmit() {
|
||||
const foundHippodrome = this.hippodromes().find(h => String(h.id) === String(hippodromeId));
|
||||
const hippodromeObj = foundHippodrome ?? (hippodromeId ? { id: +hippodromeId } : undefined);
|
||||
|
||||
|
||||
// 2️⃣ Transformer typesParisOuverts CSV → tablea
|
||||
|
||||
// 3️⃣ Construire payload
|
||||
@@ -318,6 +325,8 @@ onSubmit() {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// Persist: create or update via CourseService, then emit the saved Course
|
||||
if (this.value && this.value.id) {
|
||||
this.courseServive.update(this.value.id, payload).subscribe({
|
||||
@@ -325,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({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ZardFormModule } from '@shared/components/form/form.module';
|
||||
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
||||
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
||||
import { Course, CourseType } from 'src/app/core/interfaces/course';
|
||||
import { Resultat } from 'src/app/core/interfaces/resultat';
|
||||
import { Resultat, ResultatStatut } from 'src/app/core/interfaces/resultat';
|
||||
|
||||
type PlaceRow = { picks: FormArray<FormControl<number | null>> };
|
||||
type ResultatShape = { places: FormArray<FormGroup<PlaceRow>> };
|
||||
@@ -61,16 +61,16 @@ export class ResultatForm {
|
||||
maxNum = computed(() => this.course?.nombrePartants ?? 0);
|
||||
// Ensure non-partants are compared as strings to avoid type mismatches
|
||||
npSet = computed(() => new Set((this.course?.nonPartants ?? []).map((v) => String(v))));
|
||||
statut = computed((): 'PROVISOIRE' | 'OFFICIEL' | 'ANNULE' | 'EN_ATTENTE' => {
|
||||
return this.resultat ? 'PROVISOIRE' : 'EN_ATTENTE';
|
||||
statut = computed((): ResultatStatut => {
|
||||
return this.resultat ? this.resultat.statut : ResultatStatut.EN_ATTENTE;
|
||||
});
|
||||
|
||||
canValidate(): boolean {
|
||||
return this.statut() === 'EN_ATTENTE';
|
||||
return String(this.statut()) === "EN_ATTENTE";
|
||||
}
|
||||
|
||||
canConfirm(): boolean {
|
||||
return this.statut() === 'PROVISOIRE';
|
||||
return String(this.statut()) === "PROVISOIRE";
|
||||
}
|
||||
|
||||
// Helper methods for template
|
||||
|
||||
@@ -25,6 +25,24 @@
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Statut</label>
|
||||
<div z-form-control>
|
||||
<z-select formControlName="statut" [zPlaceholder]="'Sélectionner...'">
|
||||
@for (s of allStatuses; track s) {
|
||||
<z-select-item [zValue]="s">{{ s }}</z-select-item>
|
||||
}
|
||||
</z-select>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Assigné</label>
|
||||
<div z-form-control>
|
||||
<input type="checkbox" formControlName="assigne" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Marque</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('marque') || ''">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ZardFormModule } from '@shared/components/form/form.module';
|
||||
import { ZardInputDirective } from '@shared/components/input/input.directive';
|
||||
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
||||
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
||||
import { TpeDevice, TpeType } from 'src/app/core/interfaces/tpe';
|
||||
import { TpeDevice, TpeStatus, TpeType } from 'src/app/core/interfaces/tpe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tpe-form',
|
||||
@@ -47,6 +47,8 @@ export class TpeForm {
|
||||
type: ['POS' as TpeType, Validators.required],
|
||||
marque: ['', Validators.required],
|
||||
modele: ['', Validators.required],
|
||||
statut: ['VALIDE' as TpeStatus, Validators.required],
|
||||
assigne: [false],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,6 +70,8 @@ export class TpeForm {
|
||||
imei: '',
|
||||
serial: '',
|
||||
type: 'POS',
|
||||
statut: 'VALIDE',
|
||||
assigne: false,
|
||||
marque: '',
|
||||
modele: '',
|
||||
});
|
||||
@@ -78,6 +82,8 @@ export class TpeForm {
|
||||
imei: v.imei,
|
||||
serial: v.serial,
|
||||
type: v.type,
|
||||
statut: v.statut,
|
||||
assigne: !!v.assigne,
|
||||
marque: v.marque,
|
||||
modele: v.modele,
|
||||
});
|
||||
@@ -94,6 +100,8 @@ export class TpeForm {
|
||||
imei: raw.imei,
|
||||
serial: raw.serial,
|
||||
type: raw.type,
|
||||
statut: raw.statut,
|
||||
assigne: !!raw.assigne,
|
||||
marque: raw.marque,
|
||||
modele: raw.modele,
|
||||
};
|
||||
@@ -122,4 +130,16 @@ export class TpeForm {
|
||||
{ label: 'POS', value: 'POS' as TpeType },
|
||||
{ label: 'Autre', value: 'OTHER' as TpeType },
|
||||
];
|
||||
|
||||
allStatuses: TpeStatus[] = [
|
||||
'VALIDE',
|
||||
'INVALIDE',
|
||||
'EN_PANNE',
|
||||
'BLOQUE',
|
||||
'DISPONIBLE',
|
||||
'AFFECTE',
|
||||
'EN_MAINTENANCE',
|
||||
'HORS_SERVICE',
|
||||
'VOLE',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://192.168.1.235:8280',
|
||||
apiBaseUrl: 'http://192.168.1.235:8381',
|
||||
depouillementBaseUrl: 'http://192.168.1.235:8383'
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://192.168.1.235:8280',
|
||||
apiBaseUrl: 'http://192.168.1.235:8381',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user