agent and tpe save done
This commit is contained in:
@@ -35,16 +35,11 @@
|
||||
</div>
|
||||
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xxl">
|
||||
<app-agent-full-form
|
||||
<app-agent-form
|
||||
[value]="editingItem() ?? undefined"
|
||||
[compact]="!editingItem()"
|
||||
(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>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
@@ -319,7 +314,7 @@
|
||||
@for (tpe of getAgentTpes(agent.id); track tpe.id) {
|
||||
<div class="px-3 py-2.5 rounded bg-primary/10 border border-primary/20">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="font-medium text-sm">{{ tpe.imei }}</div>
|
||||
<div class="font-medium text-sm">{{ tpe.numeroSerie }}</div>
|
||||
@if (tpe.statut) {
|
||||
<span class="text-xs px-2 py-0.5 rounded bg-surface text-muted-foreground">
|
||||
{{ formatTpeStatut(tpe.statut) }}
|
||||
@@ -327,19 +322,19 @@
|
||||
}
|
||||
</div>
|
||||
<div class="space-y-1 text-xs text-muted-foreground">
|
||||
@if (tpe.marque || tpe.modele) {
|
||||
@if (tpe.modeleAppareil) {
|
||||
<div>
|
||||
<span class="font-medium">Modèle:</span> {{ tpe.marque }} {{ tpe.modele }}
|
||||
<span class="font-medium">Modèle:</span> {{ tpe.modeleAppareil }}
|
||||
</div>
|
||||
}
|
||||
@if (tpe.serial) {
|
||||
@if (tpe.numeroSerie) {
|
||||
<div>
|
||||
<span class="font-medium">Série:</span> {{ tpe.serial }}
|
||||
<span class="font-medium">Série:</span> {{ tpe.numeroSerie }}
|
||||
</div>
|
||||
}
|
||||
@if (tpe.type) {
|
||||
@if (tpe.typeTerminal) {
|
||||
<div>
|
||||
<span class="font-medium">Type:</span> {{ tpe.type }}
|
||||
<span class="font-medium">Type:</span> {{ tpe.typeTerminal }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -385,9 +380,9 @@
|
||||
>
|
||||
@for (tpe of availableTpes(); track tpe.id) {
|
||||
<z-select-item [zValue]="tpe.id">
|
||||
{{ tpe.imei }} - {{ tpe.marque }} {{ tpe.modele }}
|
||||
@if (tpe.statut === 'VALIDE') {
|
||||
<span class="text-xs text-green-600 dark:text-green-400 ml-2">(Valide)</span>
|
||||
{{ tpe.numeroSerie }} - {{ tpe.modeleAppareil }}
|
||||
@if (tpe.statut === 'ACTIF') {
|
||||
<span class="text-xs text-green-600 dark:text-green-400 ml-2">(Actif)</span>
|
||||
}
|
||||
</z-select-item>
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ 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';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -43,7 +44,7 @@ import { toast } from 'ngx-sonner';
|
||||
ZardSelectComponent,
|
||||
ZardSelectItemComponent,
|
||||
ZardFormModule,
|
||||
AgentFullForm,
|
||||
AgentForm
|
||||
],
|
||||
})
|
||||
export class AgentsPage {
|
||||
@@ -111,7 +112,7 @@ export class AgentsPage {
|
||||
) {
|
||||
// Preload TPE maps for display
|
||||
this.tpeSvc
|
||||
.list({ page: 1, size: 200, search: '', sortKey: 'imei', sortDir: 'asc' } as any)
|
||||
.list({ page: 1, size: 200, search: '', sortKey: 'id', sortDir: 'asc' } as any)
|
||||
.subscribe((res) => {
|
||||
const tpes = res.content as TpeDevice[];
|
||||
this.rebuildTpeMaps(tpes);
|
||||
@@ -138,6 +139,7 @@ export class AgentsPage {
|
||||
this.loading.set(true);
|
||||
this.api.list(params).subscribe({
|
||||
next: (res) => {
|
||||
console.log(res.content);
|
||||
this.rows.set(res.content);
|
||||
this.total.set(res.pageable.total);
|
||||
this.loading.set(false);
|
||||
@@ -166,7 +168,7 @@ export class AgentsPage {
|
||||
this.agentTpesMap.clear();
|
||||
tpes.forEach((t) => {
|
||||
this.tpeMap.set(t.id, t);
|
||||
const agentId = t.agent?.id;
|
||||
const agentId = t.agentConnecteId;
|
||||
if (agentId) {
|
||||
const list = this.agentTpesMap.get(agentId) || [];
|
||||
list.push(t);
|
||||
@@ -417,14 +419,14 @@ export class AgentsPage {
|
||||
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({
|
||||
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.assigne &&
|
||||
(t.statut === 'DISPONIBLE' || t.statut === 'VALIDE') &&
|
||||
!t.agentConnecteId &&
|
||||
(t.statut === 'ACTIF') &&
|
||||
!agentTpeIds.has(t.id)
|
||||
);
|
||||
// Remove duplicates
|
||||
|
||||
0
src/app/dashboard/pages/point-vente/point-vente.css
Normal file
0
src/app/dashboard/pages/point-vente/point-vente.css
Normal file
41
src/app/dashboard/pages/point-vente/point-vente.html
Normal file
41
src/app/dashboard/pages/point-vente/point-vente.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<div class="flex flex-col gap-2 min-h-screen">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-semibold">Gestion des Points de Vente</h2>
|
||||
<z-button (click)="openCreate()">Nouveau point de vente</z-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-3">
|
||||
<button z-button zType="ghost" (click)="openEdit(row)" title="Modifier">
|
||||
<i class="icon-pen"></i>
|
||||
</button>
|
||||
<button z-button zType="destructive" (click)="remove(row)" title="Supprimer">
|
||||
<i class="icon-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-data-table>
|
||||
|
||||
<app-paginator
|
||||
[total]="total()"
|
||||
[page]="page()"
|
||||
[perPage]="size()"
|
||||
(pageChange)="page.set($event - 1)"
|
||||
(perPageChange)="size.set($event)"
|
||||
></app-paginator>
|
||||
</div>
|
||||
|
||||
<app-modal [open]="modalOpen()" [title]="modalTitle()" (close)="closeModal()" size="xl">
|
||||
<app-points-vente-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>
|
||||
23
src/app/dashboard/pages/point-vente/point-vente.spec.ts
Normal file
23
src/app/dashboard/pages/point-vente/point-vente.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PointVente } from './point-vente';
|
||||
|
||||
describe('PointVente', () => {
|
||||
let component: PointVente;
|
||||
let fixture: ComponentFixture<PointVente>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PointVente]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PointVente);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
209
src/app/dashboard/pages/point-vente/point-vente.ts
Normal file
209
src/app/dashboard/pages/point-vente/point-vente.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
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 { SortDir } from '@shared/paging/paging';
|
||||
import { PointVente, PointVenteStatut } from 'src/app/core/interfaces/points-ventes';
|
||||
import { PointsVenteService } from 'src/app/core/services/points-vente';
|
||||
import { PointsVenteForm } from '@shared/forms/points-vente-form/points-vente-form';
|
||||
import { toast } from 'ngx-sonner';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-point-vente',
|
||||
templateUrl: './point-vente.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
DataTable,
|
||||
Paginator,
|
||||
SearchBar,
|
||||
Modal,
|
||||
ZardButtonComponent,
|
||||
PointsVenteForm,
|
||||
],
|
||||
})
|
||||
export class PointVentePage {
|
||||
rows = signal<PointVente[]>([]);
|
||||
total = signal(0);
|
||||
loading = signal(false);
|
||||
|
||||
page = signal(0);
|
||||
size = signal(10);
|
||||
search = signal('');
|
||||
sort = signal<SortState>({ key: 'nom', dir: 'asc' });
|
||||
|
||||
modalOpen = signal(false);
|
||||
modalTitle = signal('Nouveau point de vente');
|
||||
editingItem = signal<PointVente | null>(null);
|
||||
|
||||
@ViewChild(PointsVenteForm) formComp?: PointsVenteForm;
|
||||
|
||||
cols: TableColumn<PointVente>[] = [
|
||||
{ key: 'nom', label: 'Nom', sortable: true, defaultVisible: true },
|
||||
{ key: 'adresse', label: 'Adresse', sortable: true, defaultVisible: true },
|
||||
{ key: 'ville', label: 'Ville', sortable: true, defaultVisible: true },
|
||||
{
|
||||
key: 'statut',
|
||||
label: 'Statut',
|
||||
sortable: true,
|
||||
defaultVisible: true,
|
||||
cell: (pv) => this.renderStatutBadge(pv.statut),
|
||||
},
|
||||
{
|
||||
key: 'latitude',
|
||||
label: 'Latitude',
|
||||
cell: (pv) => pv.latitude?.toFixed(6) || '—',
|
||||
},
|
||||
{
|
||||
key: 'longitude',
|
||||
label: 'Longitude',
|
||||
cell: (pv) => pv.longitude?.toFixed(6) || '—',
|
||||
},
|
||||
];
|
||||
|
||||
constructor(private api: PointsVenteService) {
|
||||
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: PointVenteStatut | string | undefined): string {
|
||||
if (!statut) return '';
|
||||
const s = String(statut).toUpperCase();
|
||||
if (s === 'ACTIVE') {
|
||||
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>`;
|
||||
}
|
||||
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>`;
|
||||
}
|
||||
|
||||
onSearch(q: string) {
|
||||
this.search.set(q);
|
||||
this.page.set(0);
|
||||
}
|
||||
|
||||
openCreate() {
|
||||
this.modalTitle.set('Nouveau point de vente');
|
||||
this.editingItem.set(null);
|
||||
queueMicrotask(() => this.modalOpen.set(true));
|
||||
}
|
||||
|
||||
openEdit(row: PointVente) {
|
||||
this.modalTitle.set("Modifier le point de vente");
|
||||
this.editingItem.set(row);
|
||||
queueMicrotask(() => this.modalOpen.set(true));
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.modalOpen.set(false);
|
||||
}
|
||||
|
||||
submitChildForm() {
|
||||
this.formComp?.onSubmit();
|
||||
}
|
||||
|
||||
onFormSave(payload: Partial<PointVente>) {
|
||||
const current = this.editingItem();
|
||||
const isCreating = !current?.id;
|
||||
|
||||
const req$ = current?.id
|
||||
? this.api.update(current.id, payload)
|
||||
: this.api.create(payload as Omit<PointVente, 'id'>);
|
||||
|
||||
req$.subscribe({
|
||||
next: (result) => {
|
||||
if (result) {
|
||||
toast.success(
|
||||
isCreating
|
||||
? 'Point de vente créé avec succès'
|
||||
: 'Point de vente modifié avec succès'
|
||||
);
|
||||
// Close modal first
|
||||
this.closeModal();
|
||||
// Reset form
|
||||
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 point de vente:', err);
|
||||
alert(
|
||||
isCreating
|
||||
? "Erreur lors de la création du point de vente"
|
||||
: "Erreur lors de la modification du point de vente"
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
remove(row: PointVente) {
|
||||
if (!confirm(`Supprimer le point de vente "${row.nom}" ?`)) return;
|
||||
this.api.delete(row.id).subscribe({
|
||||
next: (success) => {
|
||||
if (success) {
|
||||
toast.success('Point de vente supprimé avec succès');
|
||||
this.fetch({
|
||||
page: this.page(),
|
||||
size: this.size(),
|
||||
search: this.search(),
|
||||
sortKey: this.sort().key,
|
||||
sortDir: this.sort().dir as SortDir,
|
||||
});
|
||||
} else {
|
||||
alert("Erreur lors de la suppression du point de vente");
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
alert("Erreur lors de la suppression du point de vente");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@
|
||||
<!-- Agent Assignment Modal -->
|
||||
<app-modal
|
||||
[open]="assignModalOpen()"
|
||||
[title]="'Assigner le TPE ' + (assigningTpe()?.imei || '')"
|
||||
[title]="'Assigner le TPE ' + (assigningTpe()?.numeroSerie || '')"
|
||||
(close)="closeAssignModal()"
|
||||
size="md"
|
||||
>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ZardSelectItemComponent } from '@shared/components/select/select-item.c
|
||||
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';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -105,11 +106,12 @@ export class TpePage implements OnInit {
|
||||
key: 'assigne',
|
||||
label: 'Assigné à',
|
||||
cell: (d) => {
|
||||
if (!d.assigne || !d.agent) {
|
||||
if (!d.agentConnecteId) {
|
||||
return '<span class="text-muted-foreground text-sm">Non assigné</span>';
|
||||
}
|
||||
const agent = d.agent;
|
||||
const code = agent.code
|
||||
// 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 =
|
||||
@@ -135,15 +137,8 @@ export class TpePage implements OnInit {
|
||||
];
|
||||
|
||||
allStatuses: TpeStatus[] = [
|
||||
'VALIDE',
|
||||
'INVALIDE',
|
||||
'EN_PANNE',
|
||||
'BLOQUE',
|
||||
'DISPONIBLE',
|
||||
'AFFECTE',
|
||||
'EN_MAINTENANCE',
|
||||
'HORS_SERVICE',
|
||||
'VOLE',
|
||||
'ACTIF',
|
||||
'HORS_SERVICE'
|
||||
];
|
||||
|
||||
constructor(private api: TpeService, private agentService: AgentService) {
|
||||
@@ -326,7 +321,7 @@ export class TpePage implements OnInit {
|
||||
}
|
||||
|
||||
onUpdateStatut(row: TpeDevice, newStatut: TpeStatus) {
|
||||
if (!confirm(`Changer le statut de ${row.imei} vers ${this.formatStatut(newStatut)} ?`)) return;
|
||||
if (!confirm(`Changer le statut de ${row.numeroSerie} vers ${this.formatStatut(newStatut)} ?`)) return;
|
||||
this.api.updateStatut(row.id, newStatut).subscribe({
|
||||
next: () => {
|
||||
this.fetch({
|
||||
@@ -342,7 +337,7 @@ export class TpePage implements OnInit {
|
||||
}
|
||||
|
||||
onLiberer(row: TpeDevice) {
|
||||
if (!confirm(`Libérer le TPE ${row.imei} ?`)) return;
|
||||
if (!confirm(`Libérer le TPE ${row.numeroSerie} ?`)) return;
|
||||
this.api.liberer(row.id).subscribe({
|
||||
next: () => {
|
||||
this.fetch({
|
||||
@@ -440,6 +435,7 @@ export class TpePage implements OnInit {
|
||||
: 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');
|
||||
@@ -472,7 +468,7 @@ export class TpePage implements OnInit {
|
||||
}
|
||||
|
||||
remove(row: TpeDevice) {
|
||||
if (!confirm(`Supprimer l\'équipement IMEI ${row.imei} ?`)) return;
|
||||
if (!confirm(`Supprimer l\'équipement IMEI ${row.numeroSerie} ?`)) return;
|
||||
this.api.delete(row.id).subscribe(() => {
|
||||
this.fetch({
|
||||
page: this.page(),
|
||||
|
||||
Reference in New Issue
Block a user