504 lines
16 KiB
TypeScript
504 lines
16 KiB
TypeScript
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';
|
|
import { toast } from 'ngx-sonner';
|
|
import { AgentForm } from '@shared/forms/agent-form/agent-form';
|
|
import { TpeSelect } from "../tpe-select/tpe-select";
|
|
|
|
@Component({
|
|
standalone: true,
|
|
selector: 'app-agents',
|
|
templateUrl: './agents.html',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
imports: [
|
|
CommonModule,
|
|
DataTable,
|
|
Paginator,
|
|
SearchBar,
|
|
Modal,
|
|
ZardButtonComponent,
|
|
ZardCardComponent,
|
|
ZardFormModule,
|
|
AgentForm,
|
|
TpeSelect
|
|
],
|
|
})
|
|
export class AgentsPage {
|
|
rows = signal<Agent[]>([]);
|
|
total = signal(0);
|
|
loading = signal(false);
|
|
|
|
page = signal(0);
|
|
size = 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 | undefined>(undefined);
|
|
availableTpes = signal<TpeDevice[]>([]);
|
|
selectedTpeId = signal<string[]>([]);
|
|
tpesLoading = signal(false);
|
|
|
|
tpeDevice = signal<TpeDevice | undefined>(undefined);
|
|
tpeDevices = signal<TpeDevice[]>([]);
|
|
|
|
tpeArray = signal<boolean>(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, defaultVisible: true },
|
|
{ key: 'nomPrenom', label: 'Nom complet', sortable: true, defaultVisible: true, cell: (a) => `${a.nom} ${a.prenom}` },
|
|
{ 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, defaultVisible: true },
|
|
];
|
|
|
|
tpeMap = new Map<string, TpeDevice>();
|
|
agentTpesMap = new Map<string, TpeDevice[]>();
|
|
|
|
|
|
|
|
constructor(
|
|
private api: AgentService,
|
|
private tpeSvc: TpeService,
|
|
private familyMemberService: AgentFamilyMemberService
|
|
) {
|
|
effect(() => {
|
|
const params = {
|
|
page: this.page(),
|
|
size: this.size(),
|
|
search: this.search(),
|
|
sortKey: this.sort().key,
|
|
sortDir: this.sort().dir as SortDir,
|
|
};
|
|
untracked(() => this.fetch(params));
|
|
});
|
|
}
|
|
|
|
private fetch(params: {
|
|
page: number;
|
|
size: number;
|
|
search: string;
|
|
sortKey: string;
|
|
sortDir: SortDir;
|
|
}) {
|
|
this.loading.set(true);
|
|
this.api.list(params).subscribe({
|
|
next: (res) => {
|
|
this.rows.set(res.content);
|
|
this.total.set(res.pageable.total);
|
|
this.loading.set(false);
|
|
},
|
|
error: () => {
|
|
this.rows.set([]);
|
|
this.total.set(0);
|
|
this.loading.set(false);
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
renderStatutBadge(statut: Agent['statut'] | string | undefined): string {
|
|
if (!statut) return '';
|
|
const s = String(statut).toUpperCase();
|
|
if (s === 'ACTIF') {
|
|
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-green-500/10 text-green-600 dark:text-green-400 text-xs font-medium"><i class="icon-check"></i> Actif</span>`;
|
|
}
|
|
if (s === 'INACTIF') {
|
|
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-gray-500/10 text-gray-600 dark:text-gray-400 text-xs font-medium"><i class="icon-x"></i> Inactif</span>`;
|
|
}
|
|
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded bg-orange-500/10 text-orange-600 dark:text-orange-400 text-xs font-medium"><i class="icon-alert-circle"></i> Suspendu</span>`;
|
|
}
|
|
|
|
formatLimits(a: Agent): string {
|
|
const parts: string[] = [];
|
|
const nf = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 });
|
|
if (a.limiteInferieure !== undefined) parts.push(nf.format(a.limiteInferieure));
|
|
if (a.limiteSuperieure !== undefined) parts.push(nf.format(a.limiteSuperieure));
|
|
return parts.length ? parts.join(' — ') : '';
|
|
}
|
|
|
|
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([]);
|
|
},
|
|
});
|
|
const tpeIds = agent.terminauxIds;
|
|
if(Array.isArray(tpeIds)){
|
|
this.tpeArray.set(true);
|
|
forkJoin(
|
|
tpeIds.map(id=>this.tpeSvc.getById(String(id)))
|
|
).subscribe({
|
|
next:(tpes)=>{
|
|
this.tpeDevices.set(tpes.filter(tpe=>tpe!==undefined))
|
|
},
|
|
error:(err)=>{
|
|
console.error(err);
|
|
}
|
|
})
|
|
}else{
|
|
this.tpeArray.set(false);
|
|
this.tpeSvc.getById(String(tpeIds)).subscribe({
|
|
next:(tpe)=>{
|
|
if(tpe && tpe !== undefined)
|
|
this.tpeDevice.set(tpe);
|
|
},
|
|
error:(err)=>{
|
|
console.error(err);
|
|
}
|
|
})
|
|
}
|
|
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 isCreating = !current?.id; // Mode création (compact)
|
|
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é");
|
|
}
|
|
|
|
// 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));
|
|
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: () => {
|
|
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);
|
|
// Refresh data
|
|
this.fetch({
|
|
page: this.page(),
|
|
size: this.size(),
|
|
search: this.search(),
|
|
sortKey: this.sort().key,
|
|
sortDir: this.sort().dir as SortDir,
|
|
});
|
|
},
|
|
error: (err) => {
|
|
console.error('Error saving agent:', err);
|
|
alert("Erreur lors de la sauvegarde de l'agent");
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
getSelectedTpeIds = (): string[] => {
|
|
const ids = this.assigningAgent()?.terminauxIds;
|
|
|
|
if (!ids) return []; // undefined ou null → tableau vide
|
|
|
|
// Si c'est un tableau, on map en string
|
|
if (Array.isArray(ids)) {
|
|
return ids.map(id => id.toString());
|
|
}
|
|
|
|
// Si c'est un seul nombre, on retourne un tableau avec un élément
|
|
return [ids.toString()];
|
|
};
|
|
|
|
|
|
|
|
remove(row: Agent) {
|
|
if (!confirm(`Supprimer l\'agent ${row.code} ?`)) return;
|
|
this.api.delete(row.id).subscribe(() =>
|
|
this.fetch({
|
|
page: this.page(),
|
|
size: this.size(),
|
|
search: this.search(),
|
|
sortKey: this.sort().key,
|
|
sortDir: this.sort().dir as SortDir,
|
|
})
|
|
);
|
|
}
|
|
|
|
openAssignTpe(agent: Agent) {
|
|
this.assigningAgent.set(agent);
|
|
this.selectedTpeId.set([]);
|
|
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('ACTIF'), this.tpeSvc.getByStatut('ACTIF')]).subscribe({
|
|
// next: ([disponibleTpes, valideTpes]) => {
|
|
// // Combine and filter: only show TPEs that are not assigned to any agent AND not already assigned to this agent
|
|
// const allTpes = [...disponibleTpes, ...valideTpes];
|
|
// const available = allTpes.filter(
|
|
// (t) =>
|
|
// !t.agentConnecteId &&
|
|
// (t.statut === 'ACTIF') &&
|
|
// !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);
|
|
// },
|
|
// });
|
|
// }
|
|
|
|
// testAssigne(tpeIds: string[]){
|
|
// console.log(tpeIds);
|
|
// }
|
|
|
|
confirmAssignTpe() {
|
|
const agent = this.assigningAgent();
|
|
const tpeId = this.selectedTpeId();
|
|
if (!agent || tpeId.length === 0) {
|
|
alert('Veuillez sélectionner un TPE');
|
|
return;
|
|
}
|
|
|
|
forkJoin(this.selectedTpeId().map(id=> this.api.assigner(id, agent.id))).subscribe(
|
|
{
|
|
next:()=>{
|
|
this.assignTpeModalOpen.set(false);
|
|
this.assigningAgent.set(undefined);
|
|
this.selectedTpeId.set([]);
|
|
toast.success(`Tpe affecté à l'agent avec succès1`)
|
|
},
|
|
error: (err)=>{
|
|
console.error(err);
|
|
}
|
|
}
|
|
)
|
|
|
|
// // 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.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(undefined);
|
|
this.selectedTpeId.set([]);
|
|
}
|
|
}
|