first commit
This commit is contained in:
483
src/app/dashboard/pages/agents/agents.ts
Normal file
483
src/app/dashboard/pages/agents/agents.ts
Normal file
@@ -0,0 +1,483 @@
|
||||
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 { ZardCardComponent } from '@shared/components/card/card.component';
|
||||
import { ZardSelectComponent } from '@shared/components/select/select.component';
|
||||
import { ZardSelectItemComponent } from '@shared/components/select/select-item.component';
|
||||
import { ZardFormModule } from '@shared/components/form/form.module';
|
||||
import { SortDir } from '@shared/paging/paging';
|
||||
import { Agent, AgentFamilyMember } from 'src/app/core/interfaces/agent';
|
||||
import { AgentService } from 'src/app/core/services/agent';
|
||||
import { AgentFamilyMemberService } from 'src/app/core/services/agent-family-member';
|
||||
import { TpeService } from 'src/app/core/services/tpe';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-agents',
|
||||
templateUrl: './agents.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
DataTable,
|
||||
Paginator,
|
||||
SearchBar,
|
||||
Modal,
|
||||
ZardButtonComponent,
|
||||
ZardCardComponent,
|
||||
ZardSelectComponent,
|
||||
ZardSelectItemComponent,
|
||||
ZardFormModule,
|
||||
AgentFullForm,
|
||||
],
|
||||
})
|
||||
export class AgentsPage {
|
||||
rows = signal<Agent[]>([]);
|
||||
total = signal(0);
|
||||
loading = signal(false);
|
||||
|
||||
page = signal(1);
|
||||
perPage = signal(10);
|
||||
search = signal('');
|
||||
sort = signal<SortState>({ key: 'code', dir: 'asc' });
|
||||
|
||||
modalOpen = signal(false);
|
||||
modalTitle = signal('Nouvel agent');
|
||||
editingItem = signal<Agent | null>(null);
|
||||
|
||||
detailModalOpen = signal(false);
|
||||
detailItem = signal<Agent | null>(null);
|
||||
detailFamilyMembers = signal<AgentFamilyMember[]>([]);
|
||||
|
||||
// TPE Assignment modal
|
||||
assignTpeModalOpen = signal(false);
|
||||
assigningAgent = signal<Agent | null>(null);
|
||||
availableTpes = signal<TpeDevice[]>([]);
|
||||
selectedTpeId = signal<string>('');
|
||||
tpesLoading = signal(false);
|
||||
|
||||
@ViewChild(AgentFullForm) formComp?: AgentFullForm;
|
||||
|
||||
formatTpeStatut(statut: TpeStatus): string {
|
||||
const statutMap: Record<string, string> = {
|
||||
VALIDE: 'Valide',
|
||||
INVALIDE: 'Invalide',
|
||||
EN_PANNE: 'En panne',
|
||||
BLOQUE: 'Bloqué',
|
||||
DISPONIBLE: 'Disponible',
|
||||
AFFECTE: 'Affecté',
|
||||
EN_MAINTENANCE: 'En maintenance',
|
||||
HORS_SERVICE: 'Hors service',
|
||||
VOLE: 'Volé',
|
||||
};
|
||||
return statutMap[statut] || statut;
|
||||
}
|
||||
|
||||
cols: TableColumn<Agent>[] = [
|
||||
{ key: 'code', label: 'Code', sortable: true },
|
||||
{ key: 'nom', label: 'Nom', sortable: true },
|
||||
{ key: 'prenom', label: 'Prénom', sortable: true },
|
||||
{ key: 'phone', label: 'Téléphone', sortable: true },
|
||||
{
|
||||
key: 'tpes',
|
||||
label: 'TPE assignés',
|
||||
cell: (a) => {
|
||||
const tpes = this.agentTpesMap.get(a.id) || [];
|
||||
if (tpes.length === 0) {
|
||||
return '<span class="text-muted-foreground text-sm">Aucun</span>';
|
||||
}
|
||||
// Show up to 2 TPEs with full details, then count for the rest
|
||||
const displayCount = Math.min(2, tpes.length);
|
||||
const displayed = tpes.slice(0, displayCount);
|
||||
const remaining = tpes.length - displayCount;
|
||||
|
||||
const tpeCards = displayed
|
||||
.map((t) => {
|
||||
const imei = `<div class="font-medium text-xs">${t.imei}</div>`;
|
||||
const details = [
|
||||
t.marque && t.modele ? `${t.marque} ${t.modele}` : t.marque || t.modele || '',
|
||||
t.statut ? this.formatTpeStatut(t.statut) : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' • ');
|
||||
const detailsHtml = details
|
||||
? `<div class="text-xs text-muted-foreground">${details}</div>`
|
||||
: '';
|
||||
return `<div class="px-2 py-1.5 rounded bg-primary/10 border border-primary/20 flex flex-col gap-0.5">${imei}${detailsHtml}</div>`;
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
const moreHtml =
|
||||
remaining > 0
|
||||
? `<div class="text-xs text-muted-foreground px-2 py-1.5">+${remaining} autre${
|
||||
remaining > 1 ? 's' : ''
|
||||
}</div>`
|
||||
: '';
|
||||
|
||||
return `<div class="flex flex-col gap-1">${tpeCards}${moreHtml}</div>`;
|
||||
},
|
||||
},
|
||||
{ key: 'zone', label: 'Zone', sortable: true },
|
||||
{ key: 'kiosk', label: 'Kiosque', sortable: true },
|
||||
{ key: 'profile', label: 'Profil', sortable: true },
|
||||
{ key: 'statut', label: 'Statut', sortable: true },
|
||||
{ key: 'limiteSuperieure', label: 'Limite sup.', sortable: true },
|
||||
];
|
||||
|
||||
tpeMap = new Map<string, TpeDevice>();
|
||||
agentTpesMap = new Map<string, TpeDevice[]>();
|
||||
|
||||
constructor(
|
||||
private api: AgentService,
|
||||
private tpeSvc: TpeService,
|
||||
private familyMemberService: AgentFamilyMemberService
|
||||
) {
|
||||
// Preload TPE maps for display
|
||||
this.tpeSvc
|
||||
.list({ page: 1, perPage: 200, search: '', sortKey: 'imei', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => {
|
||||
const tpes = res.data as TpeDevice[];
|
||||
this.rebuildTpeMaps(tpes);
|
||||
});
|
||||
effect(() => {
|
||||
const params = {
|
||||
page: this.page(),
|
||||
perPage: this.perPage(),
|
||||
search: this.search(),
|
||||
sortKey: this.sort().key,
|
||||
sortDir: this.sort().dir as SortDir,
|
||||
};
|
||||
untracked(() => this.fetch(params));
|
||||
});
|
||||
}
|
||||
|
||||
private fetch(params: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
search: string;
|
||||
sortKey: string;
|
||||
sortDir: SortDir;
|
||||
}) {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
this.rows.set(res.data);
|
||||
this.total.set(res.meta.total);
|
||||
this.loading.set(false);
|
||||
// Refresh TPE map to ensure we have latest data
|
||||
this.refreshTpeMap();
|
||||
},
|
||||
error: () => {
|
||||
this.rows.set([]);
|
||||
this.total.set(0);
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private refreshTpeMap() {
|
||||
this.tpeSvc
|
||||
.list({ page: 1, perPage: 200, search: '', sortKey: 'imei', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => {
|
||||
const tpes = res.data as TpeDevice[];
|
||||
this.rebuildTpeMaps(tpes);
|
||||
});
|
||||
}
|
||||
|
||||
private rebuildTpeMaps(tpes: TpeDevice[]) {
|
||||
this.tpeMap.clear();
|
||||
this.agentTpesMap.clear();
|
||||
tpes.forEach((t) => {
|
||||
this.tpeMap.set(t.id, t);
|
||||
const agentId = t.agent?.id;
|
||||
if (agentId) {
|
||||
const list = this.agentTpesMap.get(agentId) || [];
|
||||
list.push(t);
|
||||
this.agentTpesMap.set(agentId, list);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAgentTpes(agentId: string): TpeDevice[] {
|
||||
return this.agentTpesMap.get(agentId) || [];
|
||||
}
|
||||
|
||||
onSearch(q: string) {
|
||||
this.search.set(q);
|
||||
this.page.set(1);
|
||||
}
|
||||
openCreate() {
|
||||
this.modalTitle.set('Nouvel agent');
|
||||
this.editingItem.set(null);
|
||||
queueMicrotask(() => this.modalOpen.set(true));
|
||||
}
|
||||
openEdit(row: Agent) {
|
||||
this.modalTitle.set("Modifier l'agent");
|
||||
this.editingItem.set(row);
|
||||
queueMicrotask(() => this.modalOpen.set(true));
|
||||
}
|
||||
closeModal() {
|
||||
this.modalOpen.set(false);
|
||||
}
|
||||
openDetail(row: Agent) {
|
||||
// Fetch full agent details
|
||||
this.api.getById(row.id).subscribe({
|
||||
next: (agent) => {
|
||||
if (agent) {
|
||||
this.detailItem.set(agent);
|
||||
// Load family members separately
|
||||
this.familyMemberService.getByAgentId(agent.id).subscribe({
|
||||
next: (members) => {
|
||||
this.detailFamilyMembers.set(members);
|
||||
},
|
||||
error: () => {
|
||||
this.detailFamilyMembers.set([]);
|
||||
},
|
||||
});
|
||||
this.detailModalOpen.set(true);
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
// If fetch fails, use the row data
|
||||
this.detailItem.set(row);
|
||||
// Try to load family members anyway
|
||||
this.familyMemberService.getByAgentId(row.id).subscribe({
|
||||
next: (members) => {
|
||||
this.detailFamilyMembers.set(members);
|
||||
},
|
||||
error: () => {
|
||||
this.detailFamilyMembers.set([]);
|
||||
},
|
||||
});
|
||||
this.detailModalOpen.set(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
closeDetailModal() {
|
||||
this.detailModalOpen.set(false);
|
||||
this.detailItem.set(null);
|
||||
this.detailFamilyMembers.set([]);
|
||||
}
|
||||
submitChildForm() {
|
||||
this.formComp?.onSubmit();
|
||||
}
|
||||
|
||||
onFormSave(payload: Partial<Agent>) {
|
||||
const current = this.editingItem();
|
||||
const familyMembersData = this.formComp?.getFamilyMembersData() || [];
|
||||
|
||||
// Save agent first
|
||||
const req$ = current?.id
|
||||
? this.api.update(current.id, payload)
|
||||
: this.api.create(payload as Omit<Agent, 'id'>);
|
||||
|
||||
req$
|
||||
.pipe(
|
||||
switchMap((result) => {
|
||||
if (!result && current?.id) {
|
||||
// Update failed
|
||||
throw new Error("Erreur lors de la sauvegarde de l'agent");
|
||||
}
|
||||
|
||||
const savedAgentId = result?.id || current?.id || '';
|
||||
if (!savedAgentId) {
|
||||
throw new Error("Impossible d'obtenir l'ID de l'agent sauvegardé");
|
||||
}
|
||||
|
||||
// Get existing family members for this agent
|
||||
return this.familyMemberService.getByAgentId(savedAgentId).pipe(
|
||||
switchMap((existingMembers) => {
|
||||
const existingIds = new Set(existingMembers.map((m) => m.id));
|
||||
const newMembers = familyMembersData.filter((fm) => !fm.id);
|
||||
const updatedMembers = familyMembersData.filter(
|
||||
(fm) => fm.id && existingIds.has(fm.id)
|
||||
);
|
||||
const deletedIds = existingMembers
|
||||
.filter((em) => !familyMembersData.some((fm) => fm.id === em.id))
|
||||
.map((em) => em.id);
|
||||
|
||||
const operations: any[] = [];
|
||||
|
||||
// Delete removed members
|
||||
deletedIds.forEach((id) => {
|
||||
operations.push(
|
||||
this.familyMemberService.delete(id).pipe(
|
||||
catchError((err) => {
|
||||
console.error(`Error deleting family member ${id}:`, err);
|
||||
return of(false);
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// Create new members
|
||||
newMembers.forEach((member) => {
|
||||
operations.push(
|
||||
this.familyMemberService
|
||||
.create({
|
||||
agentId: savedAgentId,
|
||||
nom: member.nom,
|
||||
statut: member.statut,
|
||||
dateNaissance: member.dateNaissance,
|
||||
sexe: member.sexe as 'M' | 'F' | undefined,
|
||||
})
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
console.error('Error creating family member:', err);
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// Update existing members
|
||||
updatedMembers.forEach((member) => {
|
||||
if (member.id) {
|
||||
operations.push(
|
||||
this.familyMemberService
|
||||
.update(member.id, {
|
||||
nom: member.nom,
|
||||
statut: member.statut,
|
||||
dateNaissance: member.dateNaissance,
|
||||
sexe: member.sexe as 'M' | 'F' | undefined,
|
||||
})
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
console.error(`Error updating family member ${member.id}:`, err);
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return operations.length > 0 ? forkJoin(operations) : of([]);
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
// 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(),
|
||||
perPage: this.perPage(),
|
||||
search: this.search(),
|
||||
sortKey: this.sort().key,
|
||||
sortDir: this.sort().dir as SortDir,
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error saving agent:', err);
|
||||
alert("Erreur lors de la sauvegarde de l'agent");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
remove(row: Agent) {
|
||||
if (!confirm(`Supprimer l\'agent ${row.code} ?`)) return;
|
||||
this.api.delete(row.id).subscribe(() =>
|
||||
this.fetch({
|
||||
page: this.page(),
|
||||
perPage: this.perPage(),
|
||||
search: this.search(),
|
||||
sortKey: this.sort().key,
|
||||
sortDir: this.sort().dir as SortDir,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
openAssignTpe(agent: Agent) {
|
||||
this.assigningAgent.set(agent);
|
||||
this.selectedTpeId.set('');
|
||||
this.loadAvailableTpes();
|
||||
this.assignTpeModalOpen.set(true);
|
||||
}
|
||||
|
||||
loadAvailableTpes() {
|
||||
this.tpesLoading.set(true);
|
||||
const agent = this.assigningAgent();
|
||||
if (!agent) {
|
||||
this.availableTpes.set([]);
|
||||
this.tpesLoading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentAgentTpes = this.agentTpesMap.get(agent.id) || [];
|
||||
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({
|
||||
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') &&
|
||||
!agentTpeIds.has(t.id)
|
||||
);
|
||||
// Remove duplicates
|
||||
const uniqueTpes = Array.from(new Map(available.map((t) => [t.id, t])).values());
|
||||
this.availableTpes.set(uniqueTpes);
|
||||
this.tpesLoading.set(false);
|
||||
},
|
||||
error: () => {
|
||||
this.availableTpes.set([]);
|
||||
this.tpesLoading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
confirmAssignTpe() {
|
||||
const agent = this.assigningAgent();
|
||||
const tpeId = this.selectedTpeId();
|
||||
if (!agent || !tpeId) {
|
||||
alert('Veuillez sélectionner un TPE');
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign TPE to agent
|
||||
this.tpeSvc.assigner(tpeId, agent.id).subscribe({
|
||||
next: (tpe) => {
|
||||
if (tpe) {
|
||||
// Fermer le modal et recharger complètement la page
|
||||
this.assignTpeModalOpen.set(false);
|
||||
this.assigningAgent.set(null);
|
||||
this.selectedTpeId.set('');
|
||||
// Rechargement complet pour s'assurer que la liste des agents / TPE est à jour
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
alert("Erreur lors de l'assignation du TPE");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
closeAssignTpeModal() {
|
||||
this.assignTpeModalOpen.set(false);
|
||||
this.assigningAgent.set(null);
|
||||
this.selectedTpeId.set('');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user