507 lines
15 KiB
TypeScript
507 lines
15 KiB
TypeScript
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';
|
|
import { toast } from 'ngx-sonner';
|
|
import { PointsVenteService } from 'src/app/core/services/points-vente';
|
|
|
|
@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(0);
|
|
perPage = signal(10);
|
|
search = signal('');
|
|
sort = signal<SortState>({ key: 'id', 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: 'numeroSerie', label: 'Numéro de série', sortable: true },
|
|
{
|
|
key: 'pointDeVenteId',
|
|
label: 'Point de vente',
|
|
sortable: true,
|
|
cell:(p) => {
|
|
let pointVente = signal('');
|
|
this.pointVenteService.getById(p.pointDeVenteId).subscribe({
|
|
next: (pdv)=>{
|
|
if(!pdv || pdv === undefined){
|
|
pointVente.set("Aucun point")
|
|
return;
|
|
};
|
|
pointVente.set(`${pdv.ville}/${pdv.adresse}`)
|
|
},
|
|
error:(err)=>{
|
|
pointVente.set('Aucun point')
|
|
}
|
|
})
|
|
return pointVente();
|
|
},
|
|
},
|
|
{ key: 'versionLogicielle', label: 'Version', sortable: true },
|
|
{ key: 'typeTerminal', label: 'Type', sortable: true },
|
|
{ key: 'systemeExploitation', label: 'Sytème', sortable: true },
|
|
{ key: 'statut', label: 'Statut', sortable: true, cell: (d) => this.formatStatut(d.statut) },
|
|
{
|
|
key: 'assigne',
|
|
label: 'Assigné à',
|
|
cell: (d) => {
|
|
if (!d.agentConnecteId) {
|
|
return '<span class="text-muted-foreground text-sm">Non assigné</span>';
|
|
}
|
|
// a rectifier apres avec les données des agents!
|
|
const agent = {} as any;
|
|
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[] = ['ACTIF', 'HORS_SERVICE'];
|
|
|
|
constructor(
|
|
private api: TpeService,
|
|
private agentService: AgentService,
|
|
private pointVenteService: PointsVenteService
|
|
) {
|
|
effect(() => {
|
|
// Only trigger fetch when page, perPage, or sort changes (not search - handled by searchSubject)
|
|
const searchValue = this.search();
|
|
const params = {
|
|
page: this.page(),
|
|
size: 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(),
|
|
size: 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.content);
|
|
this.total.set(res.pageable.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(),
|
|
size: 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;
|
|
size: 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.content);
|
|
this.total.set(res.pageable.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(),
|
|
size: 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(),
|
|
size: 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.numeroSerie} vers ${this.formatStatut(newStatut)} ?`))
|
|
return;
|
|
this.api.updateStatut(row.id, newStatut).subscribe({
|
|
next: () => {
|
|
this.fetch({
|
|
page: this.page(),
|
|
size: 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.numeroSerie} ?`)) return;
|
|
this.api.liberer(row.id).subscribe({
|
|
next: () => {
|
|
this.fetch({
|
|
page: this.page(),
|
|
size: 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(),
|
|
size: 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) => {
|
|
toast.success('Tpe créé avec succès!');
|
|
// 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(),
|
|
size: 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.numeroSerie} ?`)) return;
|
|
this.api.delete(row.id).subscribe(() => {
|
|
this.fetch({
|
|
page: this.page(),
|
|
size: this.perPage(),
|
|
search: this.search(),
|
|
sortKey: this.sort().key,
|
|
sortDir: this.sort().dir as SortDir,
|
|
});
|
|
this.loadStats();
|
|
});
|
|
}
|
|
}
|