agent and tpe save done
This commit is contained in:
@@ -48,7 +48,7 @@ export interface Agent {
|
||||
autreTelephone?: string;
|
||||
|
||||
// TPE assignés (actifs seulement)
|
||||
tpes?: TpeDevice[];
|
||||
tpes?: TpeDevice[] | TpeDevice;
|
||||
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
|
||||
12
src/app/core/interfaces/points-ventes.ts
Normal file
12
src/app/core/interfaces/points-ventes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type PointVenteStatut = 'ACTIF' | 'INACTIF';
|
||||
|
||||
export interface PointVente {
|
||||
id: string;
|
||||
code: string;
|
||||
nom: string;
|
||||
adresse: string;
|
||||
ville: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
statut: PointVenteStatut;
|
||||
}
|
||||
@@ -1,27 +1,23 @@
|
||||
import { Agent } from './agent';
|
||||
|
||||
export type TpeStatus =
|
||||
| 'VALIDE'
|
||||
| 'INVALIDE'
|
||||
| 'EN_PANNE'
|
||||
| 'BLOQUE'
|
||||
| 'DISPONIBLE'
|
||||
| 'AFFECTE'
|
||||
| 'EN_MAINTENANCE'
|
||||
| 'HORS_SERVICE'
|
||||
| 'VOLE';
|
||||
| 'ACTIF'
|
||||
| 'HORS_SERVICE';
|
||||
export type TpeType = 'POS' | 'OTHER';
|
||||
|
||||
export interface TpeDevice {
|
||||
id: string;
|
||||
imei: string;
|
||||
serial: string;
|
||||
type: TpeType;
|
||||
marque: string;
|
||||
modele: string;
|
||||
numeroSerie: string;
|
||||
pointDeVenteId: string;
|
||||
statut: TpeStatus;
|
||||
agent?: Agent;
|
||||
assigne: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
versionLogicielle: string;
|
||||
typeTerminal: string;
|
||||
plateforme: string;
|
||||
modeleAppareil: string;
|
||||
systemeExploitation: string;
|
||||
versionOs: string;
|
||||
adresseIp: string;
|
||||
adresseMac: string;
|
||||
agentConnecteId: string;
|
||||
derniereConnexionAgent: string;
|
||||
derniereDeconnexionAgent: string;
|
||||
journalSession: string;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ interface AgentApiResponse {
|
||||
statutMarital?: string;
|
||||
epoux?: string;
|
||||
autreTelephone?: string;
|
||||
tpes?: TpeApiResponse[];
|
||||
tpes?: TpeDevice;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
createdBy?: string;
|
||||
@@ -88,44 +88,37 @@ export class AgentService {
|
||||
}
|
||||
|
||||
// Transform API TPE response to TpeDevice
|
||||
private transformTpe(apiTpe: TpeApiResponse): TpeDevice {
|
||||
const transformStatut = (apiStatut: string): TpeStatus => {
|
||||
const upperStatut = apiStatut.toUpperCase() as TpeStatus;
|
||||
const validStatuses: TpeStatus[] = [
|
||||
'VALIDE',
|
||||
'INVALIDE',
|
||||
'EN_PANNE',
|
||||
'BLOQUE',
|
||||
'DISPONIBLE',
|
||||
'AFFECTE',
|
||||
'EN_MAINTENANCE',
|
||||
'HORS_SERVICE',
|
||||
'VOLE',
|
||||
];
|
||||
return validStatuses.includes(upperStatut) ? upperStatut : 'INVALIDE';
|
||||
};
|
||||
// private transformTpe(apiTpe: TpeApiResponse): TpeDevice {
|
||||
// const transformStatut = (apiStatut: string): TpeStatus => {
|
||||
// const upperStatut = apiStatut.toUpperCase() as TpeStatus;
|
||||
// const validStatuses: TpeStatus[] = [
|
||||
// 'ACTIF',
|
||||
// 'HORS_SERVICE'
|
||||
// ];
|
||||
// return validStatuses.includes(upperStatut) ? upperStatut : 'INVALIDE';
|
||||
// };
|
||||
|
||||
// Transform agent if it's an object (not just a string reference)
|
||||
let transformedAgent: Agent | undefined = undefined;
|
||||
if (apiTpe.agent && typeof apiTpe.agent === 'object' && apiTpe.agent.id) {
|
||||
// If agent is a full object, transform it
|
||||
transformedAgent = this.transformAgent(apiTpe.agent as any);
|
||||
}
|
||||
// // Transform agent if it's an object (not just a string reference)
|
||||
// let transformedAgent: Agent | undefined = undefined;
|
||||
// if (apiTpe.agent && typeof apiTpe.agent === 'object' && apiTpe.agent.id) {
|
||||
// // If agent is a full object, transform it
|
||||
// transformedAgent = this.transformAgent(apiTpe.agent as any);
|
||||
// }
|
||||
|
||||
return {
|
||||
id: String(apiTpe.id),
|
||||
imei: apiTpe.imei,
|
||||
serial: apiTpe.serial,
|
||||
type: apiTpe.type as TpeType,
|
||||
marque: apiTpe.marque,
|
||||
modele: apiTpe.modele,
|
||||
statut: transformStatut(apiTpe.statut),
|
||||
agent: transformedAgent,
|
||||
assigne: apiTpe.assigne,
|
||||
createdAt: apiTpe.createdAt,
|
||||
updatedAt: apiTpe.updatedAt,
|
||||
};
|
||||
}
|
||||
// return {
|
||||
// id: String(apiTpe.id),
|
||||
// imei: apiTpe.imei,
|
||||
// serial: apiTpe.serial,
|
||||
// type: apiTpe.type as TpeType,
|
||||
// marque: apiTpe.marque,
|
||||
// modele: apiTpe.modele,
|
||||
// statut: transformStatut(apiTpe.statut),
|
||||
// agent: transformedAgent,
|
||||
// assigne: apiTpe.assigne,
|
||||
// createdAt: apiTpe.createdAt,
|
||||
// updatedAt: apiTpe.updatedAt,
|
||||
// };
|
||||
// }
|
||||
|
||||
// Transform API response to Agent
|
||||
private transformAgent(apiAgent: AgentApiResponse): Agent {
|
||||
@@ -166,10 +159,7 @@ export class AgentService {
|
||||
statutMarital: apiAgent.statutMarital,
|
||||
epoux: apiAgent.epoux,
|
||||
autreTelephone: apiAgent.autreTelephone,
|
||||
tpes: apiAgent.tpes?.map((tpe) => {
|
||||
const transformed = this.transformTpe(tpe);
|
||||
return transformed;
|
||||
}),
|
||||
tpes: apiAgent.tpes ,
|
||||
createdAt: apiAgent.createdAt,
|
||||
updatedAt: apiAgent.updatedAt,
|
||||
createdBy: apiAgent.createdBy,
|
||||
|
||||
16
src/app/core/services/points-vente.spec.ts
Normal file
16
src/app/core/services/points-vente.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PointsVente } from './points-vente';
|
||||
|
||||
describe('PointsVente', () => {
|
||||
let service: PointsVente;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(PointsVente);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
92
src/app/core/services/points-vente.ts
Normal file
92
src/app/core/services/points-vente.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
import { PointVente, PointVenteStatut } from 'src/app/core/interfaces/points-ventes';
|
||||
import { environment } from 'src/environments/environment.development';
|
||||
import { ServicesUtils } from './services-utils';
|
||||
|
||||
const API_BASE = '/api/points-de-vente';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PointsVenteService {
|
||||
private apiUrl = environment.apiBaseUrl + API_BASE;
|
||||
|
||||
constructor(private http: HttpClient, private servicesUtils: 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(params: ListParams): Observable<PagedResult<PointVente>> {
|
||||
return this.http
|
||||
.get<PagedResult<PointVente>>(this.apiUrl, {
|
||||
params: this.servicesUtils.getParamsFromModel(params),
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
console.error('Error fetching points de vente:', err);
|
||||
return of({
|
||||
content: [],
|
||||
pageable: { pageNumber: 0, pageSize: params.size || 10, total: 0 },
|
||||
totalPages: 0,
|
||||
totalElements: 0,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getById(id: string): Observable<PointVente | undefined> {
|
||||
return this.http
|
||||
.get<PointVente>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() })
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
console.error(`Error fetching point de vente ${id}:`, err);
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
create(pointVente: Omit<PointVente, 'id'>): Observable<PointVente> {
|
||||
return this.http
|
||||
.post<PointVente>(this.apiUrl, pointVente, { headers: this.getNgrokHeaders() })
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
console.log(err)
|
||||
console.error('Error creating point de vente:', err);
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
update(id: string, payload: Partial<PointVente>): Observable<PointVente | undefined> {
|
||||
return this.http
|
||||
.put<PointVente>(`${this.apiUrl}/${id}`, payload, { headers: this.getNgrokHeaders() })
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
console.error(`Error updating point de vente ${id}:`, err);
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
delete(id: string): Observable<boolean> {
|
||||
return this.http
|
||||
.delete<void>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() })
|
||||
.pipe(
|
||||
map(() => true),
|
||||
catchError((err) => {
|
||||
console.error(`Error deleting point de vente ${id}:`, err);
|
||||
return of(false);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -104,17 +104,10 @@ export class TpeService {
|
||||
private transformStatut(apiStatut: string): TpeStatus {
|
||||
const upperStatut = apiStatut.toUpperCase() as TpeStatus;
|
||||
const validStatuses: TpeStatus[] = [
|
||||
'VALIDE',
|
||||
'INVALIDE',
|
||||
'EN_PANNE',
|
||||
'BLOQUE',
|
||||
'DISPONIBLE',
|
||||
'AFFECTE',
|
||||
'EN_MAINTENANCE',
|
||||
'HORS_SERVICE',
|
||||
'VOLE',
|
||||
'ACTIF',
|
||||
'HORS_SERVICE'
|
||||
];
|
||||
return validStatuses.includes(upperStatut) ? upperStatut : 'INVALIDE';
|
||||
return validStatuses.includes(upperStatut) ? upperStatut : 'HORS_SERVICE';
|
||||
}
|
||||
|
||||
// Transform interface statut to API statut (both use uppercase now, so direct return)
|
||||
@@ -168,55 +161,55 @@ export class TpeService {
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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: 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,
|
||||
};
|
||||
}
|
||||
// return {
|
||||
// id: String(apiTpe.id),
|
||||
// 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 (best-effort)
|
||||
private transformToApiPayload(tpe: Partial<TpeDevice>): any {
|
||||
const payload: any = {};
|
||||
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;
|
||||
}
|
||||
// private transformToApiPayload(tpe: Partial<TpeDevice>): any {
|
||||
// const payload: any = {};
|
||||
// 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;
|
||||
// }
|
||||
|
||||
// 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((api) => this.transformTpe(api)),
|
||||
return this.http.get<TpeDevice>(`${this.apiUrl}/${id}`, { headers: this.getNgrokHeaders() }).pipe(
|
||||
map((api) => api),
|
||||
catchError((err) => {
|
||||
console.error(`Error fetching TPE ${id}:`, err);
|
||||
return of(undefined);
|
||||
@@ -239,13 +232,13 @@ getById(id: string): Observable<TpeDevice | undefined> {
|
||||
}
|
||||
|
||||
return this.http
|
||||
.get<PagedResult<TpeApiResponse>>(this.apiUrl, {
|
||||
.get<PagedResult<TpeDevice>>(this.apiUrl, {
|
||||
params: httpParams,
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((list) => {
|
||||
const content = (list.content || []).map((api) => this.transformTpe(api));
|
||||
const content = (list.content || []).map((api) => api);
|
||||
return { ...list, content } as PagedResult<TpeDevice>;
|
||||
}),
|
||||
catchError((err) => {
|
||||
@@ -257,11 +250,10 @@ getById(id: string): Observable<TpeDevice | undefined> {
|
||||
|
||||
// POST /api/v1/tpes - Create
|
||||
create(payload: Partial<TpeDevice>): Observable<TpeDevice> {
|
||||
const apiPayload = this.transformToApiPayload(payload);
|
||||
return this.http
|
||||
.post<TpeApiResponse>(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() })
|
||||
.post<TpeDevice>(this.apiUrl, payload, { headers: this.getNgrokHeaders() })
|
||||
.pipe(
|
||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
||||
map((apiTpe) => apiTpe),
|
||||
catchError((err) => {
|
||||
console.error('Error creating TPE:', err);
|
||||
throw err;
|
||||
@@ -271,13 +263,12 @@ getById(id: string): Observable<TpeDevice | undefined> {
|
||||
|
||||
// PUT /api/v1/tpes/{id} - Update
|
||||
update(id: string, payload: Partial<TpeDevice>): Observable<TpeDevice | undefined> {
|
||||
const apiPayload = this.transformToApiPayload(payload);
|
||||
return this.http
|
||||
.put<TpeApiResponse>(`${this.apiUrl}/${id}`, apiPayload, {
|
||||
.put<TpeDevice>(`${this.apiUrl}/${id}`, payload, {
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
||||
map((apiTpe) => apiTpe),
|
||||
catchError((err) => {
|
||||
console.error(`Error updating TPE ${id}:`, err);
|
||||
return of(undefined);
|
||||
@@ -305,13 +296,13 @@ update(id: string, payload: Partial<TpeDevice>): Observable<TpeDevice | undefine
|
||||
updateStatut(id: string, statut: TpeStatus): Observable<TpeDevice | undefined> {
|
||||
if (USE_SERVER) {
|
||||
return this.http
|
||||
.patch<TpeApiResponse>(
|
||||
.patch<TpeDevice>(
|
||||
`${this.apiUrl}/${id}/statut`,
|
||||
{ statut: this.transformStatutToApi(statut) },
|
||||
{ headers: this.getNgrokHeaders() }
|
||||
)
|
||||
.pipe(
|
||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
||||
map((apiTpe) => apiTpe),
|
||||
catchError((err) => {
|
||||
console.error(`Error updating TPE statut ${id}:`, err);
|
||||
return of(undefined);
|
||||
@@ -323,77 +314,71 @@ update(id: string, payload: Partial<TpeDevice>): Observable<TpeDevice | undefine
|
||||
|
||||
// PATCH /api/v1/tpes/{id}/liberer - Liberate TPE (updates whole TPE, sets assigne to false and statut to DISPONIBLE)
|
||||
liberer(id: string): Observable<TpeDevice | undefined> {
|
||||
if (USE_SERVER) {
|
||||
// First get the current TPE data
|
||||
return this.getById(id).pipe(
|
||||
switchMap((tpe) => {
|
||||
if (!tpe) {
|
||||
return of(undefined);
|
||||
}
|
||||
// Update the whole TPE with assigne set to false and statut to DISPONIBLE
|
||||
const updatedTpe = { ...tpe, assigne: false, statut: 'DISPONIBLE' as TpeStatus };
|
||||
const apiPayload = this.transformToApiPayload(updatedTpe);
|
||||
return this.http
|
||||
.patch<TpeApiResponse>(`${this.apiUrl}/liberer/${id}`, apiPayload, {
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
||||
catchError((err) => {
|
||||
console.error(`Error liberating TPE ${id}:`, err);
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError((err) => {
|
||||
console.error(`Error fetching TPE ${id} for liberation:`, err);
|
||||
return this.getById(id).pipe(
|
||||
switchMap((tpe) => {
|
||||
if (!tpe) {
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
}
|
||||
return of(undefined);
|
||||
}
|
||||
// Update the whole TPE with assigne set to false and statut to DISPONIBLE
|
||||
const updatedTpe = {
|
||||
...tpe,
|
||||
statut: 'ACTIF'
|
||||
};
|
||||
return this.http
|
||||
.patch<TpeDevice>(`${this.apiUrl}/liberer/${id}`, updatedTpe, {
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((apiTpe) => apiTpe),
|
||||
catchError((err) => {
|
||||
console.error(`Error liberating TPE ${id}:`, err);
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError((err) => {
|
||||
console.error(`Error fetching TPE ${id} for liberation:`, err);
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// PATCH /api/v1/tpes/assigner - Assign TPE
|
||||
// Payload: { tpeId: number, agentId: number }
|
||||
assigner(id: string, agentId: string): Observable<TpeDevice | undefined> {
|
||||
if (USE_SERVER) {
|
||||
const payload = {
|
||||
tpeId: Number(id),
|
||||
agentId: Number(agentId),
|
||||
};
|
||||
return this.http
|
||||
.patch<TpeApiResponse>(`${this.apiUrl}/assigner`, payload, {
|
||||
headers: this.getNgrokHeaders(),
|
||||
const payload = {
|
||||
tpeId: Number(id),
|
||||
agentId: Number(agentId),
|
||||
};
|
||||
return this.http
|
||||
.patch<TpeDevice>(`${this.apiUrl}/assigner`, payload, {
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((apiTpe) => apiTpe),
|
||||
catchError((err) => {
|
||||
console.error(`Error assigning TPE ${id}:`, err);
|
||||
return of(undefined);
|
||||
})
|
||||
.pipe(
|
||||
map((apiTpe) => this.transformTpe(apiTpe)),
|
||||
catchError((err) => {
|
||||
console.error(`Error assigning TPE ${id}:`, err);
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
}
|
||||
return of(undefined);
|
||||
);
|
||||
}
|
||||
|
||||
// GET /api/v1/tpes/statut/{statut} - List by statut
|
||||
getByStatut(statut: TpeStatus): Observable<TpeDevice[]> {
|
||||
if (USE_SERVER) {
|
||||
const apiStatut = this.transformStatutToApi(statut);
|
||||
return this.http
|
||||
.get<TpeApiResponse[]>(`${this.apiUrl}/statut/${apiStatut}`, {
|
||||
headers: this.getNgrokHeaders(),
|
||||
const apiStatut = this.transformStatutToApi(statut);
|
||||
return this.http
|
||||
.get<TpeDevice[]>(`${this.apiUrl}/statut/${apiStatut}`, {
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((list) => list.map((apiTpe) => apiTpe)),
|
||||
catchError((err) => {
|
||||
console.error(`Error fetching TPEs by statut ${statut}:`, err);
|
||||
return of([]);
|
||||
})
|
||||
.pipe(
|
||||
map((list) => list.map((apiTpe) => this.transformTpe(apiTpe))),
|
||||
catchError((err) => {
|
||||
console.error(`Error fetching TPEs by statut ${statut}:`, err);
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}
|
||||
return of([]);
|
||||
);
|
||||
}
|
||||
|
||||
// GET /api/v1/tpes/stats/count-by-statut - Get count by statut
|
||||
@@ -432,36 +417,30 @@ update(id: string, payload: Partial<TpeDevice>): Observable<TpeDevice | undefine
|
||||
|
||||
// GET /api/v1/tpes/search - Search
|
||||
search(query: string): Observable<TpeDevice[]> {
|
||||
if (USE_SERVER) {
|
||||
return this.http
|
||||
.get<TpeApiResponse[]>(`${this.apiUrl}/search`, {
|
||||
params: { q: query.trim() },
|
||||
headers: this.getNgrokHeaders(),
|
||||
return this.http
|
||||
.get<TpeDevice[]>(`${this.apiUrl}/search`, {
|
||||
params: { q: query.trim() },
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((list) => list.map((apiTpe) => apiTpe)),
|
||||
catchError((err) => {
|
||||
console.error(`Error searching TPEs with query ${query}:`, err);
|
||||
return of([]);
|
||||
})
|
||||
.pipe(
|
||||
map((list) => list.map((apiTpe) => this.transformTpe(apiTpe))),
|
||||
catchError((err) => {
|
||||
console.error(`Error searching TPEs with query ${query}:`, err);
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}
|
||||
return of([]);
|
||||
);
|
||||
}
|
||||
|
||||
// GET /api/v1/tpes/disponibles - List available TPEs
|
||||
getDisponibles(): Observable<TpeDevice[]> {
|
||||
if (USE_SERVER) {
|
||||
return this.http
|
||||
.get<TpeApiResponse[]>(`${this.apiUrl}/disponibles`, { headers: this.getNgrokHeaders() })
|
||||
.pipe(
|
||||
map((list) => list.map((apiTpe) => this.transformTpe(apiTpe))),
|
||||
catchError((err) => {
|
||||
console.error('Error fetching available TPEs:', err);
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}
|
||||
return of([]);
|
||||
return this.http
|
||||
.get<TpeDevice[]>(`${this.apiUrl}/disponibles`, { headers: this.getNgrokHeaders() })
|
||||
.pipe(
|
||||
map((list) => list.map((apiTpe) => apiTpe)),
|
||||
catchError((err) => {
|
||||
console.error('Error fetching available TPEs:', err);
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ const routes: Routes = [
|
||||
path: 'limits',
|
||||
loadComponent: () => import('./pages/limits/limits').then((m) => m.LimitsPage),
|
||||
},
|
||||
{
|
||||
path: 'points-vente',
|
||||
loadComponent: () => import('./pages/point-vente/point-vente').then((m) => m.PointVentePage),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -60,6 +60,7 @@ export class Layout {
|
||||
],
|
||||
},
|
||||
{ icon: 'icon-monitor', label: 'Gestion des TPE', link: '/tpes' },
|
||||
{ icon: 'icon-map-pin', label: 'Points de vente', link: '/points-vente' },
|
||||
{
|
||||
icon: 'icon-users',
|
||||
label: 'Utilisateurs',
|
||||
|
||||
@@ -35,16 +35,11 @@
|
||||
</div>
|
||||
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xxl">
|
||||
<app-agent-full-form
|
||||
<app-agent-form
|
||||
[value]="editingItem() ?? undefined"
|
||||
[compact]="!editingItem()"
|
||||
(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>
|
||||
</div>
|
||||
</app-modal>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
@@ -319,7 +314,7 @@
|
||||
@for (tpe of getAgentTpes(agent.id); track tpe.id) {
|
||||
<div class="px-3 py-2.5 rounded bg-primary/10 border border-primary/20">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="font-medium text-sm">{{ tpe.imei }}</div>
|
||||
<div class="font-medium text-sm">{{ tpe.numeroSerie }}</div>
|
||||
@if (tpe.statut) {
|
||||
<span class="text-xs px-2 py-0.5 rounded bg-surface text-muted-foreground">
|
||||
{{ formatTpeStatut(tpe.statut) }}
|
||||
@@ -327,19 +322,19 @@
|
||||
}
|
||||
</div>
|
||||
<div class="space-y-1 text-xs text-muted-foreground">
|
||||
@if (tpe.marque || tpe.modele) {
|
||||
@if (tpe.modeleAppareil) {
|
||||
<div>
|
||||
<span class="font-medium">Modèle:</span> {{ tpe.marque }} {{ tpe.modele }}
|
||||
<span class="font-medium">Modèle:</span> {{ tpe.modeleAppareil }}
|
||||
</div>
|
||||
}
|
||||
@if (tpe.serial) {
|
||||
@if (tpe.numeroSerie) {
|
||||
<div>
|
||||
<span class="font-medium">Série:</span> {{ tpe.serial }}
|
||||
<span class="font-medium">Série:</span> {{ tpe.numeroSerie }}
|
||||
</div>
|
||||
}
|
||||
@if (tpe.type) {
|
||||
@if (tpe.typeTerminal) {
|
||||
<div>
|
||||
<span class="font-medium">Type:</span> {{ tpe.type }}
|
||||
<span class="font-medium">Type:</span> {{ tpe.typeTerminal }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -385,9 +380,9 @@
|
||||
>
|
||||
@for (tpe of availableTpes(); track tpe.id) {
|
||||
<z-select-item [zValue]="tpe.id">
|
||||
{{ tpe.imei }} - {{ tpe.marque }} {{ tpe.modele }}
|
||||
@if (tpe.statut === 'VALIDE') {
|
||||
<span class="text-xs text-green-600 dark:text-green-400 ml-2">(Valide)</span>
|
||||
{{ tpe.numeroSerie }} - {{ tpe.modeleAppareil }}
|
||||
@if (tpe.statut === 'ACTIF') {
|
||||
<span class="text-xs text-green-600 dark:text-green-400 ml-2">(Actif)</span>
|
||||
}
|
||||
</z-select-item>
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { AgentFullForm } from '@shared/forms/agent-full-form/agent-full-form';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { switchMap, catchError } from 'rxjs/operators';
|
||||
import { toast } from 'ngx-sonner';
|
||||
import { AgentForm } from '@shared/forms/agent-form/agent-form';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -43,7 +44,7 @@ import { toast } from 'ngx-sonner';
|
||||
ZardSelectComponent,
|
||||
ZardSelectItemComponent,
|
||||
ZardFormModule,
|
||||
AgentFullForm,
|
||||
AgentForm
|
||||
],
|
||||
})
|
||||
export class AgentsPage {
|
||||
@@ -111,7 +112,7 @@ export class AgentsPage {
|
||||
) {
|
||||
// Preload TPE maps for display
|
||||
this.tpeSvc
|
||||
.list({ page: 1, size: 200, search: '', sortKey: 'imei', sortDir: 'asc' } as any)
|
||||
.list({ page: 1, size: 200, search: '', sortKey: 'id', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => {
|
||||
const tpes = res.content as TpeDevice[];
|
||||
this.rebuildTpeMaps(tpes);
|
||||
@@ -138,6 +139,7 @@ export class AgentsPage {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
console.log(res.content);
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
this.loading.set(false);
|
||||
@@ -166,7 +168,7 @@ export class AgentsPage {
|
||||
this.agentTpesMap.clear();
|
||||
tpes.forEach((t) => {
|
||||
this.tpeMap.set(t.id, t);
|
||||
const agentId = t.agent?.id;
|
||||
const agentId = t.agentConnecteId;
|
||||
if (agentId) {
|
||||
const list = this.agentTpesMap.get(agentId) || [];
|
||||
list.push(t);
|
||||
@@ -417,14 +419,14 @@ export class AgentsPage {
|
||||
const agentTpeIds = new Set(currentAgentTpes.map((t) => t.id));
|
||||
|
||||
// Load available TPEs (DISPONIBLE or VALIDE status)
|
||||
forkJoin([this.tpeSvc.getByStatut('DISPONIBLE'), this.tpeSvc.getByStatut('VALIDE')]).subscribe({
|
||||
forkJoin([this.tpeSvc.getByStatut('ACTIF'), this.tpeSvc.getByStatut('ACTIF')]).subscribe({
|
||||
next: ([disponibleTpes, valideTpes]) => {
|
||||
// Combine and filter: only show TPEs that are not assigned to any agent AND not already assigned to this agent
|
||||
const allTpes = [...disponibleTpes, ...valideTpes];
|
||||
const available = allTpes.filter(
|
||||
(t) =>
|
||||
!t.assigne &&
|
||||
(t.statut === 'DISPONIBLE' || t.statut === 'VALIDE') &&
|
||||
!t.agentConnecteId &&
|
||||
(t.statut === 'ACTIF') &&
|
||||
!agentTpeIds.has(t.id)
|
||||
);
|
||||
// Remove duplicates
|
||||
|
||||
0
src/app/dashboard/pages/point-vente/point-vente.css
Normal file
0
src/app/dashboard/pages/point-vente/point-vente.css
Normal file
41
src/app/dashboard/pages/point-vente/point-vente.html
Normal file
41
src/app/dashboard/pages/point-vente/point-vente.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<div class="flex flex-col gap-2 min-h-screen">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Gestion des Points de Vente</h2>
|
||||
<z-button (click)="openCreate()">Nouveau point de vente</z-button>
|
||||
</div>
|
||||
|
||||
<app-search-bar (search)="onSearch($event)"></app-search-bar>
|
||||
|
||||
<app-data-table [data]="rows()" [columns]="cols" [sort]="sort()" (sortChange)="sort.set($event)">
|
||||
<ng-template #rowActions let-row>
|
||||
<div class="flex gap-3">
|
||||
<button z-button zType="ghost" (click)="openEdit(row)" title="Modifier">
|
||||
<i class="icon-pen"></i>
|
||||
</button>
|
||||
<button z-button zType="destructive" (click)="remove(row)" title="Supprimer">
|
||||
<i class="icon-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
|
||||
<app-paginator
|
||||
[total]="total()"
|
||||
[page]="page()"
|
||||
[perPage]="size()"
|
||||
(pageChange)="page.set($event - 1)"
|
||||
(perPageChange)="size.set($event)"
|
||||
></app-paginator>
|
||||
</div>
|
||||
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xl">
|
||||
<app-points-vente-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>
|
||||
</div>
|
||||
</app-modal>
|
||||
23
src/app/dashboard/pages/point-vente/point-vente.spec.ts
Normal file
23
src/app/dashboard/pages/point-vente/point-vente.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PointVente } from './point-vente';
|
||||
|
||||
describe('PointVente', () => {
|
||||
let component: PointVente;
|
||||
let fixture: ComponentFixture<PointVente>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PointVente]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PointVente);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
209
src/app/dashboard/pages/point-vente/point-vente.ts
Normal file
209
src/app/dashboard/pages/point-vente/point-vente.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ViewChild,
|
||||
effect,
|
||||
signal,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { DataTable, SortState, TableColumn } from '@shared/components/data-table/data-table';
|
||||
import { Paginator } from '@shared/components/paginator/paginator';
|
||||
import { SearchBar } from '@shared/components/search-bar/search-bar';
|
||||
import { Modal } from '@shared/components/modal/modal';
|
||||
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||
import { SortDir } from '@shared/paging/paging';
|
||||
import { PointVente, PointVenteStatut } from 'src/app/core/interfaces/points-ventes';
|
||||
import { PointsVenteService } from 'src/app/core/services/points-vente';
|
||||
import { PointsVenteForm } from '@shared/forms/points-vente-form/points-vente-form';
|
||||
import { toast } from 'ngx-sonner';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-point-vente',
|
||||
templateUrl: './point-vente.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
DataTable,
|
||||
Paginator,
|
||||
SearchBar,
|
||||
Modal,
|
||||
ZardButtonComponent,
|
||||
PointsVenteForm,
|
||||
],
|
||||
})
|
||||
export class PointVentePage {
|
||||
rows = signal<PointVente[]>([]);
|
||||
total = signal(0);
|
||||
loading = signal(false);
|
||||
|
||||
page = signal(0);
|
||||
size = signal(10);
|
||||
search = signal('');
|
||||
sort = signal<SortState>({ key: 'nom', dir: 'asc' });
|
||||
|
||||
modalOpen = signal(false);
|
||||
modalTitle = signal('Nouveau point de vente');
|
||||
editingItem = signal<PointVente | null>(null);
|
||||
|
||||
@ViewChild(PointsVenteForm) formComp?: PointsVenteForm;
|
||||
|
||||
cols: TableColumn<PointVente>[] = [
|
||||
{ key: 'nom', label: 'Nom', sortable: true, defaultVisible: true },
|
||||
{ key: 'adresse', label: 'Adresse', sortable: true, defaultVisible: true },
|
||||
{ key: 'ville', label: 'Ville', sortable: true, defaultVisible: true },
|
||||
{
|
||||
key: 'statut',
|
||||
label: 'Statut',
|
||||
sortable: true,
|
||||
defaultVisible: true,
|
||||
cell: (pv) => this.renderStatutBadge(pv.statut),
|
||||
},
|
||||
{
|
||||
key: 'latitude',
|
||||
label: 'Latitude',
|
||||
cell: (pv) => pv.latitude?.toFixed(6) || '—',
|
||||
},
|
||||
{
|
||||
key: 'longitude',
|
||||
label: 'Longitude',
|
||||
cell: (pv) => pv.longitude?.toFixed(6) || '—',
|
||||
},
|
||||
];
|
||||
|
||||
constructor(private api: PointsVenteService) {
|
||||
effect(() => {
|
||||
const params = {
|
||||
page: this.page(),
|
||||
size: this.size(),
|
||||
search: this.search(),
|
||||
sortKey: this.sort().key,
|
||||
sortDir: this.sort().dir as SortDir,
|
||||
};
|
||||
untracked(() => this.fetch(params));
|
||||
});
|
||||
}
|
||||
|
||||
private fetch(params: {
|
||||
page: number;
|
||||
size: number;
|
||||
search: string;
|
||||
sortKey: string;
|
||||
sortDir: SortDir;
|
||||
}) {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.rows.set([]);
|
||||
this.total.set(0);
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
renderStatutBadge(statut: PointVenteStatut | string | undefined): string {
|
||||
if (!statut) return '';
|
||||
const s = String(statut).toUpperCase();
|
||||
if (s === 'ACTIVE') {
|
||||
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>`;
|
||||
}
|
||||
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>`;
|
||||
}
|
||||
|
||||
onSearch(q: string) {
|
||||
this.search.set(q);
|
||||
this.page.set(0);
|
||||
}
|
||||
|
||||
openCreate() {
|
||||
this.modalTitle.set('Nouveau point de vente');
|
||||
this.editingItem.set(null);
|
||||
queueMicrotask(() => this.modalOpen.set(true));
|
||||
}
|
||||
|
||||
openEdit(row: PointVente) {
|
||||
this.modalTitle.set("Modifier le point de vente");
|
||||
this.editingItem.set(row);
|
||||
queueMicrotask(() => this.modalOpen.set(true));
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.modalOpen.set(false);
|
||||
}
|
||||
|
||||
submitChildForm() {
|
||||
this.formComp?.onSubmit();
|
||||
}
|
||||
|
||||
onFormSave(payload: Partial<PointVente>) {
|
||||
const current = this.editingItem();
|
||||
const isCreating = !current?.id;
|
||||
|
||||
const req$ = current?.id
|
||||
? this.api.update(current.id, payload)
|
||||
: this.api.create(payload as Omit<PointVente, 'id'>);
|
||||
|
||||
req$.subscribe({
|
||||
next: (result) => {
|
||||
if (result) {
|
||||
toast.success(
|
||||
isCreating
|
||||
? 'Point de vente créé avec succès'
|
||||
: 'Point de vente modifié avec succès'
|
||||
);
|
||||
// Close modal first
|
||||
this.closeModal();
|
||||
// Reset form
|
||||
this.formComp?.resetForm();
|
||||
// Clear editing item
|
||||
this.editingItem.set(null);
|
||||
// Refresh data
|
||||
this.fetch({
|
||||
page: this.page(),
|
||||
size: this.size(),
|
||||
search: this.search(),
|
||||
sortKey: this.sort().key,
|
||||
sortDir: this.sort().dir as SortDir,
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error saving point de vente:', err);
|
||||
alert(
|
||||
isCreating
|
||||
? "Erreur lors de la création du point de vente"
|
||||
: "Erreur lors de la modification du point de vente"
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
remove(row: PointVente) {
|
||||
if (!confirm(`Supprimer le point de vente "${row.nom}" ?`)) return;
|
||||
this.api.delete(row.id).subscribe({
|
||||
next: (success) => {
|
||||
if (success) {
|
||||
toast.success('Point de vente supprimé avec succès');
|
||||
this.fetch({
|
||||
page: this.page(),
|
||||
size: this.size(),
|
||||
search: this.search(),
|
||||
sortKey: this.sort().key,
|
||||
sortDir: this.sort().dir as SortDir,
|
||||
});
|
||||
} else {
|
||||
alert("Erreur lors de la suppression du point de vente");
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
alert("Erreur lors de la suppression du point de vente");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@
|
||||
<!-- Agent Assignment Modal -->
|
||||
<app-modal
|
||||
[open]="assignModalOpen()"
|
||||
[title]="'Assigner le TPE ' + (assigningTpe()?.imei || '')"
|
||||
[title]="'Assigner le TPE ' + (assigningTpe()?.numeroSerie || '')"
|
||||
(close)="closeAssignModal()"
|
||||
size="md"
|
||||
>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ZardSelectItemComponent } from '@shared/components/select/select-item.c
|
||||
import { ZardFormModule } from '@shared/components/form/form.module';
|
||||
import { forkJoin, Subject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { toast } from 'ngx-sonner';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -105,11 +106,12 @@ export class TpePage implements OnInit {
|
||||
key: 'assigne',
|
||||
label: 'Assigné à',
|
||||
cell: (d) => {
|
||||
if (!d.assigne || !d.agent) {
|
||||
if (!d.agentConnecteId) {
|
||||
return '<span class="text-muted-foreground text-sm">Non assigné</span>';
|
||||
}
|
||||
const agent = d.agent;
|
||||
const code = agent.code
|
||||
// a rectifier apres avec les données des agents!
|
||||
const agent = {} as any;
|
||||
const code = agent?.code
|
||||
? `<span class="inline-flex items-center px-2 py-1 rounded bg-primary/10 text-primary text-xs font-medium">${agent.code}</span>`
|
||||
: '';
|
||||
const name =
|
||||
@@ -135,15 +137,8 @@ export class TpePage implements OnInit {
|
||||
];
|
||||
|
||||
allStatuses: TpeStatus[] = [
|
||||
'VALIDE',
|
||||
'INVALIDE',
|
||||
'EN_PANNE',
|
||||
'BLOQUE',
|
||||
'DISPONIBLE',
|
||||
'AFFECTE',
|
||||
'EN_MAINTENANCE',
|
||||
'HORS_SERVICE',
|
||||
'VOLE',
|
||||
'ACTIF',
|
||||
'HORS_SERVICE'
|
||||
];
|
||||
|
||||
constructor(private api: TpeService, private agentService: AgentService) {
|
||||
@@ -326,7 +321,7 @@ export class TpePage implements OnInit {
|
||||
}
|
||||
|
||||
onUpdateStatut(row: TpeDevice, newStatut: TpeStatus) {
|
||||
if (!confirm(`Changer le statut de ${row.imei} vers ${this.formatStatut(newStatut)} ?`)) return;
|
||||
if (!confirm(`Changer le statut de ${row.numeroSerie} vers ${this.formatStatut(newStatut)} ?`)) return;
|
||||
this.api.updateStatut(row.id, newStatut).subscribe({
|
||||
next: () => {
|
||||
this.fetch({
|
||||
@@ -342,7 +337,7 @@ export class TpePage implements OnInit {
|
||||
}
|
||||
|
||||
onLiberer(row: TpeDevice) {
|
||||
if (!confirm(`Libérer le TPE ${row.imei} ?`)) return;
|
||||
if (!confirm(`Libérer le TPE ${row.numeroSerie} ?`)) return;
|
||||
this.api.liberer(row.id).subscribe({
|
||||
next: () => {
|
||||
this.fetch({
|
||||
@@ -440,6 +435,7 @@ export class TpePage implements OnInit {
|
||||
: this.api.create(payload as Omit<TpeDevice, 'id'>);
|
||||
req$.subscribe({
|
||||
next: (result) => {
|
||||
toast.success('Tpe créé avec succès!')
|
||||
// For update, check if result is valid (update can return undefined on error)
|
||||
if (current?.id && !result) {
|
||||
console.error('Update failed - result is undefined');
|
||||
@@ -472,7 +468,7 @@ export class TpePage implements OnInit {
|
||||
}
|
||||
|
||||
remove(row: TpeDevice) {
|
||||
if (!confirm(`Supprimer l\'équipement IMEI ${row.imei} ?`)) return;
|
||||
if (!confirm(`Supprimer l\'équipement IMEI ${row.numeroSerie} ?`)) return;
|
||||
this.api.delete(row.id).subscribe(() => {
|
||||
this.fetch({
|
||||
page: this.page(),
|
||||
|
||||
@@ -1,91 +1,293 @@
|
||||
<form class="space-y-4" (ngSubmit)="onSubmit()" [formGroup]="form">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<z-form-field>
|
||||
<label z-form-label>Code</label>
|
||||
<div z-form-control [errorMessage]="error('code') || ''"><input z-input formControlName="code" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Profil</label>
|
||||
<div z-form-control [errorMessage]="error('profil') || ''"><input z-input formControlName="profil" /></div>
|
||||
</z-form-field>
|
||||
<form class="space-y-6" [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Statut</label>
|
||||
<div z-form-control [errorMessage]="error('statut') || ''">
|
||||
<z-select formControlName="statut" [zPlaceholder]="'Sélectionner...'">
|
||||
<z-select-item zValue="ACTIF">Actif</z-select-item>
|
||||
<z-select-item zValue="INACTIF">Inactif</z-select-item>
|
||||
<z-select-item zValue="SUSPENDU">Suspendu</z-select-item>
|
||||
</z-select>
|
||||
</div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Zone</label>
|
||||
<div z-form-control><input z-input formControlName="zone" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Kiosque</label>
|
||||
<div z-form-control><input z-input formControlName="kiosk" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Fonction</label>
|
||||
<div z-form-control><input z-input formControlName="fonction" /></div>
|
||||
</z-form-field>
|
||||
<!-- SECTION IDENTIFICATION -->
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow space-y-4 border border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Identification</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Nom</label>
|
||||
<div z-form-control [errorMessage]="error('nom') || ''"><input z-input formControlName="nom" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Prénom</label>
|
||||
<div z-form-control [errorMessage]="error('prenom') || ''"><input z-input formControlName="prenom" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Téléphone</label>
|
||||
<div z-form-control [errorMessage]="error('phone') || ''"><input z-input formControlName="phone" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Code</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('code') || ''">
|
||||
<input z-input formControlName="code" placeholder="Code de l'agent" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Limite inférieure</label>
|
||||
<div z-form-control><input z-input type="number" formControlName="limiteInferieure" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Limite supérieure</label>
|
||||
<div z-form-control><input z-input type="number" formControlName="limiteSuperieure" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Limite / transaction</label>
|
||||
<div z-form-control><input z-input type="number" formControlName="limiteParTransaction" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Min Airtime</label>
|
||||
<div z-form-control><input z-input type="number" formControlName="limiteMinAirtime" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Max Airtime</label>
|
||||
<div z-form-control><input z-input type="number" formControlName="limiteMaxAirtime" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Nbre max. périphériques</label>
|
||||
<div z-form-control><input z-input type="number" formControlName="maxPeripheriques" /></div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Profil</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('profil') || ''">
|
||||
<z-select formControlName="profil">
|
||||
@for (p of profils; track p) {
|
||||
<z-select-item [zValue]="p.value">{{ p.label }}</z-select-item>
|
||||
}
|
||||
</z-select>
|
||||
</div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Profil caisse</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('profil') || ''">
|
||||
<input z-input formControlName="caisseProfile"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Princial</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('principalCode') || ''">
|
||||
<input z-input formControlName="principalCode" placeholder="12242" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Groupe de limites</label>
|
||||
<div z-form-control [errorMessage]="error('limitId') || ''">
|
||||
<z-select formControlName="limitId" [zPlaceholder]="'Sélectionner...'">
|
||||
@for (l of limits; track l.id) {
|
||||
<z-select-item [zValue]="l.id">{{ l.nom }}</z-select-item>
|
||||
}
|
||||
</z-select>
|
||||
</div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Nom</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('nom') || ''">
|
||||
<input z-input formControlName="nom" placeholder="Nom" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Prénom</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('prenom') || ''">
|
||||
<input z-input formControlName="prenom" placeholder="Prénom" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Autres noms</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="autresNoms" placeholder="Autres noms" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Statut</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('statut') || ''">
|
||||
<z-select formControlName="statut">
|
||||
@for (s of statutOptions; track s) {
|
||||
<z-select-item [zValue]="s.value">{{ s.label }}</z-select-item>
|
||||
}
|
||||
</z-select>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Date d'embauche</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="date" formControlName="dateEmbauche"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Fonction</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="fonction" placeholder="Fonction" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-4">
|
||||
<z-button zType="destructive" type="button" (click)="cancel.emit()">Annuler</z-button>
|
||||
<z-button type="submit">Enregistrer</z-button>
|
||||
<!-- SECTION CONTACT -->
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow space-y-4 border border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Contact & Identité</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Phone</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('phone') || ''">
|
||||
<input z-input formControlName="phone" placeholder="Numéro de téléphone" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>PIN</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="password" formControlName="pin" placeholder="PIN" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Adresse</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="adresse" placeholder="Adresse" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Ville</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="ville" placeholder="Ville" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Lieu de naissance</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="lieuNaissance" placeholder="Lieu de naissance" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Date de naissance</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="date" formControlName="dateNaissance"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION LIMITE & PERIPHERIQUES -->
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow space-y-4 border border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Limites & Périphériques</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<z-form-field>
|
||||
<label z-form-label>Zone</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="zone"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Kiosk</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="kiosk"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Limite inférieure</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteInferieure"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Limite supérieure</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteSuperieure"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Limite par transaction</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteParTransaction"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Limite Airtime (min)</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteMinAirtime"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Limite Airtime (max)</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteMaxAirtime"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Nombre max périphériques</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="maxPeripheriques"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION INFORMATIONS LEGALES -->
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow space-y-4 border border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-200">Informations légales</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Nationalité</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="nationalite" placeholder="Nationalité" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>CNI</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="cni" placeholder="Numéro CNI" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>CNI délivrée le</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="date" formControlName="cniDelivreeLe"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>CNI délivrée à</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="cniDelivreeA" placeholder="Lieu de délivrance"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Résidence</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="residence" placeholder="Résidence"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Autre adresse</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="autreAdresse1" placeholder="Résidence"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Statut marital</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="statutMarital" placeholder="Statut marital"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Époux / Épouse</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="epoux" placeholder="Nom époux / épouse"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Autre téléphone</label>
|
||||
<div z-form-control>
|
||||
<input z-input formControlName="autreTelephone" placeholder="Autre numéro"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Autoriser aides</label>
|
||||
<div z-form-control>
|
||||
<input type="checkbox" formControlName="autoriserAides"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOUTONS SUBMIT -->
|
||||
<div class="flex justify-end space-x-4">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 rounded-lg bg-gray-300 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-gray-400 dark:hover:bg-gray-600"
|
||||
(click)="onClose()"
|
||||
>
|
||||
Réinitialiser
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ZardButtonComponent } from '@shared/components/button/button.component'
|
||||
import { Agent, AgentStatus } from 'src/app/core/interfaces/agent';
|
||||
import { AgentLimit } from 'src/app/core/interfaces/agent-limit';
|
||||
import { AgentLimitService } from 'src/app/core/services/agent-limit';
|
||||
import { ZardCheckboxComponent } from "@shared/components/checkbox/checkbox.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-agent-form',
|
||||
@@ -23,6 +24,33 @@ export class AgentForm {
|
||||
|
||||
limits: AgentLimit[] = [];
|
||||
|
||||
profils = [
|
||||
{
|
||||
label: "Caissier",
|
||||
value: "CAISSIER"
|
||||
},
|
||||
{
|
||||
label: "Agent",
|
||||
value: "AGENT"
|
||||
},
|
||||
{
|
||||
label: "Superviseur",
|
||||
value: "SUPERVISEUR"
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
statutOptions = [
|
||||
{
|
||||
label: "Actif",
|
||||
value: "ACTIF"
|
||||
},
|
||||
{
|
||||
label: "Inactif",
|
||||
value: "INACTIF"
|
||||
}
|
||||
]
|
||||
|
||||
private _value?: Agent;
|
||||
@Input() set value(v: Agent | undefined) { this._value = v; this.hydrateFromValue(v); }
|
||||
get value(): Agent | undefined { return this._value; }
|
||||
@@ -33,25 +61,46 @@ export class AgentForm {
|
||||
constructor(private fb: FormBuilder, private limitService: AgentLimitService) {
|
||||
this.form = this.fb.group({
|
||||
code: ['', Validators.required],
|
||||
profil: ['', Validators.required],
|
||||
statut: ['ACTIF' as AgentStatus, Validators.required],
|
||||
profil: ['CAISSIER', Validators.required],
|
||||
principalCode: ['', Validators.required],
|
||||
caisseProfile: ['', Validators.required],
|
||||
statut: ['ACTIF', Validators.required],
|
||||
zone: [''],
|
||||
kiosk: [''],
|
||||
fonction: [''],
|
||||
dateEmbauche: [''],
|
||||
|
||||
nom: ['', Validators.required],
|
||||
prenom: ['', Validators.required],
|
||||
phone: ['', [Validators.required, Validators.minLength(6)]],
|
||||
autresNoms: [''],
|
||||
dateNaissance: [''],
|
||||
lieuNaissance: [''],
|
||||
ville: [''],
|
||||
adresse: [''],
|
||||
autoriserAides: [false],
|
||||
|
||||
limiteInferieure: [0, [Validators.min(0)]],
|
||||
limiteSuperieure: [0, [Validators.min(0)]],
|
||||
limiteParTransaction: [0, [Validators.min(0)]],
|
||||
limiteMinAirtime: [0, [Validators.min(0)]],
|
||||
limiteMaxAirtime: [0, [Validators.min(0)]],
|
||||
maxPeripheriques: [0, [Validators.min(0)]],
|
||||
phone: ['', Validators.required],
|
||||
pin: [''],
|
||||
|
||||
limiteInferieure: [''],
|
||||
limiteSuperieure: [''],
|
||||
limiteParTransaction: [''],
|
||||
limiteMinAirtime: [''],
|
||||
limiteMaxAirtime: [''],
|
||||
|
||||
maxPeripheriques: [''],
|
||||
|
||||
limitId: ['1'],
|
||||
|
||||
nationalite: [''],
|
||||
cni: [''],
|
||||
cniDelivreeLe: [''],
|
||||
cniDelivreeA: [''],
|
||||
residence: [''],
|
||||
autreAdresse1: [''],
|
||||
statutMarital: [''],
|
||||
epoux: [''],
|
||||
autreTelephone: [''],
|
||||
|
||||
limitId: ['', Validators.required],
|
||||
});
|
||||
|
||||
this.limitService
|
||||
@@ -59,29 +108,122 @@ export class AgentForm {
|
||||
.subscribe((res) => (this.limits = res.content));
|
||||
}
|
||||
|
||||
error(control: string): string {
|
||||
errorMessage(control: string): string {
|
||||
const e = this.form.get(control)?.errors; if (!e) return ''; if (e['required']) return 'Requis'; return '';
|
||||
}
|
||||
|
||||
private hydrateFromValue(v?: Agent) {
|
||||
if (!v) {
|
||||
this.form.reset({
|
||||
code: '', profil: '', statut: 'ACTIF', zone: '', kiosk: '', fonction: '', dateEmbauche: '', nom: '', prenom: '', phone: '', limiteInferieure: 0, limiteSuperieure: 0, limiteParTransaction: 0, limiteMinAirtime: 0, limiteMaxAirtime: 0, maxPeripheriques: 0, limitId: '',
|
||||
statut: 'ACTIF',
|
||||
autoriserAides: false,
|
||||
});
|
||||
this.submitted = false;
|
||||
return;
|
||||
}
|
||||
this.form.reset({
|
||||
code: v.code, profil: v.profil, statut: v.statut, zone: v.zone ?? '', kiosk: v.kiosk ?? '', fonction: v.fonction ?? '', dateEmbauche: v.dateEmbauche ?? '', nom: v.nom, prenom: v.prenom, phone: v.phone, limiteInferieure: v.limiteInferieure ?? 0, limiteSuperieure: v.limiteSuperieure ?? 0, limiteParTransaction: v.limiteParTransaction ?? 0, limiteMinAirtime: v.limiteMinAirtime ?? 0, limiteMaxAirtime: v.limiteMaxAirtime ?? 0, maxPeripheriques: v.maxPeripheriques ?? 0, limitId: v.limitId ?? '',
|
||||
code: v.code || '',
|
||||
profil: v.profil || '',
|
||||
principalCode: v.principalCode || '',
|
||||
caisseProfile: v.caisseProfile || '',
|
||||
statut: v.statut || 'ACTIF',
|
||||
zone: v.zone || '',
|
||||
kiosk: v.kiosk || '',
|
||||
fonction: v.fonction || '',
|
||||
dateEmbauche: v.dateEmbauche || '',
|
||||
|
||||
nom: v.nom || '',
|
||||
prenom: v.prenom || '',
|
||||
autresNoms: v.autresNoms || '',
|
||||
dateNaissance: v.dateNaissance || '',
|
||||
lieuNaissance: v.lieuNaissance || '',
|
||||
ville: v.ville || '',
|
||||
adresse: v.adresse || '',
|
||||
autoriserAides: v.autoriserAides ?? false,
|
||||
|
||||
phone: v.phone || '',
|
||||
pin: v.pin || '',
|
||||
|
||||
limiteInferieure: v.limiteInferieure || '',
|
||||
limiteSuperieure: v.limiteSuperieure || '',
|
||||
limiteParTransaction: v.limiteParTransaction || '',
|
||||
limiteMinAirtime: v.limiteMinAirtime || '',
|
||||
limiteMaxAirtime: v.limiteMaxAirtime || '',
|
||||
|
||||
maxPeripheriques: v.maxPeripheriques || '',
|
||||
|
||||
limitId: v.limitId || '',
|
||||
|
||||
nationalite: v.nationalite || '',
|
||||
cni: v.cni || '',
|
||||
cniDelivreeLe: v.cniDelivreeLe || '',
|
||||
cniDelivreeA: v.cniDelivreeA || '',
|
||||
residence: v.residence || '',
|
||||
autreAdresse1: v.autreAdresse1 || '',
|
||||
statutMarital: v.statutMarital || '',
|
||||
epoux: v.epoux || '',
|
||||
autreTelephone: v.autreTelephone || '',
|
||||
});
|
||||
|
||||
this.submitted = false;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.submitted = true;
|
||||
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched(); return;
|
||||
}
|
||||
const raw = this.form.getRawValue() as any;
|
||||
const payload: Agent = { id: this.value?.id ?? '', code: raw.code, profil: raw.profil, statut: raw.statut, zone: raw.zone, kiosk: raw.kiosk, fonction: raw.fonction, dateEmbauche: raw.dateEmbauche, nom: raw.nom, prenom: raw.prenom, phone: raw.phone, limiteInferieure: +raw.limiteInferieure, limiteSuperieure: +raw.limiteSuperieure, limiteParTransaction: +raw.limiteParTransaction, limiteMinAirtime: +raw.limiteMinAirtime, limiteMaxAirtime: +raw.limiteMaxAirtime, maxPeripheriques: +raw.maxPeripheriques, limitId: raw.limitId };
|
||||
const payload: Agent = {
|
||||
id: this.value?.id ?? '',
|
||||
code: raw.code,
|
||||
profil: raw.profil,
|
||||
statut: raw.statut,
|
||||
principalCode: raw.principalCode || '',
|
||||
caisseProfile: raw.caisseProfile || '',
|
||||
zone: raw.zone || '',
|
||||
kiosk: raw.kiosk || '',
|
||||
fonction: raw.fonction || '',
|
||||
dateEmbauche: raw.dateEmbauche || '',
|
||||
|
||||
nom: raw.nom,
|
||||
prenom: raw.prenom,
|
||||
autresNoms: raw.autresNoms || '',
|
||||
dateNaissance: raw.dateNaissance || '',
|
||||
lieuNaissance: raw.lieuNaissance || '',
|
||||
ville: raw.ville || '',
|
||||
adresse: raw.adresse || '',
|
||||
autoriserAides: raw.autoriserAides ?? false,
|
||||
|
||||
phone: raw.phone,
|
||||
pin: raw.pin || '',
|
||||
|
||||
limiteInferieure: +raw.limiteInferieure || 0,
|
||||
limiteSuperieure: +raw.limiteSuperieure || 0,
|
||||
limiteParTransaction: +raw.limiteParTransaction || 0,
|
||||
limiteMinAirtime: +raw.limiteMinAirtime || 0,
|
||||
limiteMaxAirtime: +raw.limiteMaxAirtime || 0,
|
||||
|
||||
maxPeripheriques: +raw.maxPeripheriques || 0,
|
||||
limitId: raw.limitId || '1',
|
||||
|
||||
// Légales
|
||||
nationalite: raw.nationalite || '',
|
||||
cni: raw.cni || '',
|
||||
cniDelivreeLe: raw.cniDelivreeLe || '',
|
||||
cniDelivreeA: raw.cniDelivreeA || '',
|
||||
residence: raw.residence || '',
|
||||
autreAdresse1: raw.autreAdresse1 || '',
|
||||
statutMarital: raw.statutMarital || '',
|
||||
epoux: raw.epoux || '',
|
||||
autreTelephone: raw.autreTelephone || ''
|
||||
};
|
||||
this.save.emit(payload);
|
||||
}
|
||||
|
||||
onClose(){
|
||||
this.cancel.emit()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<form class="space-y-6" (ngSubmit)="onSubmit()" [formGroup]="form">
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
||||
<z-card class="p-4 space-y-3">
|
||||
<div class="text-lg font-semibold">Informations Emploi</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
@@ -10,8 +10,8 @@
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Profil</label>
|
||||
<div z-form-control [errorMessage]="error('profil') || ''">
|
||||
<input z-input formControlName="profil" /></div
|
||||
<div z-form-control [errorMessage]="error('profile') || ''">
|
||||
<input z-input formControlName="profile" /></div
|
||||
></z-form-field>
|
||||
<z-form-field class="md:col-span-2"
|
||||
><label z-form-label>Agent Principal</label>
|
||||
@@ -94,6 +94,88 @@
|
||||
></z-form-field>
|
||||
</div>
|
||||
</z-card>
|
||||
|
||||
<z-card class="p-4 space-y-3">
|
||||
<div class="text-lg font-semibold">Paramètres de connexion</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<z-form-field
|
||||
><label z-form-label>N° Téléphone</label>
|
||||
<div z-form-control [errorMessage]="error('phone') || ''">
|
||||
<input z-input formControlName="phone" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>PIN</label>
|
||||
<div z-form-control><input z-input formControlName="pin" type="password" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Limite inférieure</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteInferieure" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Limite supérieure</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteSuperieure" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Limite / transaction</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteParTransaction" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Limite min airtime</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteMinAirtime" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Limite max airtime</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteMaxAirtime" /></div
|
||||
></z-form-field>
|
||||
<z-form-field class="md:col-span-2"
|
||||
><label z-form-label>Groupe de limites</label>
|
||||
<div z-form-control [errorMessage]="error('limitId') || ''">
|
||||
<z-select formControlName="limitId" [zPlaceholder]="'Sélectionner...'">
|
||||
@for (l of limits; track l.id) {
|
||||
<z-select-item [zValue]="l.id">
|
||||
{{ l.nom }}
|
||||
@if (l.isDefault) {
|
||||
<span class="text-xs text-primary ml-1">(Par défaut)</span>
|
||||
} @if (l.actif) {
|
||||
<span class="text-xs text-green-600 dark:text-green-400 ml-1">• Actif</span>
|
||||
}
|
||||
</z-select-item>
|
||||
}
|
||||
</z-select>
|
||||
</div>
|
||||
@if (selectedLimit) {
|
||||
<div class="mt-2 p-3 bg-accent rounded text-sm space-y-1">
|
||||
<div class="font-medium">{{ selectedLimit.nom }}</div>
|
||||
<div class="text-muted-foreground text-xs">
|
||||
@if (selectedLimit.isDefault) {
|
||||
<span class="inline-flex items-center gap-1"
|
||||
><i class="icon-star size-3"></i> Limite par défaut</span
|
||||
>
|
||||
} @if (selectedLimit.actif) {
|
||||
<span class="inline-flex items-center gap-1 ml-2"
|
||||
><i class="icon-check size-3"></i> Contrôle actif</span
|
||||
>
|
||||
} @else {
|
||||
<span class="inline-flex items-center gap-1 ml-2"
|
||||
><i class="icon-x size-3"></i> Contrôle inactif</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-2">
|
||||
<div>Min Bet: {{ (selectedLimit.betMin ?? 0).toLocaleString('fr-FR') }}</div>
|
||||
<div>Max Bet: {{ (selectedLimit.betMax ?? 0).toLocaleString('fr-FR') }}</div>
|
||||
<div>Max Bet (tx): {{ (selectedLimit.maxBet ?? 0).toLocaleString('fr-FR') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</z-form-field>
|
||||
</div>
|
||||
</z-card>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
@@ -138,57 +220,15 @@
|
||||
></z-form-field>
|
||||
</div>
|
||||
</z-card>
|
||||
<z-card class="p-4 space-y-3">
|
||||
<div class="text-lg font-semibold">Paramètres de connexion</div>
|
||||
<div class="flex flex-col gap-y-3 mb-3">
|
||||
<z-form-field
|
||||
><label z-form-label>N° Téléphone</label>
|
||||
<div z-form-control [errorMessage]="error('phone') || ''">
|
||||
<input z-input formControlName="phone" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>PIN</label>
|
||||
<div z-form-control><input z-input formControlName="pin" type="password" /></div
|
||||
></z-form-field>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<z-form-field
|
||||
><label z-form-label>Limite inférieure</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteInferieure" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Limite supérieure</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteSuperieure" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Limite / transaction</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteParTransaction" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Limite min airtime</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteMinAirtime" /></div
|
||||
></z-form-field>
|
||||
<z-form-field
|
||||
><label z-form-label>Limite max airtime</label>
|
||||
<div z-form-control>
|
||||
<input z-input type="number" formControlName="limiteMaxAirtime" /></div
|
||||
></z-form-field>
|
||||
</div>
|
||||
</z-card>
|
||||
|
||||
<!-- <z-card class="p-4 space-y-4">
|
||||
<z-card class="p-4 space-y-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-lg font-semibold">Membres de famille</div>
|
||||
<z-button zType="default" (click)="addFamily()">
|
||||
<i class="icon-plus mr-2"></i>Ajouter un membre
|
||||
</z-button>
|
||||
</div>
|
||||
@if (!compact) {
|
||||
<div class="space-y-3" formArrayName="famille">
|
||||
<div class="space-y-3" formArrayName="famille">
|
||||
@for (fm of famille.controls; track $index; let i = $index) {
|
||||
<div
|
||||
[formGroupName]="i"
|
||||
@@ -267,13 +307,62 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="text-sm text-muted">Les membres de la famille sont gérés depuis la fiche agent après la création.</div>
|
||||
}
|
||||
</z-card> -->
|
||||
</z-card>
|
||||
</div>
|
||||
|
||||
<!-- TPE assignment UI removed from this form -->
|
||||
<z-card class="p-4 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-lg font-semibold">Assignation TPE (actifs)</div>
|
||||
<z-button zType="ghost" (click)="openAssignTpe()"
|
||||
><i class="icon-plus mr-2"></i>Gérer</z-button
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@for (id of tpeArray.value; track id) {
|
||||
<span class="px-2 py-1 rounded bg-accent text-sm flex items-center gap-2">
|
||||
{{ tpeLabel(id) }}
|
||||
<button z-button zType="ghost" zSize="sm" (click)="onToggleTpe(id, false)">
|
||||
<i class="icon-x"></i>
|
||||
</button>
|
||||
</span>
|
||||
} @empty {
|
||||
<span class="text-muted-foreground text-sm">Aucun TPE assigné</span>
|
||||
}
|
||||
</div>
|
||||
</z-card>
|
||||
|
||||
<!-- TPE assignment modal removed from this form; TPE assignment is handled elsewhere -->
|
||||
<app-modal
|
||||
[open]="assignModalOpen"
|
||||
[title]="'Assigner des TPE'"
|
||||
(close)="closeAssignTpe()"
|
||||
size="xxl"
|
||||
>
|
||||
<app-search-bar
|
||||
placeholder="Rechercher par IMEI, marque, modèle..."
|
||||
(search)="onTpeSearch($event)"
|
||||
></app-search-bar>
|
||||
<app-data-table [data]="tpeRows" [columns]="tpeCols" [sort]="{ key: 'imei', dir: 'asc' }">
|
||||
<ng-template #rowActions let-row>
|
||||
@if (!isSelectedTpe(row.id)) {
|
||||
<button z-button zType="ghost" (click)="onToggleTpe(row.id, true)">
|
||||
<i class="icon-plus"></i>
|
||||
</button>
|
||||
} @else {
|
||||
<button z-button zType="destructive" (click)="onToggleTpe(row.id, false)">
|
||||
<i class="icon-trash"></i>
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
<app-paginator
|
||||
[total]="tpeTotal"
|
||||
[page]="tpePage"
|
||||
[perPage]="tpePerPage"
|
||||
(pageChange)="onTpePageChange($event)"
|
||||
(perPageChange)="onTpePerPageChange($event)"
|
||||
></app-paginator>
|
||||
<div modal-actions class="flex justify-end gap-2">
|
||||
<z-button zType="default" (click)="closeAssignTpe()">Terminer</z-button>
|
||||
</div>
|
||||
</app-modal>
|
||||
</form>
|
||||
|
||||
@@ -7,12 +7,16 @@ import { ZardSelectComponent } from '@shared/components/select/select.component'
|
||||
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
||||
import { ZardButtonComponent } from '@shared/components/button/button.component';
|
||||
import { ZardCardComponent } from '@shared/components/card/card.component';
|
||||
// DataTable removed from this form (TPE UI was removed)
|
||||
import { DataTable, SortState, TableColumn } from '@shared/components/data-table/data-table';
|
||||
import { Paginator } from '@shared/components/paginator/paginator';
|
||||
import { SearchBar } from '@shared/components/search-bar/search-bar';
|
||||
import { Modal } from '@shared/components/modal/modal';
|
||||
import { Agent, AgentFamilyMember } from 'src/app/core/interfaces/agent';
|
||||
import { AgentLimit } from 'src/app/core/interfaces/agent-limit';
|
||||
import { AgentLimitService } from 'src/app/core/services/agent-limit';
|
||||
import { AgentFamilyMemberService } from 'src/app/core/services/agent-family-member';
|
||||
// TPE assignment removed from form (handled elsewhere)
|
||||
import { TpeDevice } from 'src/app/core/interfaces/tpe';
|
||||
import { TpeService } from 'src/app/core/services/tpe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-agent-full-form',
|
||||
@@ -26,18 +30,36 @@ import { AgentFamilyMemberService } from 'src/app/core/services/agent-family-mem
|
||||
ZardInputDirective,
|
||||
ZardSelectComponent,
|
||||
ZardSelectItemComponent,
|
||||
ZardButtonComponent,
|
||||
ZardCardComponent,
|
||||
// TPE UI components removed from this form
|
||||
DataTable,
|
||||
Paginator,
|
||||
SearchBar,
|
||||
Modal,
|
||||
],
|
||||
})
|
||||
export class AgentFullForm {
|
||||
@Output() save = new EventEmitter<Partial<Agent>>();
|
||||
@Output() save = new EventEmitter<Agent>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
@Input() compact = false; // when true, hide family and TPE sections for a shorter creation form
|
||||
|
||||
limits: AgentLimit[] = [];
|
||||
selectedLimit: AgentLimit | null = null;
|
||||
// TPE assignment removed from the creation form; handled in separate flows
|
||||
tpes: TpeDevice[] = [];
|
||||
// TPE picker state
|
||||
assignModalOpen = false;
|
||||
tpeRows: TpeDevice[] = [];
|
||||
tpeTotal = 0;
|
||||
tpePage = 1;
|
||||
tpePerPage = 10;
|
||||
tpeSearch = '';
|
||||
tpeCols: TableColumn<TpeDevice>[] = [
|
||||
{ key: 'imei', label: 'IMEI', sortable: true },
|
||||
{ key: 'serial', label: 'N° Série', sortable: true },
|
||||
{ key: 'marque', label: 'Marque', sortable: true },
|
||||
{ key: 'modele', label: 'Modèle', sortable: true },
|
||||
{ key: 'type', label: 'Type', sortable: true },
|
||||
{ key: 'statut', label: 'Statut', sortable: true },
|
||||
];
|
||||
|
||||
private _value?: Agent;
|
||||
@Input() set value(v: Agent | undefined) { this._value = v; this.hydrate(v); }
|
||||
@@ -49,19 +71,21 @@ export class AgentFullForm {
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private limitService: AgentLimitService,
|
||||
private tpeService: TpeService,
|
||||
private familyMemberService: AgentFamilyMemberService
|
||||
) {
|
||||
this.form = this.fb.group({
|
||||
// Emploi
|
||||
code: ['', Validators.required], profil: ['', Validators.required], principalCode: [''], caisseProfile: [''], statut: ['ACTIF', Validators.required], zone: [''], kiosk: [''], fonction: [''], dateEmbauche: [''],
|
||||
code: ['', Validators.required], profile: ['', Validators.required], principalCode: [''], caisseProfile: [''], statut: ['ACTIF', Validators.required], zone: [''], kiosk: [''], fonction: [''], dateEmbauche: [''],
|
||||
// Perso
|
||||
nom: ['', Validators.required], prenom: ['', Validators.required], autresNoms: [''], dateNaissance: [''], lieuNaissance: [''], adresse: [''], ville: [''], autoriserAides: [false], maxPeripheriques: [0, [Validators.min(0)]],
|
||||
// Connexion & limites
|
||||
phone: ['', Validators.required], pin: [''], limiteInferieure: [0], limiteSuperieure: [0], limiteParTransaction: [0], limiteMinAirtime: [0], limiteMaxAirtime: [0], limitId: ['1'],
|
||||
phone: ['', Validators.required], pin: [''], limiteInferieure: [0], limiteSuperieure: [0], limiteParTransaction: [0], limiteMinAirtime: [0], limiteMaxAirtime: [0], limitId: ['', Validators.required],
|
||||
// Légales
|
||||
nationalite: [''], cni: [''], cniDelivreeLe: [''], cniDelivreeA: [''], residence: [''], autreAdresse1: [''], statutMarital: [''], epoux: [''], autreTelephone: [''],
|
||||
// Famille
|
||||
famille: this.fb.array([]),
|
||||
// TPE (stored as IDs in form, converted to full objects on submit)
|
||||
});
|
||||
|
||||
this.limitService.list({ page: 1, perPage: 100, search: '', sortKey: 'nom', sortDir: 'asc' } as any).subscribe((res) => {
|
||||
@@ -89,14 +113,14 @@ export class AgentFullForm {
|
||||
this.selectedLimit = defaultLimit;
|
||||
}
|
||||
});
|
||||
// TPE fetching/assignment handled outside of this form
|
||||
// initial fetch page for TPE modal
|
||||
this.fetchTpes();
|
||||
}
|
||||
|
||||
get famille(): FormArray { return this.form.get('famille') as FormArray; }
|
||||
|
||||
get tpeArray(): FormArray { return this.form.get('tpeIds') as FormArray; }
|
||||
|
||||
addFamily() {
|
||||
if (this.compact) return; // no family in compact mode
|
||||
this.famille.push(
|
||||
this.fb.group({
|
||||
id: [''], // Will be set when saved
|
||||
@@ -118,6 +142,7 @@ export class AgentFullForm {
|
||||
|
||||
private hydrate(v?: Agent) {
|
||||
this.famille.clear();
|
||||
this.tpeArray.clear();
|
||||
this.selectedLimit = null;
|
||||
|
||||
if (!v) {
|
||||
@@ -127,7 +152,7 @@ export class AgentFullForm {
|
||||
|
||||
this.form.reset({
|
||||
code: '',
|
||||
profil: '',
|
||||
profile: '',
|
||||
principalCode: '',
|
||||
caisseProfile: '',
|
||||
statut: 'ACTIF',
|
||||
@@ -200,10 +225,59 @@ export class AgentFullForm {
|
||||
});
|
||||
}
|
||||
|
||||
// Assigned TPEs are not handled in this form
|
||||
|
||||
}
|
||||
|
||||
// TPE assignment / picker removed from this form
|
||||
onToggleTpe(id: string, checked: boolean) {
|
||||
const idx = this.tpeArray.value.indexOf(id);
|
||||
if (checked && idx === -1) this.tpeArray.push(this.fb.control(id));
|
||||
if (!checked && idx !== -1) this.tpeArray.removeAt(idx);
|
||||
}
|
||||
|
||||
openAssignTpe() {
|
||||
this.assignModalOpen = true;
|
||||
this.fetchTpes();
|
||||
}
|
||||
closeAssignTpe() {
|
||||
this.assignModalOpen = false;
|
||||
// Refresh TPE list to show current assignments
|
||||
this.fetchTpes();
|
||||
}
|
||||
onTpeSearch(q: string) { this.tpeSearch = q; this.tpePage = 1; this.fetchTpes(); }
|
||||
onTpePageChange(p: number) { this.tpePage = p; this.fetchTpes(); }
|
||||
onTpePerPageChange(pp: number) { this.tpePerPage = pp; this.fetchTpes(); }
|
||||
isSelectedTpe(id: string) { return (this.tpeArray.value as string[]).includes(id); }
|
||||
|
||||
fetchTpes() {
|
||||
this.tpeService
|
||||
.list({ page: this.tpePage, perPage: this.tpePerPage, search: this.tpeSearch, sortKey: 'imei', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => {
|
||||
// Only show VALIDE TPEs that are either not assigned or assigned to this agent
|
||||
const currentAgentId = this.value?.id;
|
||||
this.tpeRows = res.content.filter((t) => {
|
||||
if (t.statut !== 'ACTIF') return false;
|
||||
// If TPE is assigned but to this agent, show it
|
||||
if (t.agentConnecteId && currentAgentId) {
|
||||
// We need to check if this TPE is assigned to this agent
|
||||
// For now, show all VALIDE TPEs - the backend should handle assignment logic
|
||||
return true;
|
||||
}
|
||||
// Show unassigned TPEs
|
||||
return !t.agentConnecteId;
|
||||
});
|
||||
this.tpeTotal = this.tpeRows.length;
|
||||
});
|
||||
}
|
||||
|
||||
tpeLabel(id: string): string {
|
||||
const list = [...(this.tpeRows ?? []), ...(this.tpes ?? [])];
|
||||
const found = list.find((t) => t.id === id);
|
||||
if (found) {
|
||||
return `${found.numeroSerie}`;
|
||||
}
|
||||
// Try to load from service if not in current list
|
||||
return id;
|
||||
}
|
||||
|
||||
// === Validation helpers ===
|
||||
error(control: string): string {
|
||||
@@ -237,13 +311,23 @@ export class AgentFullForm {
|
||||
return;
|
||||
}
|
||||
const raw = this.form.getRawValue() as any;
|
||||
// Prepare partial agent payload (family members are handled separately)
|
||||
const payload: Partial<Agent> = {
|
||||
...raw,
|
||||
...(this.value?.id ? { id: this.value.id } : {}),
|
||||
};
|
||||
|
||||
// Emit the partial agent payload (parent will decide create vs update)
|
||||
// Convert TPE IDs to full TPE object
|
||||
const tpes: TpeDevice[] = [];
|
||||
|
||||
// Prepare agent payload (without famille - family members are handled separately)
|
||||
const payload: Agent = {
|
||||
...(this.value ?? {}),
|
||||
id: this.value?.id || '',
|
||||
...raw,
|
||||
tpes: tpes,
|
||||
} as Agent;
|
||||
|
||||
// Remove tpeIds from payload (it's not part of Agent interface)
|
||||
delete (payload as any).tpeIds;
|
||||
|
||||
// Emit the agent payload first
|
||||
// Family members will be saved separately in the parent component
|
||||
this.save.emit(payload);
|
||||
}
|
||||
|
||||
@@ -263,6 +347,7 @@ export class AgentFullForm {
|
||||
this._value = undefined;
|
||||
this.selectedLimit = null;
|
||||
this.famille.clear();
|
||||
this.tpeArray.clear();
|
||||
|
||||
// Find default limit to assign it automatically
|
||||
const defaultLimit = this.limits.find((l) => l.isDefault);
|
||||
@@ -270,7 +355,7 @@ export class AgentFullForm {
|
||||
|
||||
this.form.reset({
|
||||
code: '',
|
||||
profil: '',
|
||||
profile: '',
|
||||
principalCode: '',
|
||||
caisseProfile: '',
|
||||
statut: 'ACTIF',
|
||||
|
||||
@@ -180,14 +180,14 @@
|
||||
Types de paris ouverts
|
||||
</label>
|
||||
|
||||
<z-form-control class="space-y-2">
|
||||
<z-form-control class="space-y-2">
|
||||
@for (t of courseTypes; track t.value) {
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
[value]="t.value"
|
||||
(change)="onToggleType($event)"
|
||||
[checked]="(form.get('typesParisOuverts')?.value || []).includes(t.value)"
|
||||
[checked]="form.value.typesParisOuverts?.includes(t.value)"
|
||||
/>
|
||||
{{ t.label }}
|
||||
</label>
|
||||
|
||||
@@ -216,8 +216,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -248,9 +246,6 @@ 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);
|
||||
@@ -279,9 +274,6 @@ 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -300,6 +292,7 @@ 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
|
||||
@@ -325,8 +318,6 @@ 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({
|
||||
@@ -334,8 +325,7 @@ 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({
|
||||
|
||||
60
src/app/shared/forms/point-vente-form/point-vente-form.html
Normal file
60
src/app/shared/forms/point-vente-form/point-vente-form.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<form class="space-y-4" (ngSubmit)="onSubmit()" [formGroup]="form">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<z-form-field>
|
||||
<label z-form-label>Nom</label>
|
||||
<div z-form-control [errorMessage]="error('nom') || ''">
|
||||
<input z-input formControlName="nom" placeholder="Nom du point de vente" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Ville</label>
|
||||
<div z-form-control [errorMessage]="error('ville') || ''">
|
||||
<input z-input formControlName="ville" placeholder="Ville" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field class="md:col-span-2">
|
||||
<label z-form-label>Adresse</label>
|
||||
<div z-form-control [errorMessage]="error('adresse') || ''">
|
||||
<input z-input formControlName="adresse" placeholder="Adresse complète" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Latitude</label>
|
||||
<div z-form-control [errorMessage]="error('latitude') || ''">
|
||||
<input
|
||||
z-input
|
||||
type="number"
|
||||
step="any"
|
||||
formControlName="latitude"
|
||||
placeholder="0.000000"
|
||||
/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Longitude</label>
|
||||
<div z-form-control [errorMessage]="error('longitude') || ''">
|
||||
<input
|
||||
z-input
|
||||
type="number"
|
||||
step="any"
|
||||
formControlName="longitude"
|
||||
placeholder="0.000000"
|
||||
/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Statut</label>
|
||||
<div z-form-control [errorMessage]="error('statut') || ''">
|
||||
<z-select formControlName="statut">
|
||||
<z-select-item zValue="ACTIVE">Actif</z-select-item>
|
||||
<z-select-item zValue="INACTIVE">Inactif</z-select-item>
|
||||
</z-select>
|
||||
</div>
|
||||
</z-form-field>
|
||||
</div>
|
||||
</form>
|
||||
115
src/app/shared/forms/point-vente-form/point-vente-form.ts
Normal file
115
src/app/shared/forms/point-vente-form/point-vente-form.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
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 { PointVente, PointVenteStatut } from 'src/app/core/interfaces/points-ventes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-point-vente-form',
|
||||
standalone: true,
|
||||
templateUrl: './point-vente-form.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
ZardFormModule,
|
||||
ZardInputDirective,
|
||||
ZardSelectComponent,
|
||||
ZardSelectItemComponent,
|
||||
],
|
||||
})
|
||||
export class PointVenteForm {
|
||||
@Output() save = new EventEmitter<Partial<PointVente>>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
private _value?: PointVente;
|
||||
@Input() set value(v: PointVente | undefined) {
|
||||
this._value = v;
|
||||
this.hydrate(v);
|
||||
}
|
||||
get value(): PointVente | undefined {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
form: FormGroup;
|
||||
submitted = false;
|
||||
|
||||
constructor(private fb: FormBuilder) {
|
||||
this.form = this.fb.group({
|
||||
nom: ['', Validators.required],
|
||||
adresse: ['', Validators.required],
|
||||
ville: ['', Validators.required],
|
||||
latitude: [0, [Validators.required, Validators.min(-90), Validators.max(90)]],
|
||||
longitude: [0, [Validators.required, Validators.min(-180), Validators.max(180)]],
|
||||
statut: ['ACTIVE', Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
error(control: string): string {
|
||||
const c = this.form.get(control);
|
||||
if (!c) return '';
|
||||
const invalid = c.invalid && (c.touched || this.submitted);
|
||||
if (!invalid) return '';
|
||||
const e = c.errors || {};
|
||||
if (e['required']) return 'Ce champ est obligatoire';
|
||||
if (e['min']) return `Valeur minimale: ${e['min'].min}`;
|
||||
if (e['max']) return `Valeur maximale: ${e['max'].max}`;
|
||||
return 'Valeur invalide';
|
||||
}
|
||||
|
||||
private hydrate(v?: PointVente) {
|
||||
if (!v) {
|
||||
this.form.reset({
|
||||
nom: '',
|
||||
adresse: '',
|
||||
ville: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
statut: 'ACTIVE',
|
||||
});
|
||||
this.submitted = false;
|
||||
return;
|
||||
}
|
||||
this.form.reset({
|
||||
nom: v.nom,
|
||||
adresse: v.adresse,
|
||||
ville: v.ville,
|
||||
latitude: v.latitude ?? 0,
|
||||
longitude: v.longitude ?? 0,
|
||||
statut: v.statut ?? 'ACTIVE',
|
||||
});
|
||||
this.submitted = false;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.submitted = true;
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
const raw = this.form.getRawValue() as any;
|
||||
const payload: Partial<PointVente> = {
|
||||
...raw,
|
||||
latitude: Number(raw.latitude),
|
||||
longitude: Number(raw.longitude),
|
||||
...(this.value?.id ? { id: this.value.id } : {}),
|
||||
};
|
||||
this.save.emit(payload);
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
this._value = undefined;
|
||||
this.form.reset({
|
||||
nom: '',
|
||||
adresse: '',
|
||||
ville: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
statut: 'ACTIVE',
|
||||
});
|
||||
this.submitted = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<form class="space-y-4" (ngSubmit)="onSubmit()" [formGroup]="form">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<z-form-field>
|
||||
<label z-form-label>Nom</label>
|
||||
<div z-form-control [errorMessage]="error('nom') || ''">
|
||||
<input z-input formControlName="nom" placeholder="Nom du point de vente" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Ville</label>
|
||||
<div z-form-control [errorMessage]="error('ville') || ''">
|
||||
<input z-input formControlName="ville" placeholder="Ville" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field >
|
||||
<label z-form-label>Adresse</label>
|
||||
<div z-form-control [errorMessage]="error('adresse') || ''">
|
||||
<input z-input formControlName="adresse" placeholder="Adresse complète" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<label z-form-label>Code</label>
|
||||
<div z-form-control [errorMessage]="error('code') || ''">
|
||||
<input z-input formControlName="code" placeholder="Code du point de vente" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Latitude</label>
|
||||
<div z-form-control [errorMessage]="error('latitude') || ''">
|
||||
<input
|
||||
z-input
|
||||
type="number"
|
||||
step="any"
|
||||
formControlName="latitude"
|
||||
placeholder="0.000000"
|
||||
/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Longitude</label>
|
||||
<div z-form-control [errorMessage]="error('longitude') || ''">
|
||||
<input
|
||||
z-input
|
||||
type="number"
|
||||
step="any"
|
||||
formControlName="longitude"
|
||||
placeholder="0.000000"
|
||||
/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Statut</label>
|
||||
<div z-form-control [errorMessage]="error('statut') || ''">
|
||||
<z-select formControlName="statut">
|
||||
<z-select-item *ngFor="let statut of pointsVenteStatuts" [zValue]="statut.value">{{ statut.label }}</z-select-item>
|
||||
</z-select>
|
||||
</div>
|
||||
</z-form-field>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PointsVenteForm } from './points-vente-form';
|
||||
|
||||
describe('PointsVenteForm', () => {
|
||||
let component: PointsVenteForm;
|
||||
let fixture: ComponentFixture<PointsVenteForm>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PointsVenteForm]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PointsVenteForm);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
126
src/app/shared/forms/points-vente-form/points-vente-form.ts
Normal file
126
src/app/shared/forms/points-vente-form/points-vente-form.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
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 { PointVente, PointVenteStatut } from 'src/app/core/interfaces/points-ventes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-points-vente-form',
|
||||
standalone: true,
|
||||
templateUrl: './points-vente-form.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
ZardFormModule,
|
||||
ZardInputDirective,
|
||||
ZardSelectComponent,
|
||||
ZardSelectItemComponent,
|
||||
],
|
||||
})
|
||||
export class PointsVenteForm {
|
||||
@Output() save = new EventEmitter<Partial<PointVente>>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
pointsVenteStatuts = [{
|
||||
label: 'Actif',
|
||||
value: 'ACTIF'
|
||||
},
|
||||
{
|
||||
label: 'Inactif',
|
||||
value: 'INACTIF'
|
||||
}
|
||||
]
|
||||
|
||||
private _value?: PointVente;
|
||||
@Input() set value(v: PointVente | undefined) {
|
||||
this._value = v;
|
||||
this.hydrate(v);
|
||||
}
|
||||
get value(): PointVente | undefined {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
form: FormGroup;
|
||||
submitted = false;
|
||||
|
||||
constructor(private fb: FormBuilder) {
|
||||
this.form = this.fb.group({
|
||||
nom: ['', Validators.required],
|
||||
adresse: ['', Validators.required],
|
||||
code: ['', Validators.required],
|
||||
ville: ['', Validators.required],
|
||||
latitude: [0, [Validators.required, Validators.min(-90), Validators.max(90)]],
|
||||
longitude: [0, [Validators.required, Validators.min(-180), Validators.max(180)]],
|
||||
statut: ['', Validators.required],
|
||||
});
|
||||
}
|
||||
|
||||
error(control: string): string {
|
||||
const c = this.form.get(control);
|
||||
if (!c) return '';
|
||||
const invalid = c.invalid && (c.touched || this.submitted);
|
||||
if (!invalid) return '';
|
||||
const e = c.errors || {};
|
||||
if (e['required']) return 'Ce champ est obligatoire';
|
||||
if (e['min']) return `Valeur minimale: ${e['min'].min}`;
|
||||
if (e['max']) return `Valeur maximale: ${e['max'].max}`;
|
||||
return 'Valeur invalide';
|
||||
}
|
||||
|
||||
private hydrate(v?: PointVente) {
|
||||
if (!v) {
|
||||
this.form.reset({
|
||||
nom: '',
|
||||
adresse: '',
|
||||
ville: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
statut: 'ACTIF',
|
||||
});
|
||||
this.submitted = false;
|
||||
return;
|
||||
}
|
||||
this.form.reset({
|
||||
nom: v.nom,
|
||||
adresse: v.adresse,
|
||||
ville: v.ville,
|
||||
latitude: v.latitude ?? 0,
|
||||
longitude: v.longitude ?? 0,
|
||||
statut: v.statut ?? 'ACTIF',
|
||||
});
|
||||
this.submitted = false;
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
this.submitted = true;
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
const raw = this.form.getRawValue() as any;
|
||||
const payload: Partial<PointVente> = {
|
||||
...raw,
|
||||
latitude: Number(raw.latitude),
|
||||
longitude: Number(raw.longitude),
|
||||
...(this.value?.id ? { id: this.value.id } : {}),
|
||||
};
|
||||
this.save.emit(payload);
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
this._value = undefined;
|
||||
this.form.reset({
|
||||
nom: '',
|
||||
adresse: '',
|
||||
ville: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
statut: 'ACTIVE',
|
||||
});
|
||||
this.submitted = false;
|
||||
}
|
||||
}
|
||||
@@ -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, ResultatStatut } from 'src/app/core/interfaces/resultat';
|
||||
import { Resultat } 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((): ResultatStatut => {
|
||||
return this.resultat ? this.resultat.statut : ResultatStatut.EN_ATTENTE;
|
||||
});
|
||||
statut = computed((): 'PROVISOIRE' | 'OFFICIEL' | 'ANNULE' | 'EN_ATTENTE' => {
|
||||
return this.resultat ? 'PROVISOIRE' : 'EN_ATTENTE';
|
||||
});
|
||||
|
||||
canValidate(): boolean {
|
||||
return String(this.statut()) === "EN_ATTENTE";
|
||||
return this.statut() === 'EN_ATTENTE';
|
||||
}
|
||||
|
||||
canConfirm(): boolean {
|
||||
return String(this.statut()) === "PROVISOIRE";
|
||||
return this.statut() === 'PROVISOIRE';
|
||||
}
|
||||
|
||||
// Helper methods for template
|
||||
|
||||
@@ -1,60 +1,110 @@
|
||||
<form class="space-y-4" (ngSubmit)="onSubmit()" [formGroup]="form">
|
||||
<form class="space-y-4" [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>IMEI</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('imei') || ''">
|
||||
<input z-input formControlName="imei" placeholder="IMEI" />
|
||||
<label z-form-label>Numéro de série</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('numeroSerie') || ''">
|
||||
<input z-input formControlName="numeroSerie" placeholder="Numéro de série" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>N° de Série</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('serial') || ''">
|
||||
<input z-input formControlName="serial" placeholder="Numéro de série" />
|
||||
<label z-form-label>Type de terminal</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('typeTerminal') || ''">
|
||||
<input z-input formControlName="typeTerminal" zPlaceholder="Sélectionner..."/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Type</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('type') || ''">
|
||||
<z-select formControlName="type" [zPlaceholder]="'Sélectionner...'">
|
||||
@for (t of types; track t.value) {
|
||||
<z-select-item [zValue]="t.value">{{ t.label }}</z-select-item>
|
||||
<label z-form-label>Plateforme</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('plateforme') || ''">
|
||||
<input z-input formControlName="plateforme"/>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Modèle appareil</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('modeleAppareil') || ''">
|
||||
<input z-input formControlName="modeleAppareil" placeholder="Modèle" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Système d’exploitation</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('systemeExploitation') || ''">
|
||||
<z-select formControlName="systemeExploitation">
|
||||
@for (os of osList; track os) {
|
||||
<z-select-item [zValue]="os">{{ os }}</z-select-item>
|
||||
}
|
||||
</z-select>
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Version OS</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('versionOs') || ''">
|
||||
<input z-input formControlName="versionOs" placeholder="Ex: 12.0" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Version logicielle</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('versionLogicielle') || ''">
|
||||
<input z-input formControlName="versionLogicielle" placeholder="Ex: v3.2.1" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Adresse IP</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('adresseIp') || ''">
|
||||
<input z-input formControlName="adresseIp" placeholder="192.168.1.10" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Adresse MAC</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('adresseMac') || ''">
|
||||
<input z-input formControlName="adresseMac" placeholder="AA:BB:CC:DD:EE:FF" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Point de vente</label>
|
||||
<input [value]="pointDeVenteText()" (click)="onClose()" (input)="pointDeVenteTextChange($event)" z-input placeholder="Entrez le nom du point de vente"/>
|
||||
<div z-form-control [errorMessage]="errorMessage('pointDeVenteId') || ''">
|
||||
@if (pointDeVenteLoading()){
|
||||
<div class="flex items-center justify-center">
|
||||
<div
|
||||
class="w-6 h-6 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
}
|
||||
@if (pointsDevente.length > 0 && !pointDeVenteLoading()) {
|
||||
<ul class="mt-1 border rounded-lg bg-white dark:bg-indigo-950 shadow max-h-60 overflow-auto">
|
||||
@for (pdv of pointsDevente; track pdv.id) {
|
||||
<li
|
||||
class="px-3 py-2 cursor-pointer hover:bg-blue-500"
|
||||
(click)="selectPointDeVente(pdv)"
|
||||
>
|
||||
{{ pdv.nom }} / {{ pdv.code }}
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</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>
|
||||
<div z-form-control [errorMessage]="errorMessage('statut') || ''">
|
||||
<z-select formControlName="statut">
|
||||
@for (s of statuts; track s) {
|
||||
<z-select-item [zValue]="s.value">{{ s.label }}</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') || ''">
|
||||
<input z-input formControlName="marque" placeholder="Marque" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
|
||||
<z-form-field>
|
||||
<label z-form-label>Modèle</label>
|
||||
<div z-form-control [errorMessage]="errorMessage('modele') || ''">
|
||||
<input z-input formControlName="modele" placeholder="Modèle" />
|
||||
</div>
|
||||
</z-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, effect, EventEmitter, Input, Output, signal } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
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, TpeStatus, TpeType } from 'src/app/core/interfaces/tpe';
|
||||
import { TpeDevice, TpeType } from 'src/app/core/interfaces/tpe';
|
||||
import { PointsVenteService } from 'src/app/core/services/points-vente';
|
||||
import { ListParams } from '@shared/paging/paging';
|
||||
import { PointVente } from 'src/app/core/interfaces/points-ventes';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tpe-form',
|
||||
@@ -27,6 +30,13 @@ export class TpeForm {
|
||||
|
||||
private _value?: TpeDevice;
|
||||
private _skipHydration = false;
|
||||
|
||||
|
||||
pointDeVenteText = signal('');
|
||||
|
||||
pointDeVenteLoading = signal<boolean>(false);
|
||||
|
||||
|
||||
@Input() set value(v: TpeDevice | undefined) {
|
||||
this._value = v;
|
||||
if (!this._skipHydration) {
|
||||
@@ -37,21 +47,89 @@ export class TpeForm {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
osList = ['Windows', 'Linux', 'Mac Os'];
|
||||
|
||||
pointsDevente:PointVente[] = [];
|
||||
|
||||
statuts = [
|
||||
{
|
||||
label: 'Actif',
|
||||
value: 'ACTIF'
|
||||
},
|
||||
{
|
||||
label: 'Hors service',
|
||||
value: 'HORS_SERVICE'
|
||||
}
|
||||
]
|
||||
|
||||
form: FormGroup;
|
||||
submitted = false;
|
||||
|
||||
constructor(private fb: FormBuilder) {
|
||||
constructor(private fb: FormBuilder, private pointsVenteService: PointsVenteService) {
|
||||
this.form = this.fb.group({
|
||||
imei: ['', [Validators.required, Validators.minLength(10)]],
|
||||
serial: ['', Validators.required],
|
||||
type: ['POS' as TpeType, Validators.required],
|
||||
marque: ['', Validators.required],
|
||||
modele: ['', Validators.required],
|
||||
statut: ['VALIDE' as TpeStatus, Validators.required],
|
||||
assigne: [false],
|
||||
numeroSerie: ['', [Validators.required, Validators.minLength(10)]],
|
||||
pointDeVenteId: ['', Validators.required],
|
||||
statut: ['', Validators.required],
|
||||
versionLogicielle: ['', Validators.required],
|
||||
typeTerminal: ['', Validators.required],
|
||||
plateforme: ['', Validators.required],
|
||||
modeleAppareil: ['', Validators.required],
|
||||
systemeExploitation: ['', Validators.required],
|
||||
versionOs: ['', Validators.required],
|
||||
adresseIp: ['', Validators.required],
|
||||
adresseMac: ['', Validators.required],
|
||||
agentConnecteId: ['1'],
|
||||
journalSession: ['1'],
|
||||
});
|
||||
|
||||
effect(()=>{
|
||||
const text = this.pointDeVenteText();
|
||||
const params: ListParams = {
|
||||
page: 0,
|
||||
size: 10,
|
||||
search: text
|
||||
}
|
||||
this.getPointDeventeFromText(text, params);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
pointDeVenteTextChange =(event: Event)=>{
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
this.pointDeVenteText.set(value);
|
||||
}
|
||||
|
||||
getPointDeventeFromText=(text: string, params: ListParams)=>{
|
||||
if(text.length < 2) return;
|
||||
this.pointDeVenteLoading.set(true);
|
||||
this.pointsVenteService.list(params).subscribe({
|
||||
next: res =>{
|
||||
this.pointsDevente = res.content;
|
||||
this.pointDeVenteLoading.set(false);
|
||||
},
|
||||
error: err =>{
|
||||
this.pointDeVenteLoading.set(false);
|
||||
return console.error(err);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
selectPointDeVente =(pvt:PointVente):void=>{
|
||||
this.form.patchValue({
|
||||
pointDeVenteId: pvt.id
|
||||
})
|
||||
this.pointsDevente = [];
|
||||
this.pointDeVenteText.set(`${pvt.nom}/${pvt.code} `)
|
||||
}
|
||||
|
||||
onClose(){
|
||||
this.pointsDevente = [];
|
||||
}
|
||||
|
||||
|
||||
isInvalid(control: string): boolean {
|
||||
const c = this.form.get(control);
|
||||
return !!(c && c.invalid && (c.touched || this.submitted));
|
||||
@@ -67,25 +145,37 @@ export class TpeForm {
|
||||
private hydrateFromValue(v?: TpeDevice) {
|
||||
if (!v) {
|
||||
this.form.reset({
|
||||
imei: '',
|
||||
serial: '',
|
||||
type: 'POS',
|
||||
statut: 'VALIDE',
|
||||
assigne: false,
|
||||
marque: '',
|
||||
modele: '',
|
||||
numeroSerie: '',
|
||||
pointDeVenteId: 0,
|
||||
statut: 'ACTIF',
|
||||
versionLogicielle: '',
|
||||
typeTerminal: '',
|
||||
plateforme: '',
|
||||
modeleAppareil: '',
|
||||
systemeExploitation: '',
|
||||
versionOs: '',
|
||||
adresseIp: '',
|
||||
adresseMac: '',
|
||||
agentConnecteId: '',
|
||||
journalSession: '',
|
||||
});
|
||||
this.submitted = false; // Reset submitted flag when form is cleared
|
||||
return;
|
||||
}
|
||||
this.form.reset({
|
||||
imei: v.imei,
|
||||
serial: v.serial,
|
||||
type: v.type,
|
||||
statut: v.statut,
|
||||
assigne: !!v.assigne,
|
||||
marque: v.marque,
|
||||
modele: v.modele,
|
||||
numeroSerie: v.numeroSerie,
|
||||
pointDeVenteId: v.pointDeVenteId,
|
||||
statut: 'ACTIF',
|
||||
versionLogicielle: v.versionLogicielle,
|
||||
typeTerminal: v.typeTerminal,
|
||||
plateforme: v.plateforme,
|
||||
modeleAppareil: v.modeleAppareil,
|
||||
systemeExploitation: v.systemeExploitation,
|
||||
versionOs: v.versionOs,
|
||||
adresseIp: v.adresseIp,
|
||||
adresseMac: v.adresseMac,
|
||||
agentConnecteId: v.agentConnecteId,
|
||||
journalSession: v.journalSession,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,20 +185,16 @@ export class TpeForm {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
const raw = this.form.getRawValue() as any;
|
||||
const raw = this.form.getRawValue() as Partial<TpeDevice>;
|
||||
const payload: Partial<TpeDevice> = {
|
||||
imei: raw.imei,
|
||||
serial: raw.serial,
|
||||
type: raw.type,
|
||||
statut: raw.statut,
|
||||
assigne: !!raw.assigne,
|
||||
marque: raw.marque,
|
||||
modele: raw.modele,
|
||||
...raw
|
||||
};
|
||||
// Preserve existing id, statut, and assigne if editing
|
||||
if (this.value?.id) {
|
||||
payload.id = this.value.id;
|
||||
}
|
||||
|
||||
|
||||
this.save.emit(payload as TpeDevice);
|
||||
}
|
||||
|
||||
@@ -116,11 +202,19 @@ export class TpeForm {
|
||||
this._skipHydration = true; // Prevent hydration when clearing value
|
||||
this._value = undefined;
|
||||
this.form.reset({
|
||||
imei: '',
|
||||
serial: '',
|
||||
type: 'POS',
|
||||
marque: '',
|
||||
modele: '',
|
||||
numeroSerie: '',
|
||||
pointDeVenteId: 0,
|
||||
statut: 'ACTIF',
|
||||
versionLogicielle: '',
|
||||
typeTerminal: '',
|
||||
plateforme: '',
|
||||
modeleAppareil: '',
|
||||
systemeExploitation: '',
|
||||
versionOs: '',
|
||||
adresseIp: '',
|
||||
adresseMac: '',
|
||||
agentConnecteId: '',
|
||||
journalSession: '',
|
||||
});
|
||||
this.submitted = false;
|
||||
this._skipHydration = false; // Re-enable hydration
|
||||
@@ -130,16 +224,4 @@ 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,5 +1,5 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'https://cuddly-years-work.loca.lt',
|
||||
apiBaseUrl: 'http://192.168.40.225:8080',
|
||||
depouillementBaseUrl: 'http://192.168.1.235:8383'
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user