first commit

This commit is contained in:
OnlyPapy98
2025-12-16 14:20:02 +01:00
commit dde2e8aebf
320 changed files with 30462 additions and 0 deletions

View 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('');
}
}