feat(rental): add inventory and condition reports (#7)

This commit is contained in:
nessar authored and Nessar committed 2026-08-17 14:18:16 +02:00
1 parent 92d8a3e351
commit e4be5cd05f
193 files changed
+9697 -483

No files matched your search

@@ -0,0 +1,188 @@
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { finalize as rxFinalize } from 'rxjs';
import {
ConditionReportService,
FurnitureInventory,
FurnitureInventoryLine,
FurniturePresence,
} from '../../services/condition-report.service';
import { httpErrorMessage } from '../../services/http-error-message';
const emptyInventory: FurnitureInventory = {
id: '',
templateVersion: '',
lines: [],
missingCategories: [],
absentCategories: [],
legalWarnings: [],
readyForLeaseDocument: false,
frozen: false,
privateAreas: [],
version: 0,
};
@Component({
selector: 'app-inventory-page',
imports: [RouterLink],
templateUrl: './inventory-page.html',
styleUrl: './inventory-page.css',
})
export class InventoryPage {
private readonly route = inject(ActivatedRoute);
private readonly inventories = inject(ConditionReportService);
readonly leaseId = this.route.snapshot.paramMap.get('leaseId') ?? '';
readonly inventory = signal<FurnitureInventory>(emptyInventory);
readonly loading = signal(true);
readonly saving = signal(false);
readonly error = signal('');
readonly success = signal('');
constructor() {
this.load();
}
load(): void {
this.loading.set(true);
this.error.set('');
this.success.set('');
this.inventories
.getInventory(this.leaseId)
.pipe(rxFinalize(() => this.loading.set(false)))
.subscribe({
next: (inventory) => this.inventory.set(inventory),
error: (error) =>
this.error.set(
httpErrorMessage(error, 'Impossible de charger l’inventaire du mobilier.'),
),
});
}
updateText(
id: string,
field: 'privateAreaId' | 'designation' | 'description',
event: Event,
): void {
const value =
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement ||
event.target instanceof HTMLSelectElement
? event.target.value
: '';
this.updateLine(id, { [field]: value });
}
updateQuantity(id: string, event: Event): void {
const input = event.target instanceof HTMLInputElement ? event.target : null;
const quantity =
!input || input.value === '' || !Number.isFinite(input.valueAsNumber)
? -1
: input.valueAsNumber;
this.updateLine(id, { quantity });
}
updatePresence(id: string, event: Event): void {
const line = this.inventory().lines.find((candidate) => candidate.id === id);
const value = event.target instanceof HTMLSelectElement ? event.target.value : '';
const presence: FurniturePresence | null =
value === 'PRESENT' || value === 'ABSENT' ? value : null;
if (!line?.category) return;
this.updateLine(id, {
presence,
quantity:
presence === 'ABSENT'
? 0
: presence === 'PRESENT' && line.quantity === 0
? 1
: line.quantity,
});
}
addLine(): void {
if (this.inventory().frozen || this.saving()) return;
this.inventory.update((inventory) => ({
...inventory,
lines: [
...inventory.lines,
{
id: crypto.randomUUID(),
category: null,
presence: null,
privateAreaId: '',
designation: '',
quantity: 1,
description: '',
},
],
}));
}
removeLine(id: string): void {
if (this.inventory().frozen || this.saving()) return;
this.inventory.update((inventory) => ({
...inventory,
lines: inventory.lines.filter((line) => line.id !== id || line.category !== null),
}));
}
warningLabel(category: string): string {
return (
this.inventory().lines.find((line) => line.category === category)?.designation ?? category
);
}
save(): void {
const inventory = this.inventory();
if (inventory.frozen) {
this.error.set('Cet inventaire est figé et ne peut plus être modifié.');
return;
}
const validationError = this.validationError(inventory.lines);
if (validationError) {
this.error.set(validationError);
return;
}
this.saving.set(true);
this.error.set('');
this.success.set('');
this.inventories
.saveInventory(this.leaseId, { version: inventory.version, lines: inventory.lines })
.pipe(rxFinalize(() => this.saving.set(false)))
.subscribe({
next: (saved) => {
this.inventory.set(saved);
this.success.set('Inventaire enregistré.');
},
error: (error) =>
this.error.set(httpErrorMessage(error, 'Impossible d’enregistrer l’inventaire.')),
});
}
private updateLine(id: string, changes: Partial<FurnitureInventoryLine>): void {
if (this.inventory().frozen || this.saving()) return;
this.inventory.update((inventory) => ({
...inventory,
lines: inventory.lines.map((line) => (line.id === id ? { ...line, ...changes } : line)),
}));
}
private validationError(lines: FurnitureInventoryLine[]): string | null {
for (const line of lines) {
if (!line.privateAreaId.trim()) return 'Renseignez la zone de chaque ligne.';
if (!line.designation.trim()) return 'Renseignez la désignation de chaque ligne.';
if (!Number.isInteger(line.quantity) || line.quantity < 0) {
return 'Saisissez une quantité entière positive ou nulle.';
}
if (line.category && line.presence === null) {
return 'Déclarez la présence ou l’absence de chaque élément réglementaire.';
}
if (line.category && line.presence === 'PRESENT' && line.quantity === 0) {
return 'Un élément réglementaire présent doit avoir une quantité positive.';
}
if (line.category && line.presence === 'ABSENT' && line.quantity !== 0) {
return 'Un élément réglementaire absent doit avoir une quantité nulle.';
}
}
return null;
}
}