first commit
This commit is contained in:
194
src/app/dashboard/pages/tpe/tpe.html
Normal file
194
src/app/dashboard/pages/tpe/tpe.html
Normal file
@@ -0,0 +1,194 @@
|
||||
<div class="flex flex-col gap-4 min-h-screen">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">TPES (Terminal Point de Vente)</h2>
|
||||
<z-button (click)="openCreate()">Nouvel équipement</z-button>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
@if (statsLoading()) {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
@for (i of [1, 2, 3, 4]; track i) {
|
||||
<div class="bg-surface border rounded-lg p-4 animate-pulse">
|
||||
<div class="h-4 bg-gray-200 rounded w-24 mb-2"></div>
|
||||
<div class="h-8 bg-gray-200 rounded w-16"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="bg-surface border rounded-lg p-4">
|
||||
<div class="text-sm text-muted-foreground">Total TPEs</div>
|
||||
<div class="text-2xl font-bold">{{ assignmentStats().total }}</div>
|
||||
</div>
|
||||
<div class="bg-surface border rounded-lg p-4">
|
||||
<div class="text-sm text-muted-foreground">Assignés</div>
|
||||
<div class="text-2xl font-bold">{{ assignmentStats().assignes }}</div>
|
||||
</div>
|
||||
<div class="bg-surface border rounded-lg p-4">
|
||||
<div class="text-sm text-muted-foreground">Disponibles</div>
|
||||
<div class="text-2xl font-bold">{{ assignmentStats().disponibles }}</div>
|
||||
</div>
|
||||
<div class="bg-surface border rounded-lg p-4">
|
||||
<div class="text-sm text-muted-foreground">Valides</div>
|
||||
<div class="text-2xl font-bold">{{ statsByStatut()['VALIDE'] || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Statut Filter Chips -->
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<span class="text-sm font-medium">Filtrer par statut:</span>
|
||||
<button
|
||||
z-button
|
||||
zType="ghost"
|
||||
zSize="sm"
|
||||
[class]="!selectedStatut() ? '!bg-primary/10 !text-primary' : ''"
|
||||
(click)="onStatutFilter(null)"
|
||||
>
|
||||
Tous
|
||||
</button>
|
||||
@for (statut of allStatuses; track statut) {
|
||||
<button
|
||||
z-button
|
||||
zType="ghost"
|
||||
zSize="sm"
|
||||
[class]="selectedStatut() === statut ? '!bg-primary/10 !text-primary' : ''"
|
||||
(click)="onStatutFilter(statut)"
|
||||
>
|
||||
{{ formatStatut(statut) }}
|
||||
@if (statsByStatut()[statut]) {
|
||||
<span class="ml-1 text-xs">({{ statsByStatut()[statut] }})</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<app-search-bar (search)="onSearch($event)"></app-search-bar>
|
||||
|
||||
<app-data-table [data]="rows()" [columns]="cols" [sort]="sort()" (sortChange)="sort.set($event)">
|
||||
<ng-template #rowActions let-row>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
@if (row.assigne) {
|
||||
<button
|
||||
z-button
|
||||
zType="ghost"
|
||||
zSize="sm"
|
||||
(click)="onLiberer(row)"
|
||||
zTooltip="Libérer"
|
||||
zPosition="top"
|
||||
>
|
||||
<i class="icon-unlink-2"></i>
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
z-button
|
||||
zType="ghost"
|
||||
zSize="sm"
|
||||
(click)="onAssigner(row)"
|
||||
zTooltip="Assigner"
|
||||
zPosition="top"
|
||||
>
|
||||
<i class="icon-user-plus"></i>
|
||||
</button>
|
||||
}
|
||||
<div z-menu [zMenuTriggerFor]="statutMenu" zPlacement="bottomRight">
|
||||
<ng-template #statutMenu>
|
||||
<div z-menu-content class="w-48">
|
||||
@for (statut of allStatuses; track statut) {
|
||||
<button
|
||||
z-menu-item
|
||||
[class]="row.statut === statut ? '!text-primary' : ''"
|
||||
(click)="onUpdateStatut(row, statut)"
|
||||
>
|
||||
{{ formatStatut(statut) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
<button z-button zType="ghost" zSize="sm" zTooltip="Changer statut" zPosition="top">
|
||||
<i class="icon-sliders-horizontal"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
z-button
|
||||
zType="ghost"
|
||||
zSize="sm"
|
||||
(click)="openEdit(row)"
|
||||
zTooltip="Modifier"
|
||||
zPosition="top"
|
||||
>
|
||||
<i class="icon-pen"></i>
|
||||
</button>
|
||||
<button
|
||||
z-button
|
||||
zType="destructive"
|
||||
zSize="sm"
|
||||
(click)="remove(row)"
|
||||
zTooltip="Supprimer"
|
||||
zPosition="top"
|
||||
>
|
||||
<i class="icon-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
|
||||
<app-paginator
|
||||
[total]="total()"
|
||||
[page]="page()"
|
||||
[perPage]="perPage()"
|
||||
(pageChange)="page.set($event)"
|
||||
(perPageChange)="perPage.set($event)"
|
||||
></app-paginator>
|
||||
</div>
|
||||
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xl">
|
||||
<app-tpe-form
|
||||
[value]="editingItem() ?? undefined"
|
||||
(save)="onFormSave($event)"
|
||||
(cancel)="closeModal()"
|
||||
/>
|
||||
<div modal-actions class="flex justify-end gap-2">
|
||||
<z-button zType="destructive" (click)="closeModal()">Annuler</z-button>
|
||||
<z-button (click)="submitChildForm()">Enregistrer</z-button>
|
||||
</div>
|
||||
</app-modal>
|
||||
|
||||
<!-- Agent Assignment Modal -->
|
||||
<app-modal
|
||||
[open]="assignModalOpen()"
|
||||
[title]="'Assigner le TPE ' + (assigningTpe()?.imei || '')"
|
||||
(close)="closeAssignModal()"
|
||||
size="md"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
@if (agentsLoading()) {
|
||||
<div class="text-center py-4">Chargement des agents...</div>
|
||||
} @else if (agents().length === 0) {
|
||||
<div class="text-center py-4 text-muted-foreground">Aucun agent actif disponible</div>
|
||||
} @else {
|
||||
<z-form-field>
|
||||
<label z-form-label>Sélectionner un agent</label>
|
||||
<div z-form-control>
|
||||
<z-select
|
||||
[zValue]="selectedAgentId()"
|
||||
(zSelectionChange)="selectedAgentId.set($event)"
|
||||
[zPlaceholder]="'Sélectionner un agent...'"
|
||||
>
|
||||
@for (agent of agents(); track agent.id) {
|
||||
<z-select-item [zValue]="agent.id">
|
||||
{{ agent.code }} - {{ agent.nom }} {{ agent.prenom }}
|
||||
</z-select-item>
|
||||
}
|
||||
</z-select>
|
||||
</div>
|
||||
</z-form-field>
|
||||
}
|
||||
</div>
|
||||
<div modal-actions class="flex justify-end gap-2">
|
||||
<z-button zType="destructive" (click)="closeAssignModal()">Annuler</z-button>
|
||||
<button z-button [disabled]="!selectedAgentId() || agentsLoading()" (click)="confirmAssign()">
|
||||
Assigner
|
||||
</button>
|
||||
</div>
|
||||
</app-modal>
|
||||
487
src/app/dashboard/pages/tpe/tpe.ts
Normal file
487
src/app/dashboard/pages/tpe/tpe.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user