refactor(frontend): separate furniture inventory client (#7)

This commit is contained in:
nessar authored and Nessar committed 2026-08-17 14:18:16 +02:00
1 parent b030ac579a
commit ef04cbe45b
14 files changed
+263 -207

No files matched your search

@@ -2,16 +2,11 @@
<h2>Date et fuseau</h2>
<label>
Date civile
<input
type="date"
required
[value]="report().occurredOn"
(input)="setOccurredOn($any($event.target).value)"
/>
<input type="date" required [value]="report().occurredOn" (input)="setOccurredOn($event)" />
</label>
<label>
Fuseau IANA du navigateur
<input required [value]="report().timeZone" (input)="setTimeZone($any($event.target).value)" />
<input required [value]="report().timeZone" (input)="setTimeZone($event)" />
</label>
</section>
@@ -27,7 +22,7 @@
Présence
<select
[value]="participant.attendance"
(change)="updateParticipant(index, 'attendance', $any($event.target).value)"
(change)="updateParticipant(index, 'attendance', $event)"
>
<option value="PRESENT">Présent</option>
<option value="REPRESENTED">Représenté</option>
@@ -39,7 +34,7 @@
<input
required
[value]="participant.representativeName"
(input)="updateParticipant(index, 'representativeName', $any($event.target).value)"
(input)="updateParticipant(index, 'representativeName', $event)"
/>
</label>
}
@@ -57,7 +52,7 @@
<select
required
[value]="zone.privateAreaId"
(change)="updateZone(zone.id, 'privateAreaId', $any($event.target).value)"
(change)="updateZone(zone.id, 'privateAreaId', $event)"
>
@for (area of privateAreas(); track area.id) {
<option [value]="area.id">{{ area.label }}</option>
@@ -66,18 +61,11 @@
</label>
<label>
Libellé
<input
required
[value]="zone.label"
(input)="updateZone(zone.id, 'label', $any($event.target).value)"
/>
<input required [value]="zone.label" (input)="updateZone(zone.id, 'label', $event)" />
</label>
<label>
Équipement
<select
[value]="zone.equipment"
(change)="updateZone(zone.id, 'equipment', $any($event.target).value)"
>
<select [value]="zone.equipment" (change)="updateZone(zone.id, 'equipment', $event)">
<option value="NONE">Aucun</option>
<option value="CLIMATISATION">Climatisation</option>
<option value="OTHER">Autre</option>
@@ -96,7 +84,7 @@
État
<select
[value]="observation.state"
(change)="updateObservation(observation.subjectId, 'state', $any($event.target).value)"
(change)="updateObservation(observation.subjectId, 'state', $event)"
>
@for (state of states; track state) {
<option [value]="state">{{ state }}</option>
@@ -111,9 +99,7 @@
<textarea
[required]="needsObservation(observation.state)"
[value]="observation.observation"
(input)="
updateObservation(observation.subjectId, 'observation', $any($event.target).value)
"
(input)="updateObservation(observation.subjectId, 'observation', $event)"
></textarea>
</label>
</fieldset>
@@ -20,23 +20,19 @@ export class ConditionReportDetailsSection {
readonly reportChanged = output<PropertyConditionReport>();
readonly states = states;
setOccurredOn(occurredOn: string): void {
this.update((report) => ({ ...report, occurredOn }));
setOccurredOn(event: Event): void {
this.update((report) => ({ ...report, occurredOn: this.value(event) }));
}
setTimeZone(timeZone: string): void {
this.update((report) => ({ ...report, timeZone }));
setTimeZone(event: Event): void {
this.update((report) => ({ ...report, timeZone: this.value(event) }));
}
updateParticipant(
index: number,
field: 'attendance' | 'representativeName',
value: string,
): void {
updateParticipant(index: number, field: 'attendance' | 'representativeName', event: Event): void {
this.update((report) => ({
...report,
participants: report.participants.map((participant, item) =>
item === index ? { ...participant, [field]: value } : participant,
item === index ? { ...participant, [field]: this.value(event) } : participant,
),
}));
}
@@ -53,10 +49,12 @@ export class ConditionReportDetailsSection {
}));
}
updateZone(id: string, field: 'privateAreaId' | 'label' | 'equipment', value: string): void {
updateZone(id: string, field: 'privateAreaId' | 'label' | 'equipment', event: Event): void {
this.update((report) => ({
...report,
zones: report.zones.map((zone) => (zone.id === id ? { ...zone, [field]: value } : zone)),
zones: report.zones.map((zone) =>
zone.id === id ? { ...zone, [field]: this.value(event) } : zone,
),
}));
}
@@ -76,11 +74,11 @@ export class ConditionReportDetailsSection {
);
}
updateObservation(id: string, field: 'state' | 'observation', value: string): void {
updateObservation(id: string, field: 'state' | 'observation', event: Event): void {
this.update((report) => ({
...report,
observations: report.observations.map((observation) =>
observation.subjectId === id ? { ...observation, [field]: value } : observation,
observation.subjectId === id ? { ...observation, [field]: this.value(event) } : observation,
),
}));
}
@@ -109,4 +107,13 @@ export class ConditionReportDetailsSection {
private update(change: (report: PropertyConditionReport) => PropertyConditionReport): void {
this.reportChanged.emit(change(this.report()));
}
private value(event: Event): string {
const target = event.target;
return target instanceof HTMLInputElement ||
target instanceof HTMLSelectElement ||
target instanceof HTMLTextAreaElement
? target.value
: '';
}
}
@@ -6,10 +6,7 @@
<h3>{{ zone.label }}</h3>
<label>
Motif d’absence
<input
[value]="absenceReason(zone.id)"
(input)="setZoneAbsence(zone.id, $any($event.target).value)"
/>
<input [value]="absenceReason(zone.id)" (input)="setZoneAbsence(zone.id, $event)" />
</label>
<label>
Prendre ou choisir une photo
@@ -72,13 +69,7 @@
Photo de sortie correspondante
<select
[value]="disposition(entryPhoto.reference).exitPhotoId ?? ''"
(change)="
setEntryPhotoDisposition(
entryPhoto.reference,
'exitPhotoId',
$any($event.target).value
)
"
(change)="setEntryPhotoDisposition(entryPhoto.reference, 'exitPhotoId', $event)"
>
<option value="">Aucune</option>
@for (exitPhoto of report().photos; track exitPhoto.reference) {
@@ -90,13 +81,7 @@
Motif d’absence
<input
[value]="disposition(entryPhoto.reference).absenceReason ?? ''"
(input)="
setEntryPhotoDisposition(
entryPhoto.reference,
'absenceReason',
$any($event.target).value
)
"
(input)="setEntryPhotoDisposition(entryPhoto.reference, 'absenceReason', $event)"
/>
</label>
</article>
@@ -39,7 +39,8 @@ export class ConditionReportEvidenceSection implements OnDestroy {
);
}
setZoneAbsence(zoneId: string, reason: string): void {
setZoneAbsence(zoneId: string, event: Event): void {
const reason = this.value(event);
this.update((report) => ({
...report,
zonePhotoAbsences: [
@@ -52,8 +53,9 @@ export class ConditionReportEvidenceSection implements OnDestroy {
setEntryPhotoDisposition(
entryPhotoId: string,
field: keyof EntryPhotoDisposition,
value: string,
event: Event,
): void {
const value = this.value(event);
this.update((report) => {
const current = report.entryPhotoDispositions.find(
(item) => item.entryPhotoId === entryPhotoId,
@@ -148,4 +150,11 @@ export class ConditionReportEvidenceSection implements OnDestroy {
private update(change: (report: PropertyConditionReport) => PropertyConditionReport): void {
this.reportChanged.emit(change(this.report()));
}
private value(event: Event): string {
const target = event.target;
return target instanceof HTMLInputElement || target instanceof HTMLSelectElement
? target.value
: '';
}
}
@@ -5,10 +5,7 @@
<legend>{{ meter.label || meter.type }}</legend>
<label>
Type
<select
[value]="meter.type"
(change)="updateMeter(meter.id, 'type', $any($event.target).value)"
>
<select [value]="meter.type" (change)="updateMeter(meter.id, 'type', $event)">
<option value="ELECTRICITE">Électricité</option>
<option value="GAZ">Gaz</option>
<option value="EAU_FROIDE">Eau froide</option>
@@ -21,26 +18,19 @@
<input
[required]="meter.type === 'OTHER'"
[value]="meter.label"
(input)="updateMeter(meter.id, 'label', $any($event.target).value)"
(input)="updateMeter(meter.id, 'label', $event)"
/>
</label>
<label>
Relevé
<select
[value]="meter.state"
(change)="updateMeter(meter.id, 'state', $any($event.target).value)"
>
<select [value]="meter.state" (change)="updateMeter(meter.id, 'state', $event)">
<option value="RECORDED">Index relevé</option>
<option value="INACCESSIBLE">Compteur inaccessible</option>
</select>
</label>
<label>
Unité
<input
required
[value]="meter.unit"
(input)="updateMeter(meter.id, 'unit', $any($event.target).value)"
/>
<input required [value]="meter.unit" (input)="updateMeter(meter.id, 'unit', $event)" />
</label>
@if (meter.state === 'RECORDED') {
<label>
@@ -51,16 +41,13 @@
step="any"
required
[value]="meter.index"
(input)="updateMeter(meter.id, 'index', $any($event.target).value)"
(input)="updateMeter(meter.id, 'index', $event)"
/>
</label>
}
<label>
Référence photo
<input
[value]="meter.photoId"
(input)="updateMeter(meter.id, 'photoId', $any($event.target).value)"
/>
<input [value]="meter.photoId" (input)="updateMeter(meter.id, 'photoId', $event)" />
</label>
<label>
Observation
@@ -70,7 +57,7 @@
<textarea
[required]="meter.state === 'INACCESSIBLE'"
[value]="meter.observation"
(input)="updateMeter(meter.id, 'observation', $any($event.target).value)"
(input)="updateMeter(meter.id, 'observation', $event)"
></textarea>
</label>
<button type="button" (click)="removeMeter(meter.id)">Supprimer ce compteur</button>
@@ -86,10 +73,7 @@
<legend>{{ access.destination || 'Accès' }}</legend>
<label>
Type
<select
[value]="access.type"
(change)="updateAccessMean(access.id, 'type', $any($event.target).value)"
>
<select [value]="access.type" (change)="updateAccessMean(access.id, 'type', $event)">
<option value="CLE">Clé</option>
<option value="BADGE">Badge</option>
<option value="TELECOMMANDE">Télécommande</option>
@@ -101,7 +85,7 @@
<input
required
[value]="access.destination"
(input)="updateAccessMean(access.id, 'destination', $any($event.target).value)"
(input)="updateAccessMean(access.id, 'destination', $event)"
/>
</label>
<label>
@@ -112,14 +96,14 @@
step="1"
required
[value]="access.quantity"
(input)="updateAccessMean(access.id, 'quantity', $any($event.target).value)"
(input)="updateAccessMean(access.id, 'quantity', $event)"
/>
</label>
<label>
Observation
<textarea
[value]="access.observation"
(input)="updateAccessMean(access.id, 'observation', $any($event.target).value)"
(input)="updateAccessMean(access.id, 'observation', $event)"
></textarea>
</label>
<button type="button" (click)="removeAccessMean(access.id)">
@@ -148,9 +132,7 @@
État
<select
[value]="split.state"
(change)="
updateInventorySplit(conditionIndex, splitIndex, 'state', $any($event.target).value)
"
(change)="updateInventorySplit(conditionIndex, splitIndex, 'state', $event)"
>
@for (state of states; track state) {
<option [value]="state">{{ state }}</option>
@@ -165,14 +147,7 @@
step="1"
required
[value]="split.quantity"
(input)="
updateInventorySplit(
conditionIndex,
splitIndex,
'quantity',
$any($event.target).value
)
"
(input)="updateInventorySplit(conditionIndex, splitIndex, 'quantity', $event)"
/>
</label>
<label>
@@ -183,14 +158,7 @@
<textarea
[required]="needsObservation(split.state)"
[value]="split.observation"
(input)="
updateInventorySplit(
conditionIndex,
splitIndex,
'observation',
$any($event.target).value
)
"
(input)="updateInventorySplit(conditionIndex, splitIndex, 'observation', $event)"
></textarea>
</label>
<button type="button" (click)="removeInventorySplit(conditionIndex, splitIndex)">
@@ -215,7 +183,7 @@
<textarea
required
[value]="forwardingAddress(participant.partyId)"
(input)="updateForwardingAddress(participant.partyId, $any($event.target).value)"
(input)="updateForwardingAddress(participant.partyId, $event)"
></textarea>
</label>
}
@@ -48,7 +48,8 @@ export class ConditionReportInventorySection {
}));
}
updateMeter(id: string, field: keyof MeterReading, value: string): void {
updateMeter(id: string, field: keyof MeterReading, event: Event): void {
const value = this.value(event);
this.update((report) => ({
...report,
meterReadings: report.meterReadings.map((meter) =>
@@ -82,7 +83,8 @@ export class ConditionReportInventorySection {
}));
}
updateAccessMean(id: string, field: keyof AccessMeans, value: string): void {
updateAccessMean(id: string, field: keyof AccessMeans, event: Event): void {
const value = this.value(event);
this.update((report) => ({
...report,
accessMeans: report.accessMeans.map((access) =>
@@ -119,8 +121,9 @@ export class ConditionReportInventorySection {
conditionIndex: number,
splitIndex: number,
field: keyof InventoryQuantitySplit,
value: string,
event: Event,
): void {
const value = this.value(event);
this.updateInventoryCondition(conditionIndex, (condition) => ({
...condition,
splits: condition.splits.map((split, item) =>
@@ -144,7 +147,8 @@ export class ConditionReportInventorySection {
}));
}
updateForwardingAddress(tenantPartyId: string, address: string): void {
updateForwardingAddress(tenantPartyId: string, event: Event): void {
const address = this.value(event);
this.update((report) => ({
...report,
forwardingAddresses: [
@@ -171,6 +175,15 @@ export class ConditionReportInventorySection {
this.reportChanged.emit(change(this.report()));
}
private value(event: Event): string {
const target = event.target;
return target instanceof HTMLInputElement ||
target instanceof HTMLSelectElement ||
target instanceof HTMLTextAreaElement
? target.value
: '';
}
private updateInventoryCondition(
index: number,
change: (condition: InventoryCondition) => InventoryCondition,
@@ -62,7 +62,7 @@
type="checkbox"
[checked]="finalizationAttested()"
[disabled]="finalizing()"
(change)="finalizationAttested.set($any($event.target).checked)"
(change)="setFinalizationAttested($event)"
/>
J’atteste que le scan signé correspond à la dernière révision générée.
</label>
@@ -71,7 +71,7 @@
type="checkbox"
[checked]="allPartiesSigned()"
[disabled]="finalizing()"
(change)="allPartiesSigned.set($any($event.target).checked)"
(change)="setAllPartiesSigned($event)"
/>
J’atteste que chaque partie présente ou son mandataire identifié a signé le document.
</label>
@@ -112,10 +112,7 @@
@if (type === 'ENTRY') {
<label>
Nature du complément
<select
[value]="supplementType()"
(change)="supplementType.set($any($event.target).value)"
>
<select [value]="supplementType()" (change)="setSupplementType($event)">
<option value="GENERAL">Constat général (délai indicatif de 10 jours)</option>
<option value="HEATING">Équipement de chauffage (premier mois de chauffe)</option>
</select>
@@ -126,7 +123,7 @@
type="date"
required
[value]="supplementRequestedOn()"
(input)="supplementRequestedOn.set($any($event.target).value)"
(input)="setSupplementRequestedOn($event)"
/>
</label>
<label>
@@ -134,7 +131,7 @@
<textarea
required
[value]="supplementAcceptedAdditions()"
(input)="supplementAcceptedAdditions.set($any($event.target).value)"
(input)="setSupplementAcceptedAdditions($event)"
></textarea>
</label>
@if (supplementType() === 'HEATING') {
@@ -144,7 +141,7 @@
type="date"
required
[value]="heatingPeriodStartedOn()"
(input)="heatingPeriodStartedOn.set($any($event.target).value)"
(input)="setHeatingPeriodStartedOn($event)"
/>
</label>
}
@@ -158,24 +155,21 @@
<textarea
required
[value]="supplementReason()"
(input)="supplementReason.set($any($event.target).value)"
(input)="setSupplementReason($event)"
></textarea>
</label>
<label>
<input
type="checkbox"
[checked]="lateWarningConfirmed()"
(change)="lateWarningConfirmed.set($any($event.target).checked)"
(change)="setLateWarningConfirmed($event)"
/>
Je confirme l’ajout malgré le dépassement du délai indicatif.
</label>
}
<label>
Zone des prochaines photos
<select
[value]="supplementPhotoZoneId()"
(change)="supplementPhotoZoneId.set($any($event.target).value)"
>
<select [value]="supplementPhotoZoneId()" (change)="setSupplementPhotoZoneId($event)">
<option value="">Choisir une zone</option>
@for (zone of value.zones; track zone.id) {
<option [value]="zone.id">{{ zone.label }}</option>
@@ -72,6 +72,12 @@ function eventFor(target: EventTarget): Event {
return event;
}
function valueEvent(value: string): Event {
const input = document.createElement('input');
input.value = value;
return eventFor(input);
}
describe('ConditionReportEditor', () => {
it('uses a rear-camera input and explains an invalid degraded observation before saving', () => {
const service = {
@@ -368,13 +374,13 @@ describe('Condition-report sections', () => {
fixture.componentRef.setInput('report', next);
});
section.setOccurredOn('2026-07-23');
section.setTimeZone('Europe/Paris');
section.updateParticipant(0, 'attendance', 'REPRESENTED');
section.updateParticipant(0, 'representativeName', 'Alex Martin');
section.updateZone('kitchen', 'label', 'Cuisine équipée');
section.updateObservation('kitchen', 'state', 'DEGRADE');
section.updateObservation('kitchen', 'observation', 'Rayure');
section.setOccurredOn(valueEvent('2026-07-23'));
section.setTimeZone(valueEvent('Europe/Paris'));
section.updateParticipant(0, 'attendance', valueEvent('REPRESENTED'));
section.updateParticipant(0, 'representativeName', valueEvent('Alex Martin'));
section.updateZone('kitchen', 'label', valueEvent('Cuisine équipée'));
section.updateObservation('kitchen', 'state', valueEvent('DEGRADE'));
section.updateObservation('kitchen', 'observation', valueEvent('Rayure'));
section.addZone();
const added = current.zones.find((zone) => zone.id !== 'kitchen');
@@ -446,20 +452,20 @@ describe('Condition-report sections', () => {
fixture.componentRef.setInput('report', next);
});
section.updateMeter('meter-1', 'state', 'INACCESSIBLE');
section.updateMeter('meter-1', 'state', valueEvent('INACCESSIBLE'));
expect(current.meterReadings[0]?.index).toBeNull();
section.updateMeter('meter-1', 'state', 'RECORDED');
section.updateMeter('meter-1', 'index', '12.5');
section.updateMeter('meter-1', 'state', valueEvent('RECORDED'));
section.updateMeter('meter-1', 'index', valueEvent('12.5'));
section.addMeter();
section.removeMeter('meter-1');
section.updateAccessMean('key-1', 'quantity', '2');
section.updateAccessMean('key-1', 'quantity', valueEvent('2'));
section.addAccessMean();
section.removeAccessMean('key-1');
section.addInventorySplit(0);
section.updateInventorySplit(0, 1, 'quantity', '0');
section.updateInventorySplit(0, 1, 'state', 'ABSENT');
section.updateInventorySplit(0, 1, 'quantity', valueEvent('0'));
section.updateInventorySplit(0, 1, 'state', valueEvent('ABSENT'));
section.removeInventorySplit(0, 1);
section.updateForwardingAddress('tenant-1', '12 rue A');
section.updateForwardingAddress('tenant-1', valueEvent('12 rue A'));
expect(current.meterReadings).toHaveLength(1);
expect(current.meterReadings[0]?.index).toBe(0);
@@ -503,10 +509,10 @@ describe('Condition-report sections', () => {
const file = new File(['photo'], 'cuisine.webp', { type: 'image/webp' });
Object.defineProperty(input, 'files', { value: { item: () => file } });
section.setZoneAbsence('kitchen', 'Accès impossible');
section.setZoneAbsence('kitchen', '');
section.setEntryPhotoDisposition('entry-photo', 'exitPhotoId', 'exit-photo');
section.setEntryPhotoDisposition('entry-photo', 'absenceReason', '');
section.setZoneAbsence('kitchen', valueEvent('Accès impossible'));
section.setZoneAbsence('kitchen', valueEvent(''));
section.setEntryPhotoDisposition('entry-photo', 'exitPhotoId', valueEvent('exit-photo'));
section.setEntryPhotoDisposition('entry-photo', 'absenceReason', valueEvent(''));
section.selectPhoto('kitchen', { target: input } as unknown as Event);
section.uploadPhoto('kitchen');
section.removePhoto({
@@ -171,6 +171,42 @@ export class ConditionReportPage {
this.signedScan.set(file);
}
setFinalizationAttested(event: Event): void {
this.finalizationAttested.set(this.checked(event));
}
setAllPartiesSigned(event: Event): void {
this.allPartiesSigned.set(this.checked(event));
}
setSupplementType(event: Event): void {
this.supplementType.set(this.value(event) === 'HEATING' ? 'HEATING' : 'GENERAL');
}
setSupplementRequestedOn(event: Event): void {
this.supplementRequestedOn.set(this.value(event));
}
setSupplementAcceptedAdditions(event: Event): void {
this.supplementAcceptedAdditions.set(this.value(event));
}
setHeatingPeriodStartedOn(event: Event): void {
this.heatingPeriodStartedOn.set(this.value(event));
}
setSupplementReason(event: Event): void {
this.supplementReason.set(this.value(event));
}
setLateWarningConfirmed(event: Event): void {
this.lateWarningConfirmed.set(this.checked(event));
}
setSupplementPhotoZoneId(event: Event): void {
this.supplementPhotoZoneId.set(this.value(event));
}
finalize(): void {
const report = this.report();
const scan = this.signedScan();
@@ -337,6 +373,19 @@ export class ConditionReportPage {
photoUrl(type: ConditionReportType, reference: string): string {
return this.reports.photoUrl(this.leaseId, type, reference);
}
private value(event: Event): string {
const target = event.target;
return target instanceof HTMLInputElement ||
target instanceof HTMLSelectElement ||
target instanceof HTMLTextAreaElement
? target.value
: '';
}
private checked(event: Event): boolean {
return event.target instanceof HTMLInputElement && event.target.checked;
}
}
interface ComparisonDisplayLine {
@@ -3,9 +3,9 @@ import { ActivatedRoute, provideRouter } from '@angular/router';
import { of, throwError } from 'rxjs';
import { vi } from 'vitest';
import {
ConditionReportService,
FurnitureInventory,
} from '../../services/condition-report.service';
FurnitureInventoryService,
} from '../../services/furniture-inventory.service';
import { InventoryPage } from './inventory-page';
const inventory: FurnitureInventory = {
@@ -51,7 +51,7 @@ describe('InventoryPage', () => {
provide: ActivatedRoute,
useValue: { snapshot: { paramMap: { get: () => 'lease-1' } } },
},
{ provide: ConditionReportService, useValue: service },
{ provide: FurnitureInventoryService, useValue: service },
],
});
const fixture = TestBed.createComponent(InventoryPage);
@@ -141,7 +141,7 @@ describe('InventoryPage', () => {
provide: ActivatedRoute,
useValue: { snapshot: { paramMap: { get: () => 'lease-1' } } },
},
{ provide: ConditionReportService, useValue: service },
{ provide: FurnitureInventoryService, useValue: service },
],
});
const fixture = TestBed.createComponent(InventoryPage);
@@ -2,11 +2,11 @@ import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { finalize as rxFinalize } from 'rxjs';
import {
ConditionReportService,
FurnitureInventory,
FurnitureInventoryLine,
FurnitureInventoryService,
FurniturePresence,
} from '../../services/condition-report.service';
} from '../../services/furniture-inventory.service';
import { httpErrorMessage } from '../../services/http-error-message';
const emptyInventory: FurnitureInventory = {
@@ -30,7 +30,7 @@ const emptyInventory: FurnitureInventory = {
})
export class InventoryPage {
private readonly route = inject(ActivatedRoute);
private readonly inventories = inject(ConditionReportService);
private readonly inventories = inject(FurnitureInventoryService);
readonly leaseId = this.route.snapshot.paramMap.get('leaseId') ?? '';
readonly inventory = signal<FurnitureInventory>(emptyInventory);
readonly loading = signal(true);
@@ -12,19 +12,6 @@ export type ZoneEquipment = 'NONE' | 'CLIMATISATION' | 'OTHER';
export type MeterType = 'ELECTRICITE' | 'GAZ' | 'EAU_FROIDE' | 'EAU_CHAUDE' | 'OTHER';
export type MeterReadingState = 'RECORDED' | 'INACCESSIBLE';
export type AccessMeansType = 'CLE' | 'BADGE' | 'TELECOMMANDE' | 'OTHER';
export type FurniturePresence = 'PRESENT' | 'ABSENT';
export type FurnitureCategory =
| 'BEDDING'
| 'SHUTTERS'
| 'HOTPLATES'
| 'OVEN_OR_MICROWAVE'
| 'REFRIGERATOR_WITH_FREEZER'
| 'DISHES'
| 'UTENSILS'
| 'TABLE_AND_SEATING'
| 'STORAGE'
| 'LIGHTING'
| 'CLEANING';
export interface ReportParticipant {
partyId: string;
@@ -206,41 +193,6 @@ export interface SaveConditionReportRequest {
entryPhotoDispositions: EntryPhotoDisposition[];
}
export interface FurnitureInventoryLine {
id: string;
category: FurnitureCategory | null;
presence: FurniturePresence | null;
equipmentKind: 'NONE' | 'CLIMATISATION' | 'OTHER';
privateAreaId: string;
designation: string;
quantity: number;
description: string;
}
export interface FurnitureInventoryWarning {
code: string;
ruleVersion: string;
category: FurnitureCategory;
}
export interface FurnitureInventory {
id: string;
templateVersion: string;
lines: FurnitureInventoryLine[];
missingCategories: FurnitureCategory[];
absentCategories: FurnitureCategory[];
legalWarnings: FurnitureInventoryWarning[];
readyForLeaseDocument: boolean;
frozen: boolean;
privateAreas: { id: string; label: string }[];
version: number;
}
export interface SaveFurnitureInventoryRequest {
version: number;
lines: FurnitureInventoryLine[];
}
export interface ReportFinalizationRequest {
signedReport: File;
version: number;
@@ -280,17 +232,6 @@ export interface ConditionReportComparison {
export class ConditionReportService {
private readonly http = inject(HttpClient);
getInventory(leaseId: string): Observable<FurnitureInventory> {
return this.http.get<FurnitureInventory>(`/api/leases/${leaseId}/inventory`);
}
saveInventory(
leaseId: string,
request: SaveFurnitureInventoryRequest,
): Observable<FurnitureInventory> {
return this.http.put<FurnitureInventory>(`/api/leases/${leaseId}/inventory`, request);
}
getReport(
leaseId: string,
type: ConditionReportType,
@@ -0,0 +1,30 @@
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { FurnitureInventoryService } from './furniture-inventory.service';
describe('FurnitureInventoryService', () => {
let service: FurnitureInventoryService;
let http: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
service = TestBed.inject(FurnitureInventoryService);
http = TestBed.inject(HttpTestingController);
});
afterEach(() => http.verify());
it('uses the versioned furniture-inventory contract', () => {
service.getInventory('lease-1').subscribe();
http.expectOne('/api/leases/lease-1/inventory').flush({});
service.saveInventory('lease-1', { version: 4, lines: [] }).subscribe();
const request = http.expectOne('/api/leases/lease-1/inventory');
expect(request.request.method).toBe('PUT');
expect(request.request.body).toEqual({ version: 4, lines: [] });
request.flush({});
});
});
@@ -0,0 +1,68 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
export type FurniturePresence = 'PRESENT' | 'ABSENT';
export type FurnitureCategory =
| 'BEDDING'
| 'SHUTTERS'
| 'HOTPLATES'
| 'OVEN_OR_MICROWAVE'
| 'REFRIGERATOR_WITH_FREEZER'
| 'DISHES'
| 'UTENSILS'
| 'TABLE_AND_SEATING'
| 'STORAGE'
| 'LIGHTING'
| 'CLEANING';
export interface FurnitureInventoryLine {
id: string;
category: FurnitureCategory | null;
presence: FurniturePresence | null;
equipmentKind: 'NONE' | 'CLIMATISATION' | 'OTHER';
privateAreaId: string;
designation: string;
quantity: number;
description: string;
}
export interface FurnitureInventoryWarning {
code: string;
ruleVersion: string;
category: FurnitureCategory;
}
export interface FurnitureInventory {
id: string;
templateVersion: string;
lines: FurnitureInventoryLine[];
missingCategories: FurnitureCategory[];
absentCategories: FurnitureCategory[];
legalWarnings: FurnitureInventoryWarning[];
readyForLeaseDocument: boolean;
frozen: boolean;
privateAreas: { id: string; label: string }[];
version: number;
}
export interface SaveFurnitureInventoryRequest {
version: number;
lines: FurnitureInventoryLine[];
}
@Injectable({ providedIn: 'root' })
export class FurnitureInventoryService {
private readonly http = inject(HttpClient);
getInventory(leaseId: string): Observable<FurnitureInventory> {
return this.http.get<FurnitureInventory>(`/api/leases/${leaseId}/inventory`);
}
saveInventory(
leaseId: string,
request: SaveFurnitureInventoryRequest,
): Observable<FurnitureInventory> {
return this.http.put<FurnitureInventory>(`/api/leases/${leaseId}/inventory`, request);
}
}