1 Commits

Author SHA1 Message Date
OnlyPapy98
95095016d2 agent creation 2026-01-05 14:14:41 +01:00
11 changed files with 126 additions and 324 deletions

View File

@@ -5,7 +5,7 @@ export type AgentStatus = 'ACTIF' | 'INACTIF' | 'SUSPENDU';
export interface Agent { export interface Agent {
id: string; id: string;
code: string; code: string;
profile: string; // ex. AGENT, SUPERVISEUR, CAISSIER profil: string; // ex. AGENT, SUPERVISEUR, CAISSIER
principalCode?: string; // Agent principal principalCode?: string; // Agent principal
caisseProfile?: string; caisseProfile?: string;
statut: AgentStatus; statut: AgentStatus;

View File

@@ -7,6 +7,7 @@ import { TpeDevice, TpeStatus, TpeType } from '../interfaces/tpe';
import { environment } from 'src/environments/environment.development'; import { environment } from 'src/environments/environment.development';
import { normalizePage } from '@shared/paging/normalize-page'; import { normalizePage } from '@shared/paging/normalize-page';
import { ListParams, PagedResult } from '@shared/paging/paging'; import { ListParams, PagedResult } from '@shared/paging/paging';
import { ServicesUtils } from './services-utils';
const USE_SERVER = true; const USE_SERVER = true;
const API_BASE = '/api/agents'; const API_BASE = '/api/agents';
@@ -31,7 +32,7 @@ interface TpeApiResponse {
interface AgentApiResponse { interface AgentApiResponse {
id: number; id: number;
code: string; code: string;
profile: string; profil: string;
principalCode?: string; principalCode?: string;
caisseProfile?: string; caisseProfile?: string;
statut: string; statut: string;
@@ -75,7 +76,7 @@ interface AgentApiResponse {
export class AgentService { export class AgentService {
private apiUrl = environment.apiBaseUrl + API_BASE; private apiUrl = environment.apiBaseUrl + API_BASE;
constructor(private http: HttpClient) {} constructor(private http: HttpClient, private serviceUtils:ServicesUtils) {}
// Helper method to get ngrok bypass headers // Helper method to get ngrok bypass headers
private getNgrokHeaders(): Record<string, string> { private getNgrokHeaders(): Record<string, string> {
@@ -131,7 +132,7 @@ export class AgentService {
return { return {
id: String(apiAgent.id), id: String(apiAgent.id),
code: apiAgent.code, code: apiAgent.code,
profile: apiAgent.profile, profil: apiAgent.profil,
principalCode: apiAgent.principalCode, principalCode: apiAgent.principalCode,
caisseProfile: apiAgent.caisseProfile, caisseProfile: apiAgent.caisseProfile,
statut: apiAgent.statut as AgentStatus, statut: apiAgent.statut as AgentStatus,
@@ -193,7 +194,7 @@ export class AgentService {
private transformToApiPayload(agent: Partial<Agent>): any { private transformToApiPayload(agent: Partial<Agent>): any {
const payload: any = {}; const payload: any = {};
if (agent.code !== undefined) payload.code = agent.code; if (agent.code !== undefined) payload.code = agent.code;
if (agent.profile !== undefined) payload.profile = agent.profile; if (agent.profil !== undefined) payload.profil = agent.profil;
if (agent.principalCode !== undefined) payload.principalCode = agent.principalCode; if (agent.principalCode !== undefined) payload.principalCode = agent.principalCode;
if (agent.caisseProfile !== undefined) payload.caisseProfile = agent.caisseProfile; if (agent.caisseProfile !== undefined) payload.caisseProfile = agent.caisseProfile;
if (agent.statut !== undefined) payload.statut = agent.statut; if (agent.statut !== undefined) payload.statut = agent.statut;
@@ -233,22 +234,6 @@ export class AgentService {
if (agent.epoux !== undefined) payload.epoux = agent.epoux; if (agent.epoux !== undefined) payload.epoux = agent.epoux;
if (agent.autreTelephone !== undefined) payload.autreTelephone = agent.autreTelephone; if (agent.autreTelephone !== undefined) payload.autreTelephone = agent.autreTelephone;
if (agent.createdBy !== undefined) payload.createdBy = agent.createdBy; if (agent.createdBy !== undefined) payload.createdBy = agent.createdBy;
// Include tpes if provided - transform to API format
if (agent.tpes !== undefined) {
payload.tpes = agent.tpes.map((tpe) => ({
id: tpe.id ? Number(tpe.id) : undefined,
imei: tpe.imei,
serial: tpe.serial,
type: tpe.type,
marque: tpe.marque,
modele: tpe.modele,
statut: tpe.statut,
agent: undefined, // Will be set by backend
assigne: tpe.assigne,
createdAt: tpe.createdAt,
updatedAt: tpe.updatedAt,
}));
}
return payload; return payload;
} }
@@ -269,23 +254,23 @@ export class AgentService {
} }
// GET /api/v1/agents - List all // GET /api/v1/agents - List all
list(params?: ListParams): Observable<PagedResult<Agent>> { list(params: ListParams): Observable<PagedResult<Agent>> {
let httpParams = new HttpParams(); // let httpParams = new HttpParams();
if (params) { // if (params) {
if (params.page) httpParams = httpParams.set('page', params.page.toString()); // if (params.page) httpParams = httpParams.set('page', params.page.toString());
if (params.size) httpParams = httpParams.set('perPage', params.size.toString()); // if (params.size) httpParams = httpParams.set('perPage', params.size.toString());
if (params.search) httpParams = httpParams.set('search', params.search); // if (params.search) httpParams = httpParams.set('search', params.search);
if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey); // if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey);
if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir); // if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir);
} // }
return this.http return this.http
.get<PagedResult<AgentApiResponse>>(this.apiUrl, { .get<PagedResult<AgentApiResponse>>(this.apiUrl, {
params: httpParams, params: this.serviceUtils.getParamsFromModel(params),
headers: this.getNgrokHeaders(), headers: this.getNgrokHeaders(),
}) })
.pipe( .pipe(
map((res) => { map((res) => {
console.log(res);
const agents = res.content.map((apiAgent) => { const agents = res.content.map((apiAgent) => {
const transformed = this.transformAgent(apiAgent); const transformed = this.transformAgent(apiAgent);
return transformed; return transformed;
@@ -305,9 +290,8 @@ export class AgentService {
); );
} }
// POST /api/v1/agents - Create // POST /api/agents - Create
create(payload: Omit<Agent, 'id' | 'createdAt' | 'updatedAt'>): Observable<Agent> { create(payload: Omit<Agent, 'id' | 'createdAt' | 'updatedAt'>): Observable<Agent> {
if (USE_SERVER) {
const apiPayload = this.transformToApiPayload(payload); const apiPayload = this.transformToApiPayload(payload);
return this.http return this.http
.post<AgentApiResponse>(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() }) .post<AgentApiResponse>(this.apiUrl, apiPayload, { headers: this.getNgrokHeaders() })
@@ -319,8 +303,6 @@ export class AgentService {
}) })
); );
} }
throw new Error('Server mode is required');
}
// PUT /api/v1/agents/{id} - Update // PUT /api/v1/agents/{id} - Update
update(id: string, payload: Partial<Agent>): Observable<Agent | undefined> { update(id: string, payload: Partial<Agent>): Observable<Agent | undefined> {

View File

@@ -127,7 +127,7 @@ export class TpeService {
return { return {
id: String(apiAgent.id), id: String(apiAgent.id),
code: String((apiAgent as any).code || ''), code: String((apiAgent as any).code || ''),
profile: String((apiAgent as any).profile || ''), profil: String((apiAgent as any).profil || ''),
principalCode: (apiAgent as any).principalCode ? String((apiAgent as any).principalCode) : undefined, principalCode: (apiAgent as any).principalCode ? String((apiAgent as any).principalCode) : undefined,
caisseProfile: (apiAgent as any).caisseProfile ? String((apiAgent as any).caisseProfile) : undefined, caisseProfile: (apiAgent as any).caisseProfile ? String((apiAgent as any).caisseProfile) : undefined,
statut: apiAgent.statut as AgentStatus, statut: apiAgent.statut as AgentStatus,

View File

@@ -29,7 +29,7 @@
[total]="total()" [total]="total()"
[page]="page()" [page]="page()"
[perPage]="size()" [perPage]="size()"
(pageChange)="page.set($event)" (pageChange)="page.set($event - 1)"
(perPageChange)="size.set($event)" (perPageChange)="size.set($event)"
></app-paginator> ></app-paginator>
</div> </div>
@@ -37,6 +37,7 @@
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xxl"> <app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xxl">
<app-agent-full-form <app-agent-full-form
[value]="editingItem() ?? undefined" [value]="editingItem() ?? undefined"
[compact]="!editingItem()"
(save)="onFormSave($event)" (save)="onFormSave($event)"
(cancel)="closeModal()" (cancel)="closeModal()"
/> />
@@ -61,7 +62,7 @@
</div> </div>
<div> <div>
<div class="text-xs text-muted-foreground mb-1">Profil</div> <div class="text-xs text-muted-foreground mb-1">Profil</div>
<div class="font-medium">{{ agent.profile }}</div> <div class="font-medium">{{ agent.profil }}</div>
</div> </div>
<div> <div>
<div class="text-xs text-muted-foreground mb-1">Statut</div> <div class="text-xs text-muted-foreground mb-1">Statut</div>

View File

@@ -25,6 +25,7 @@ import { TpeDevice, TpeStatus } from 'src/app/core/interfaces/tpe';
import { AgentFullForm } from '@shared/forms/agent-full-form/agent-full-form'; import { AgentFullForm } from '@shared/forms/agent-full-form/agent-full-form';
import { forkJoin, of } from 'rxjs'; import { forkJoin, of } from 'rxjs';
import { switchMap, catchError } from 'rxjs/operators'; import { switchMap, catchError } from 'rxjs/operators';
import { toast } from 'ngx-sonner';
@Component({ @Component({
standalone: true, standalone: true,
@@ -50,7 +51,7 @@ export class AgentsPage {
total = signal(0); total = signal(0);
loading = signal(false); loading = signal(false);
page = signal(1); page = signal(0);
size = signal(10); size = signal(10);
search = signal(''); search = signal('');
sort = signal<SortState>({ key: 'code', dir: 'asc' }); sort = signal<SortState>({ key: 'code', dir: 'asc' });
@@ -90,7 +91,7 @@ export class AgentsPage {
cols: TableColumn<Agent>[] = [ cols: TableColumn<Agent>[] = [
{ key: 'code', label: 'Code', sortable: true, defaultVisible: true }, { key: 'code', label: 'Code', sortable: true, defaultVisible: true },
{ key: 'nomPrenom', label: 'Nom complet', sortable: true, defaultVisible: true, cell: (a) => `${a.nom} ${a.prenom}` }, { key: 'nomPrenom', label: 'Nom complet', sortable: true, defaultVisible: true, cell: (a) => `${a.nom} ${a.prenom}` },
{ key: 'profile', label: 'Profil', sortable: true, defaultVisible: true }, { key: 'profil', label: 'Profil', sortable: true, defaultVisible: true },
{ key: 'statut', label: 'Statut', sortable: true, defaultVisible: true, cell: (a) => this.renderStatutBadge(a.statut) }, { key: 'statut', label: 'Statut', sortable: true, defaultVisible: true, cell: (a) => this.renderStatutBadge(a.statut) },
{ key: 'phone', label: 'Téléphone', sortable: true, defaultVisible: true }, { key: 'phone', label: 'Téléphone', sortable: true, defaultVisible: true },
{ key: 'zone', label: 'Zone', sortable: true }, { key: 'zone', label: 'Zone', sortable: true },
@@ -260,6 +261,7 @@ export class AgentsPage {
onFormSave(payload: Partial<Agent>) { onFormSave(payload: Partial<Agent>) {
const current = this.editingItem(); const current = this.editingItem();
const isCreating = !current?.id; // Mode création (compact)
const familyMembersData = this.formComp?.getFamilyMembersData() || []; const familyMembersData = this.formComp?.getFamilyMembersData() || [];
// Save agent first // Save agent first
@@ -280,7 +282,12 @@ export class AgentsPage {
throw new Error("Impossible d'obtenir l'ID de l'agent sauvegardé"); throw new Error("Impossible d'obtenir l'ID de l'agent sauvegardé");
} }
// Get existing family members for this agent // In creation mode (compact), skip family members management
if (isCreating || familyMembersData.length === 0) {
return of([]);
}
// Get existing family members for this agent (only for updates)
return this.familyMemberService.getByAgentId(savedAgentId).pipe( return this.familyMemberService.getByAgentId(savedAgentId).pipe(
switchMap((existingMembers) => { switchMap((existingMembers) => {
const existingIds = new Set(existingMembers.map((m) => m.id)); const existingIds = new Set(existingMembers.map((m) => m.id));
@@ -354,12 +361,13 @@ export class AgentsPage {
) )
.subscribe({ .subscribe({
next: () => { next: () => {
toast.success('Agent sauvegardé avec succès');
// Close modal first
this.closeModal();
// Reset form after successful save // Reset form after successful save
this.formComp?.resetForm(); this.formComp?.resetForm();
// Clear editing item // Clear editing item
this.editingItem.set(null); this.editingItem.set(null);
// Close modal
this.closeModal();
// Refresh data // Refresh data
this.fetch({ this.fetch({
page: this.page(), page: this.page(),

View File

@@ -6,7 +6,7 @@
</z-form-field> </z-form-field>
<z-form-field> <z-form-field>
<label z-form-label>Profil</label> <label z-form-label>Profil</label>
<div z-form-control [errorMessage]="error('profile') || ''"><input z-input formControlName="profile" /></div> <div z-form-control [errorMessage]="error('profil') || ''"><input z-input formControlName="profil" /></div>
</z-form-field> </z-form-field>
<z-form-field> <z-form-field>

View File

@@ -33,7 +33,7 @@ export class AgentForm {
constructor(private fb: FormBuilder, private limitService: AgentLimitService) { constructor(private fb: FormBuilder, private limitService: AgentLimitService) {
this.form = this.fb.group({ this.form = this.fb.group({
code: ['', Validators.required], code: ['', Validators.required],
profile: ['', Validators.required], profil: ['', Validators.required],
statut: ['ACTIF' as AgentStatus, Validators.required], statut: ['ACTIF' as AgentStatus, Validators.required],
zone: [''], zone: [''],
kiosk: [''], kiosk: [''],
@@ -66,12 +66,12 @@ export class AgentForm {
private hydrateFromValue(v?: Agent) { private hydrateFromValue(v?: Agent) {
if (!v) { if (!v) {
this.form.reset({ this.form.reset({
code: '', profile: '', statut: 'ACTIF', zone: '', kiosk: '', fonction: '', dateEmbauche: '', nom: '', prenom: '', phone: '', limiteInferieure: 0, limiteSuperieure: 0, limiteParTransaction: 0, limiteMinAirtime: 0, limiteMaxAirtime: 0, maxPeripheriques: 0, limitId: '', code: '', profil: '', statut: 'ACTIF', zone: '', kiosk: '', fonction: '', dateEmbauche: '', nom: '', prenom: '', phone: '', limiteInferieure: 0, limiteSuperieure: 0, limiteParTransaction: 0, limiteMinAirtime: 0, limiteMaxAirtime: 0, maxPeripheriques: 0, limitId: '',
}); });
return; return;
} }
this.form.reset({ this.form.reset({
code: v.code, profile: v.profile, 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, 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 ?? '',
}); });
} }
@@ -79,7 +79,7 @@ export class AgentForm {
this.submitted = true; 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 raw = this.form.getRawValue() as any;
const payload: Agent = { id: this.value?.id ?? '', code: raw.code, profile: raw.profile, 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, 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 };
this.save.emit(payload); this.save.emit(payload);
} }
} }

View File

@@ -1,5 +1,5 @@
<form class="space-y-6" (ngSubmit)="onSubmit()" [formGroup]="form"> <form class="space-y-6" (ngSubmit)="onSubmit()" [formGroup]="form">
<div class="grid grid-cols-1 xl:grid-cols-3 gap-4"> <div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
<z-card class="p-4 space-y-3"> <z-card class="p-4 space-y-3">
<div class="text-lg font-semibold">Informations Emploi</div> <div class="text-lg font-semibold">Informations Emploi</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
@@ -10,8 +10,8 @@
></z-form-field> ></z-form-field>
<z-form-field <z-form-field
><label z-form-label>Profil</label> ><label z-form-label>Profil</label>
<div z-form-control [errorMessage]="error('profile') || ''"> <div z-form-control [errorMessage]="error('profil') || ''">
<input z-input formControlName="profile" /></div <input z-input formControlName="profil" /></div
></z-form-field> ></z-form-field>
<z-form-field class="md:col-span-2" <z-form-field class="md:col-span-2"
><label z-form-label>Agent Principal</label> ><label z-form-label>Agent Principal</label>
@@ -94,88 +94,6 @@
></z-form-field> ></z-form-field>
</div> </div>
</z-card> </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>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4"> <div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
@@ -220,14 +138,56 @@
></z-form-field> ></z-form-field>
</div> </div>
</z-card> </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="flex items-center justify-between mb-2">
<div class="text-lg font-semibold">Membres de famille</div> <div class="text-lg font-semibold">Membres de famille</div>
<z-button zType="default" (click)="addFamily()"> <z-button zType="default" (click)="addFamily()">
<i class="icon-plus mr-2"></i>Ajouter un membre <i class="icon-plus mr-2"></i>Ajouter un membre
</z-button> </z-button>
</div> </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) { @for (fm of famille.controls; track $index; let i = $index) {
<div <div
@@ -307,62 +267,13 @@
</div> </div>
} }
</div> </div>
</z-card>
</div>
<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>
<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 { } @else {
<button z-button zType="destructive" (click)="onToggleTpe(row.id, false)"> <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>
<i class="icon-trash"></i>
</button>
} }
</ng-template> </z-card> -->
</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> </div>
</app-modal>
<!-- TPE assignment UI removed from this form -->
<!-- TPE assignment modal removed from this form; TPE assignment is handled elsewhere -->
</form> </form>

View File

@@ -7,16 +7,12 @@ import { ZardSelectComponent } from '@shared/components/select/select.component'
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component'; import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
import { ZardButtonComponent } from '@shared/components/button/button.component'; import { ZardButtonComponent } from '@shared/components/button/button.component';
import { ZardCardComponent } from '@shared/components/card/card.component'; import { ZardCardComponent } from '@shared/components/card/card.component';
import { DataTable, SortState, TableColumn } from '@shared/components/data-table/data-table'; // DataTable removed from this form (TPE UI was removed)
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 { Agent, AgentFamilyMember } from 'src/app/core/interfaces/agent';
import { AgentLimit } from 'src/app/core/interfaces/agent-limit'; import { AgentLimit } from 'src/app/core/interfaces/agent-limit';
import { AgentLimitService } from 'src/app/core/services/agent-limit'; import { AgentLimitService } from 'src/app/core/services/agent-limit';
import { AgentFamilyMemberService } from 'src/app/core/services/agent-family-member'; import { AgentFamilyMemberService } from 'src/app/core/services/agent-family-member';
import { TpeDevice } from 'src/app/core/interfaces/tpe'; // TPE assignment removed from form (handled elsewhere)
import { TpeService } from 'src/app/core/services/tpe';
@Component({ @Component({
selector: 'app-agent-full-form', selector: 'app-agent-full-form',
@@ -30,36 +26,18 @@ import { TpeService } from 'src/app/core/services/tpe';
ZardInputDirective, ZardInputDirective,
ZardSelectComponent, ZardSelectComponent,
ZardSelectItemComponent, ZardSelectItemComponent,
ZardButtonComponent,
ZardCardComponent, ZardCardComponent,
DataTable, // TPE UI components removed from this form
Paginator,
SearchBar,
Modal,
], ],
}) })
export class AgentFullForm { export class AgentFullForm {
@Output() save = new EventEmitter<Agent>(); @Output() save = new EventEmitter<Partial<Agent>>();
@Output() cancel = new EventEmitter<void>(); @Output() cancel = new EventEmitter<void>();
@Input() compact = false; // when true, hide family and TPE sections for a shorter creation form
limits: AgentLimit[] = []; limits: AgentLimit[] = [];
selectedLimit: AgentLimit | null = null; selectedLimit: AgentLimit | null = null;
tpes: TpeDevice[] = []; // TPE assignment removed from the creation form; handled in separate flows
// 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; private _value?: Agent;
@Input() set value(v: Agent | undefined) { this._value = v; this.hydrate(v); } @Input() set value(v: Agent | undefined) { this._value = v; this.hydrate(v); }
@@ -71,22 +49,19 @@ export class AgentFullForm {
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
private limitService: AgentLimitService, private limitService: AgentLimitService,
private tpeService: TpeService,
private familyMemberService: AgentFamilyMemberService private familyMemberService: AgentFamilyMemberService
) { ) {
this.form = this.fb.group({ this.form = this.fb.group({
// Emploi // Emploi
code: ['', Validators.required], profile: ['', Validators.required], principalCode: [''], caisseProfile: [''], statut: ['ACTIF', Validators.required], zone: [''], kiosk: [''], fonction: [''], dateEmbauche: [''], code: ['', Validators.required], profil: ['', Validators.required], principalCode: [''], caisseProfile: [''], statut: ['ACTIF', Validators.required], zone: [''], kiosk: [''], fonction: [''], dateEmbauche: [''],
// Perso // Perso
nom: ['', Validators.required], prenom: ['', Validators.required], autresNoms: [''], dateNaissance: [''], lieuNaissance: [''], adresse: [''], ville: [''], autoriserAides: [false], maxPeripheriques: [0, [Validators.min(0)]], nom: ['', Validators.required], prenom: ['', Validators.required], autresNoms: [''], dateNaissance: [''], lieuNaissance: [''], adresse: [''], ville: [''], autoriserAides: [false], maxPeripheriques: [0, [Validators.min(0)]],
// Connexion & limites // Connexion & limites
phone: ['', Validators.required], pin: [''], limiteInferieure: [0], limiteSuperieure: [0], limiteParTransaction: [0], limiteMinAirtime: [0], limiteMaxAirtime: [0], limitId: ['', Validators.required], phone: ['', Validators.required], pin: [''], limiteInferieure: [0], limiteSuperieure: [0], limiteParTransaction: [0], limiteMinAirtime: [0], limiteMaxAirtime: [0], limitId: ['1'],
// Légales // Légales
nationalite: [''], cni: [''], cniDelivreeLe: [''], cniDelivreeA: [''], residence: [''], autreAdresse1: [''], statutMarital: [''], epoux: [''], autreTelephone: [''], nationalite: [''], cni: [''], cniDelivreeLe: [''], cniDelivreeA: [''], residence: [''], autreAdresse1: [''], statutMarital: [''], epoux: [''], autreTelephone: [''],
// Famille // Famille
famille: this.fb.array([]), famille: this.fb.array([]),
// TPE (stored as IDs in form, converted to full objects on submit)
tpeIds: this.fb.array<string>([]),
}); });
this.limitService.list({ page: 1, perPage: 100, search: '', sortKey: 'nom', sortDir: 'asc' } as any).subscribe((res) => { this.limitService.list({ page: 1, perPage: 100, search: '', sortKey: 'nom', sortDir: 'asc' } as any).subscribe((res) => {
@@ -114,14 +89,14 @@ export class AgentFullForm {
this.selectedLimit = defaultLimit; this.selectedLimit = defaultLimit;
} }
}); });
// initial fetch page for TPE modal // TPE fetching/assignment handled outside of this form
this.fetchTpes();
} }
get famille(): FormArray { return this.form.get('famille') as FormArray; } get famille(): FormArray { return this.form.get('famille') as FormArray; }
get tpeArray(): FormArray { return this.form.get('tpeIds') as FormArray; }
addFamily() { addFamily() {
if (this.compact) return; // no family in compact mode
this.famille.push( this.famille.push(
this.fb.group({ this.fb.group({
id: [''], // Will be set when saved id: [''], // Will be set when saved
@@ -143,7 +118,6 @@ export class AgentFullForm {
private hydrate(v?: Agent) { private hydrate(v?: Agent) {
this.famille.clear(); this.famille.clear();
this.tpeArray.clear();
this.selectedLimit = null; this.selectedLimit = null;
if (!v) { if (!v) {
@@ -153,7 +127,7 @@ export class AgentFullForm {
this.form.reset({ this.form.reset({
code: '', code: '',
profile: '', profil: '',
principalCode: '', principalCode: '',
caisseProfile: '', caisseProfile: '',
statut: 'ACTIF', statut: 'ACTIF',
@@ -226,60 +200,10 @@ export class AgentFullForm {
}); });
} }
// Load assigned TPE IDs from tpes array // Assigned TPEs are not handled in this form
(v.tpes ?? []).forEach((tpe) => this.tpeArray.push(this.fb.control(tpe.id)));
} }
onToggleTpe(id: string, checked: boolean) { // TPE assignment / picker removed from this form
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 !== 'VALIDE') return false;
// If TPE is assigned but to this agent, show it
if (t.assigne && 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.assigne;
});
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.imei} (${found.marque} ${found.modele})`;
}
// Try to load from service if not in current list
return id;
}
// === Validation helpers === // === Validation helpers ===
error(control: string): string { error(control: string): string {
@@ -313,36 +237,13 @@ export class AgentFullForm {
return; return;
} }
const raw = this.form.getRawValue() as any; const raw = this.form.getRawValue() as any;
// Prepare partial agent payload (family members are handled separately)
// Convert TPE IDs to full TPE objects const payload: Partial<Agent> = {
const tpeIds = [...this.tpeArray.value] as string[];
const tpes: TpeDevice[] = tpeIds
.map((id) => {
// Try to find in tpeRows first (current modal list)
const found = this.tpeRows.find((t) => t.id === id);
if (found) return found;
// Try to find in existing tpes (from value)
const existing = this.value?.tpes?.find((t) => t.id === id);
if (existing) return existing;
// If not found, create a minimal TPE object (backend will fill in details)
// This shouldn't happen in normal flow, but handle gracefully
return null;
})
.filter((t): t is TpeDevice => t !== null);
// Prepare agent payload (without famille - family members are handled separately)
const payload: Agent = {
...(this.value ?? {}),
id: this.value?.id || '',
...raw, ...raw,
tpes: tpes, ...(this.value?.id ? { id: this.value.id } : {}),
} as Agent; };
// Remove tpeIds from payload (it's not part of Agent interface) // Emit the partial agent payload (parent will decide create vs update)
delete (payload as any).tpeIds;
// Emit the agent payload first
// Family members will be saved separately in the parent component
this.save.emit(payload); this.save.emit(payload);
} }
@@ -362,7 +263,6 @@ export class AgentFullForm {
this._value = undefined; this._value = undefined;
this.selectedLimit = null; this.selectedLimit = null;
this.famille.clear(); this.famille.clear();
this.tpeArray.clear();
// Find default limit to assign it automatically // Find default limit to assign it automatically
const defaultLimit = this.limits.find((l) => l.isDefault); const defaultLimit = this.limits.find((l) => l.isDefault);
@@ -370,7 +270,7 @@ export class AgentFullForm {
this.form.reset({ this.form.reset({
code: '', code: '',
profile: '', profil: '',
principalCode: '', principalCode: '',
caisseProfile: '', caisseProfile: '',
statut: 'ACTIF', statut: 'ACTIF',

View File

@@ -1,5 +1,5 @@
export const environment = { export const environment = {
production: false, production: false,
apiBaseUrl: 'http://192.168.1.235:8381', apiBaseUrl: 'https://cuddly-years-work.loca.lt',
depouillementBaseUrl: 'http://192.168.1.235:8383' depouillementBaseUrl: 'http://192.168.1.235:8383'
}; };

View File

@@ -1,4 +1,4 @@
export const environment = { export const environment = {
production: false, production: false,
apiBaseUrl: 'http://192.168.1.235:8381', apiBaseUrl: 'https://cuddly-years-work.loca.lt',
}; };