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