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,487 @@
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
ViewChild,
effect,
signal,
untracked,
OnInit,
} 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 { ZardMenuModule } from '@shared/components/menu/menu.module';
import { ZardTooltipModule } from '@shared/components/tooltip/tooltip';
import { SortDir } from '@shared/paging/paging';
import { TpeDevice, TpeStatus } from 'src/app/core/interfaces/tpe';
import { TpeService } from 'src/app/core/services/tpe';
import { TpeForm } from '@shared/forms/tpe-form/tpe-form';
import { Agent } from 'src/app/core/interfaces/agent';
import { AgentService } from 'src/app/core/services/agent';
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 { forkJoin, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
@Component({
standalone: true,
selector: 'app-tpe-list',
templateUrl: './tpe.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
DataTable,
Paginator,
SearchBar,
Modal,
ZardButtonComponent,
TpeForm,
ZardMenuModule,
ZardTooltipModule,
ZardSelectComponent,
ZardSelectItemComponent,
ZardFormModule,
],
})
export class TpePage implements OnInit {
rows = signal<TpeDevice[]>([]);
total = signal(0);
loading = signal(false);
page = signal(1);
perPage = signal(10);
search = signal('');
sort = signal<SortState>({ key: 'imei', dir: 'asc' });
selectedStatut = signal<TpeStatus | null>(null);
modalOpen = signal(false);
modalTitle = signal('Nouvel équipement');
editingItem = signal<TpeDevice | null>(null);
// Agent assignment modal
assignModalOpen = signal(false);
assigningTpe = signal<TpeDevice | null>(null);
agents = signal<Agent[]>([]);
selectedAgentId = signal<string>('');
agentsLoading = signal(false);
// Stats
statsByStatut = signal<Record<string, number>>({});
assignmentStats = signal({ total: 0, assignes: 0, disponibles: 0 });
statsLoading = signal(false);
// Live search
private searchSubject = new Subject<string>();
@ViewChild(TpeForm) formComp?: TpeForm;
formatStatut(statut: string): 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<TpeDevice>[] = [
{ key: 'imei', label: 'IMEI', sortable: true },
{ key: 'serial', label: 'N° de Série', sortable: true },
{ key: 'type', label: 'Type', sortable: true },
{ key: 'marque', label: 'Marque', sortable: true },
{ key: 'modele', label: 'Modèle', sortable: true },
{ key: 'statut', label: 'Statut', sortable: true, cell: (d) => this.formatStatut(d.statut) },
{
key: 'assigne',
label: 'Assigné à',
cell: (d) => {
if (!d.assigne || !d.agent) {
return '<span class="text-muted-foreground text-sm">Non assigné</span>';
}
const agent = d.agent;
const code = agent.code
? `<span class="inline-flex items-center px-2 py-1 rounded bg-primary/10 text-primary text-xs font-medium">${agent.code}</span>`
: '';
const name =
agent.nom && agent.prenom
? `<div class="font-medium text-sm">${agent.nom} ${agent.prenom}</div>`
: agent.nom || agent.prenom
? `<div class="font-medium text-sm">${agent.nom || agent.prenom}</div>`
: '';
const phone = agent.phone
? `<div class="text-xs text-muted-foreground">${agent.phone}</div>`
: '';
const zone = agent.zone
? `<div class="text-xs text-muted-foreground">Zone: ${agent.zone}</div>`
: '';
const parts = [code, name, phone, zone].filter(Boolean);
if (parts.length === 0) {
return '<span class="text-muted-foreground text-sm">Agent assigné</span>';
}
return `<div class="flex flex-col gap-1">${parts.join('')}</div>`;
},
},
];
allStatuses: TpeStatus[] = [
'VALIDE',
'INVALIDE',
'EN_PANNE',
'BLOQUE',
'DISPONIBLE',
'AFFECTE',
'EN_MAINTENANCE',
'HORS_SERVICE',
'VOLE',
];
constructor(private api: TpeService, private agentService: AgentService) {
effect(() => {
// Only trigger fetch when page, perPage, or sort changes (not search - handled by searchSubject)
const searchValue = this.search();
const params = {
page: this.page(),
perPage: this.perPage(),
search: searchValue,
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
};
// Only fetch if search is empty (search is handled by searchSubject)
if (!searchValue.trim()) {
untracked(() => this.fetch(params));
}
});
// Setup live search with debounce
this.searchSubject
.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((query) => {
if (query.trim()) {
return this.api.search(query);
} else {
// If empty, use normal list
return this.api.list({
page: this.page(),
perPage: this.perPage(),
search: '',
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
}
})
)
.subscribe({
next: (res) => {
if (Array.isArray(res)) {
// Search API returns array
this.rows.set(res);
this.total.set(res.length);
} else {
// List returns PagedResult
this.rows.set(res.data);
this.total.set(res.meta.total);
}
this.loading.set(false);
},
error: (err) => {
console.error('Search error:', err);
this.rows.set([]);
this.total.set(0);
this.loading.set(false);
},
});
}
ngOnInit() {
this.loadStats();
// Initial fetch if no search query
if (!this.search().trim()) {
this.fetch({
page: this.page(),
perPage: this.perPage(),
search: '',
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
}
}
loadStats() {
this.statsLoading.set(true);
forkJoin({
byStatut: this.api.getCountByStatut(),
assignes: this.api.getAssignesStats(),
}).subscribe({
next: ({ byStatut, assignes }) => {
this.statsByStatut.set(byStatut || {});
// Calculate total from statsByStatut
const total = Object.values(byStatut || {}).reduce((sum, count) => sum + count, 0);
// Calculate disponibles (total - assignes)
const disponibles = Math.max(0, total - (assignes || 0));
this.assignmentStats.set({
total: total,
assignes: assignes || 0,
disponibles: disponibles,
});
this.statsLoading.set(false);
},
error: (err) => {
console.error('Error loading stats:', err);
this.statsByStatut.set({});
this.assignmentStats.set({ total: 0, assignes: 0, disponibles: 0 });
this.statsLoading.set(false);
},
});
}
private fetch(params: {
page: number;
perPage: number;
search: string;
sortKey: string;
sortDir: SortDir;
}) {
// Don't fetch if there's a search query - it's handled by searchSubject
const searchQuery = params.search.trim();
if (searchQuery) {
return; // Search is handled by searchSubject subscription
}
this.loading.set(true);
const statut = this.selectedStatut();
if (statut) {
// Filter by statut - returns array
this.api.getByStatut(statut).subscribe({
next: (res: TpeDevice[]) => {
this.rows.set(res);
this.total.set(res.length);
this.loading.set(false);
},
error: () => {
this.rows.set([]);
this.total.set(0);
this.loading.set(false);
},
});
} else {
// Normal list with pagination
this.api.list(params).subscribe({
next: (res) => {
this.rows.set(res.data);
this.total.set(res.meta.total);
this.loading.set(false);
},
error: () => {
this.rows.set([]);
this.total.set(0);
this.loading.set(false);
},
});
}
}
onSearch(q: string) {
this.search.set(q);
this.page.set(1);
// Trigger search via subject for live search
if (q.trim()) {
this.loading.set(true);
this.searchSubject.next(q);
} else {
// If empty, fetch normally
this.fetch({
page: this.page(),
perPage: this.perPage(),
search: '',
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
}
}
onStatutFilter(statut: TpeStatus | null) {
this.selectedStatut.set(statut);
this.page.set(1);
this.fetch({
page: this.page(),
perPage: this.perPage(),
search: this.search(),
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
}
onUpdateStatut(row: TpeDevice, newStatut: TpeStatus) {
if (!confirm(`Changer le statut de ${row.imei} vers ${this.formatStatut(newStatut)} ?`)) return;
this.api.updateStatut(row.id, newStatut).subscribe({
next: () => {
this.fetch({
page: this.page(),
perPage: this.perPage(),
search: this.search(),
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
this.loadStats();
},
});
}
onLiberer(row: TpeDevice) {
if (!confirm(`Libérer le TPE ${row.imei} ?`)) return;
this.api.liberer(row.id).subscribe({
next: () => {
this.fetch({
page: this.page(),
perPage: this.perPage(),
search: this.search(),
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
this.loadStats();
},
});
}
onAssigner(row: TpeDevice) {
this.assigningTpe.set(row);
this.selectedAgentId.set('');
this.loadAgents();
this.assignModalOpen.set(true);
}
loadAgents() {
this.agentsLoading.set(true);
// Load active agents only
this.agentService.getByStatut('ACTIF').subscribe({
next: (agents) => {
this.agents.set(agents);
this.agentsLoading.set(false);
},
error: () => {
this.agents.set([]);
this.agentsLoading.set(false);
},
});
}
confirmAssign() {
const tpe = this.assigningTpe();
const agentId = this.selectedAgentId();
if (!tpe || !agentId) {
alert('Veuillez sélectionner un agent');
return;
}
this.api.assigner(tpe.id, agentId).subscribe({
next: () => {
this.assignModalOpen.set(false);
this.assigningTpe.set(null);
this.selectedAgentId.set('');
this.fetch({
page: this.page(),
perPage: this.perPage(),
search: this.search(),
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
this.loadStats();
},
error: () => {
alert("Erreur lors de l'assignation du TPE");
},
});
}
closeAssignModal() {
this.assignModalOpen.set(false);
this.assigningTpe.set(null);
this.selectedAgentId.set('');
}
openCreate() {
this.modalTitle.set('Nouvel équipement');
this.editingItem.set(null);
queueMicrotask(() => this.modalOpen.set(true));
}
openEdit(row: TpeDevice) {
this.modalTitle.set("Modifier l'équipement");
this.editingItem.set(row);
queueMicrotask(() => this.modalOpen.set(true));
}
closeModal() {
this.modalOpen.set(false);
setTimeout(() => {
this.editingItem.set(null);
}, 0);
}
submitChildForm() {
this.formComp?.onSubmit();
}
onFormSave(payload: Partial<TpeDevice>) {
const current = this.editingItem();
const isCreating = !current?.id;
const req$ = current?.id
? this.api.update(current.id, payload)
: this.api.create(payload as Omit<TpeDevice, 'id'>);
req$.subscribe({
next: (result) => {
// For update, check if result is valid (update can return undefined on error)
if (current?.id && !result) {
console.error('Update failed - result is undefined');
// Don't close modal, let user retry
return;
}
// Success - close modal first
this.modalOpen.set(false);
// Then reset form and clear editing item after a short delay
setTimeout(() => {
this.editingItem.set(null);
this.formComp?.resetForm();
}, 100);
// Refresh data
this.fetch({
page: this.page(),
perPage: this.perPage(),
search: this.search(),
sortKey: this.sort().key,
sortDir: this.sort().dir as SortDir,
});
this.loadStats();
},
error: (err) => {
console.error('Error saving TPE:', err);
// Don't close modal on error, let user fix and retry
// Form stays filled so user can correct and resubmit
},
});
}
remove(row: TpeDevice) {
if (!confirm(`Supprimer l\'équipement IMEI ${row.imei} ?`)) 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,
});
this.loadStats();
});
}
}