agent creation
This commit is contained in:
@@ -5,7 +5,7 @@ export type AgentStatus = 'ACTIF' | 'INACTIF' | 'SUSPENDU';
|
||||
export interface Agent {
|
||||
id: string;
|
||||
code: string;
|
||||
profile: string; // ex. AGENT, SUPERVISEUR, CAISSIER
|
||||
profil: string; // ex. AGENT, SUPERVISEUR, CAISSIER
|
||||
principalCode?: string; // Agent principal
|
||||
caisseProfile?: string;
|
||||
statut: AgentStatus;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { TpeDevice, TpeStatus, TpeType } from '../interfaces/tpe';
|
||||
import { environment } from 'src/environments/environment.development';
|
||||
import { normalizePage } from '@shared/paging/normalize-page';
|
||||
import { ListParams, PagedResult } from '@shared/paging/paging';
|
||||
import { ServicesUtils } from './services-utils';
|
||||
|
||||
const USE_SERVER = true;
|
||||
const API_BASE = '/api/agents';
|
||||
@@ -31,7 +32,7 @@ interface TpeApiResponse {
|
||||
interface AgentApiResponse {
|
||||
id: number;
|
||||
code: string;
|
||||
profile: string;
|
||||
profil: string;
|
||||
principalCode?: string;
|
||||
caisseProfile?: string;
|
||||
statut: string;
|
||||
@@ -75,7 +76,7 @@ interface AgentApiResponse {
|
||||
export class AgentService {
|
||||
private apiUrl = environment.apiBaseUrl + API_BASE;
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
constructor(private http: HttpClient, private serviceUtils:ServicesUtils) {}
|
||||
|
||||
// Helper method to get ngrok bypass headers
|
||||
private getNgrokHeaders(): Record<string, string> {
|
||||
@@ -131,7 +132,7 @@ export class AgentService {
|
||||
return {
|
||||
id: String(apiAgent.id),
|
||||
code: apiAgent.code,
|
||||
profile: apiAgent.profile,
|
||||
profil: apiAgent.profil,
|
||||
principalCode: apiAgent.principalCode,
|
||||
caisseProfile: apiAgent.caisseProfile,
|
||||
statut: apiAgent.statut as AgentStatus,
|
||||
@@ -193,7 +194,7 @@ export class AgentService {
|
||||
private transformToApiPayload(agent: Partial<Agent>): any {
|
||||
const payload: any = {};
|
||||
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.caisseProfile !== undefined) payload.caisseProfile = agent.caisseProfile;
|
||||
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.autreTelephone !== undefined) payload.autreTelephone = agent.autreTelephone;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -269,23 +254,23 @@ export class AgentService {
|
||||
}
|
||||
|
||||
// GET /api/v1/agents - List all
|
||||
list(params?: ListParams): Observable<PagedResult<Agent>> {
|
||||
let httpParams = new HttpParams();
|
||||
if (params) {
|
||||
if (params.page) httpParams = httpParams.set('page', params.page.toString());
|
||||
if (params.size) httpParams = httpParams.set('perPage', params.size.toString());
|
||||
if (params.search) httpParams = httpParams.set('search', params.search);
|
||||
if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey);
|
||||
if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir);
|
||||
}
|
||||
|
||||
list(params: ListParams): Observable<PagedResult<Agent>> {
|
||||
// let httpParams = new HttpParams();
|
||||
// if (params) {
|
||||
// if (params.page) httpParams = httpParams.set('page', params.page.toString());
|
||||
// if (params.size) httpParams = httpParams.set('perPage', params.size.toString());
|
||||
// if (params.search) httpParams = httpParams.set('search', params.search);
|
||||
// if (params.sortKey) httpParams = httpParams.set('sortKey', params.sortKey);
|
||||
// if (params.sortDir) httpParams = httpParams.set('sortDir', params.sortDir);
|
||||
// }
|
||||
return this.http
|
||||
.get<PagedResult<AgentApiResponse>>(this.apiUrl, {
|
||||
params: httpParams,
|
||||
params: this.serviceUtils.getParamsFromModel(params),
|
||||
headers: this.getNgrokHeaders(),
|
||||
})
|
||||
.pipe(
|
||||
map((res) => {
|
||||
console.log(res);
|
||||
const agents = res.content.map((apiAgent) => {
|
||||
const transformed = this.transformAgent(apiAgent);
|
||||
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> {
|
||||
if (USE_SERVER) {
|
||||
const apiPayload = this.transformToApiPayload(payload);
|
||||
return this.http
|
||||
.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
|
||||
update(id: string, payload: Partial<Agent>): Observable<Agent | undefined> {
|
||||
|
||||
@@ -127,7 +127,7 @@ export class TpeService {
|
||||
return {
|
||||
id: String(apiAgent.id),
|
||||
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,
|
||||
caisseProfile: (apiAgent as any).caisseProfile ? String((apiAgent as any).caisseProfile) : undefined,
|
||||
statut: apiAgent.statut as AgentStatus,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
[total]="total()"
|
||||
[page]="page()"
|
||||
[perPage]="size()"
|
||||
(pageChange)="page.set($event)"
|
||||
(pageChange)="page.set($event - 1)"
|
||||
(perPageChange)="size.set($event)"
|
||||
></app-paginator>
|
||||
</div>
|
||||
@@ -37,6 +37,7 @@
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xxl">
|
||||
<app-agent-full-form
|
||||
[value]="editingItem() ?? undefined"
|
||||
[compact]="!editingItem()"
|
||||
(save)="onFormSave($event)"
|
||||
(cancel)="closeModal()"
|
||||
/>
|
||||
@@ -61,7 +62,7 @@
|
||||
</div>
|
||||
<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 class="text-xs text-muted-foreground mb-1">Statut</div>
|
||||
|
||||
@@ -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 { forkJoin, of } from 'rxjs';
|
||||
import { switchMap, catchError } from 'rxjs/operators';
|
||||
import { toast } from 'ngx-sonner';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -50,7 +51,7 @@ export class AgentsPage {
|
||||
total = signal(0);
|
||||
loading = signal(false);
|
||||
|
||||
page = signal(1);
|
||||
page = signal(0);
|
||||
size = signal(10);
|
||||
search = signal('');
|
||||
sort = signal<SortState>({ key: 'code', dir: 'asc' });
|
||||
@@ -90,7 +91,7 @@ export class AgentsPage {
|
||||
cols: TableColumn<Agent>[] = [
|
||||
{ key: 'code', label: 'Code', sortable: true, defaultVisible: true },
|
||||
{ key: 'nomPrenom', label: 'Nom complet', sortable: true, defaultVisible: true, cell: (a) => `${a.nom} ${a.prenom}` },
|
||||
{ key: 'profile', label: 'Profil', sortable: true, defaultVisible: true },
|
||||
{ key: 'profil', label: 'Profil', sortable: true, defaultVisible: true },
|
||||
{ key: 'statut', label: 'Statut', sortable: true, defaultVisible: true, cell: (a) => this.renderStatutBadge(a.statut) },
|
||||
{ key: 'phone', label: 'Téléphone', sortable: true, defaultVisible: true },
|
||||
{ key: 'zone', label: 'Zone', sortable: true },
|
||||
@@ -260,6 +261,7 @@ export class AgentsPage {
|
||||
|
||||
onFormSave(payload: Partial<Agent>) {
|
||||
const current = this.editingItem();
|
||||
const isCreating = !current?.id; // Mode création (compact)
|
||||
const familyMembersData = this.formComp?.getFamilyMembersData() || [];
|
||||
|
||||
// Save agent first
|
||||
@@ -280,7 +282,12 @@ export class AgentsPage {
|
||||
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(
|
||||
switchMap((existingMembers) => {
|
||||
const existingIds = new Set(existingMembers.map((m) => m.id));
|
||||
@@ -354,12 +361,13 @@ export class AgentsPage {
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
toast.success('Agent sauvegardé avec succès');
|
||||
// Close modal first
|
||||
this.closeModal();
|
||||
// Reset form after successful save
|
||||
this.formComp?.resetForm();
|
||||
// Clear editing item
|
||||
this.editingItem.set(null);
|
||||
// Close modal
|
||||
this.closeModal();
|
||||
// Refresh data
|
||||
this.fetch({
|
||||
page: this.page(),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</z-form-field>
|
||||
<z-form-field>
|
||||
<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>
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AgentForm {
|
||||
constructor(private fb: FormBuilder, private limitService: AgentLimitService) {
|
||||
this.form = this.fb.group({
|
||||
code: ['', Validators.required],
|
||||
profile: ['', Validators.required],
|
||||
profil: ['', Validators.required],
|
||||
statut: ['ACTIF' as AgentStatus, Validators.required],
|
||||
zone: [''],
|
||||
kiosk: [''],
|
||||
@@ -66,12 +66,12 @@ export class AgentForm {
|
||||
private hydrateFromValue(v?: Agent) {
|
||||
if (!v) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<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('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 class="md:col-span-2"
|
||||
><label z-form-label>Agent Principal</label>
|
||||
@@ -94,88 +94,6 @@
|
||||
></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">
|
||||
@@ -220,14 +138,56 @@
|
||||
></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">
|
||||
@for (fm of famille.controls; track $index; let i = $index) {
|
||||
<div
|
||||
@@ -307,62 +267,13 @@
|
||||
</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 {
|
||||
<button z-button zType="destructive" (click)="onToggleTpe(row.id, false)">
|
||||
<i class="icon-trash"></i>
|
||||
</button>
|
||||
<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>
|
||||
}
|
||||
</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>
|
||||
</z-card> -->
|
||||
</div>
|
||||
</app-modal>
|
||||
|
||||
<!-- TPE assignment UI removed from this form -->
|
||||
|
||||
<!-- TPE assignment modal removed from this form; TPE assignment is handled elsewhere -->
|
||||
</form>
|
||||
|
||||
@@ -7,16 +7,12 @@ 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';
|
||||
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';
|
||||
// DataTable removed from this form (TPE UI was removed)
|
||||
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';
|
||||
import { TpeDevice } from 'src/app/core/interfaces/tpe';
|
||||
import { TpeService } from 'src/app/core/services/tpe';
|
||||
// TPE assignment removed from form (handled elsewhere)
|
||||
|
||||
@Component({
|
||||
selector: 'app-agent-full-form',
|
||||
@@ -30,36 +26,18 @@ import { TpeService } from 'src/app/core/services/tpe';
|
||||
ZardInputDirective,
|
||||
ZardSelectComponent,
|
||||
ZardSelectItemComponent,
|
||||
ZardButtonComponent,
|
||||
ZardCardComponent,
|
||||
DataTable,
|
||||
Paginator,
|
||||
SearchBar,
|
||||
Modal,
|
||||
// TPE UI components removed from this form
|
||||
],
|
||||
})
|
||||
export class AgentFullForm {
|
||||
@Output() save = new EventEmitter<Agent>();
|
||||
@Output() save = new EventEmitter<Partial<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;
|
||||
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 },
|
||||
];
|
||||
// TPE assignment removed from the creation form; handled in separate flows
|
||||
|
||||
private _value?: Agent;
|
||||
@Input() set value(v: Agent | undefined) { this._value = v; this.hydrate(v); }
|
||||
@@ -71,22 +49,19 @@ 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], 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
|
||||
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: ['', Validators.required],
|
||||
phone: ['', Validators.required], pin: [''], limiteInferieure: [0], limiteSuperieure: [0], limiteParTransaction: [0], limiteMinAirtime: [0], limiteMaxAirtime: [0], limitId: ['1'],
|
||||
// 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)
|
||||
tpeIds: this.fb.array<string>([]),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
// initial fetch page for TPE modal
|
||||
this.fetchTpes();
|
||||
// TPE fetching/assignment handled outside of this form
|
||||
}
|
||||
|
||||
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
|
||||
@@ -143,7 +118,6 @@ export class AgentFullForm {
|
||||
|
||||
private hydrate(v?: Agent) {
|
||||
this.famille.clear();
|
||||
this.tpeArray.clear();
|
||||
this.selectedLimit = null;
|
||||
|
||||
if (!v) {
|
||||
@@ -153,7 +127,7 @@ export class AgentFullForm {
|
||||
|
||||
this.form.reset({
|
||||
code: '',
|
||||
profile: '',
|
||||
profil: '',
|
||||
principalCode: '',
|
||||
caisseProfile: '',
|
||||
statut: 'ACTIF',
|
||||
@@ -226,60 +200,10 @@ export class AgentFullForm {
|
||||
});
|
||||
}
|
||||
|
||||
// Load assigned TPE IDs from tpes array
|
||||
(v.tpes ?? []).forEach((tpe) => this.tpeArray.push(this.fb.control(tpe.id)));
|
||||
// Assigned TPEs are not handled in 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 !== '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;
|
||||
}
|
||||
// TPE assignment / picker removed from this form
|
||||
|
||||
// === Validation helpers ===
|
||||
error(control: string): string {
|
||||
@@ -313,36 +237,13 @@ export class AgentFullForm {
|
||||
return;
|
||||
}
|
||||
const raw = this.form.getRawValue() as any;
|
||||
|
||||
// Convert TPE IDs to full TPE objects
|
||||
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 || '',
|
||||
// Prepare partial agent payload (family members are handled separately)
|
||||
const payload: Partial<Agent> = {
|
||||
...raw,
|
||||
tpes: tpes,
|
||||
} as Agent;
|
||||
...(this.value?.id ? { id: this.value.id } : {}),
|
||||
};
|
||||
|
||||
// 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
|
||||
// Emit the partial agent payload (parent will decide create vs update)
|
||||
this.save.emit(payload);
|
||||
}
|
||||
|
||||
@@ -362,7 +263,6 @@ 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);
|
||||
@@ -370,7 +270,7 @@ export class AgentFullForm {
|
||||
|
||||
this.form.reset({
|
||||
code: '',
|
||||
profile: '',
|
||||
profil: '',
|
||||
principalCode: '',
|
||||
caisseProfile: '',
|
||||
statut: 'ACTIF',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://192.168.1.235:8381',
|
||||
apiBaseUrl: 'https://cuddly-years-work.loca.lt',
|
||||
depouillementBaseUrl: 'http://192.168.1.235:8383'
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://192.168.1.235:8381',
|
||||
apiBaseUrl: 'https://cuddly-years-work.loca.lt',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user