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,241 @@
<section class="form-page" [attr.aria-busy]="loading() || saving() || finalizing()">
<a routerLink="/leases">← Retour aux baux</a>
<header class="page-heading">
<div>
<p class="eyebrow">État des lieux</p>
<h1>{{ type === 'ENTRY' ? 'Entrée' : 'Sortie' }}</h1>
<p>Constat contradictoire, photos de preuve et remise aux parties.</p>
</div>
</header>
@if (loading()) {
<p role="status">Chargement de l’état des lieux…</p>
} @else if (error() && !report()) {
<p role="alert" class="operation-error">{{ error() }}</p>
<button type="button" (click)="load()">Réessayer</button>
} @else if (report(); as value) {
@if (error()) {
<p role="alert" class="operation-error">{{ error() }}</p>
}
@if (success()) {
<p role="status">{{ success() }}</p>
}
@if (value.leaseRevisionNumber !== null) {
<p>
Révision de bail source {{ value.leaseRevisionNumber }}
@if (value.leaseRevisionFingerprint) {
· empreinte {{ value.leaseRevisionFingerprint }}
}
</p>
}
<app-condition-report-editor
[leaseId]="leaseId"
[type]="type"
[report]="value"
[entryPhotos]="entryPhotos()"
[dirty]="dirty()"
[saving]="saving()"
[finalizing]="finalizing()"
(reportChanged)="changeReport($event)"
(saveRequested)="save($event)"
(errorOccurred)="error.set($event)"
(successOccurred)="success.set($event)"
(reloadRequested)="load()"
/>
<section>
<h2>Révision et finalisation</h2>
@if (value.status === 'DRAFT') {
<button type="button" [disabled]="revisionPending() || dirty()" (click)="createRevision()">
{{ revisionPending() ? 'Génération…' : 'Générer et télécharger la révision PDF' }}
</button>
<label>
Scan signé PDF
<input type="file" accept="application/pdf,.pdf" (change)="selectSignedScan($event)" />
</label>
@if (signedScan(); as scan) {
<p>{{ scan.name }} · attestation de correspondance incluse à la finalisation.</p>
}
<label>
<input
type="checkbox"
[checked]="finalizationAttested()"
[disabled]="finalizing()"
(change)="finalizationAttested.set($any($event.target).checked)"
/>
J’atteste que le scan signé correspond à la dernière révision générée.
</label>
<label>
<input
type="checkbox"
[checked]="allPartiesSigned()"
[disabled]="finalizing()"
(change)="allPartiesSigned.set($any($event.target).checked)"
/>
J’atteste que chaque partie présente ou son mandataire identifié a signé le document.
</label>
<button
type="button"
[disabled]="
finalizing() ||
!signedScan() ||
!finalizationAttested() ||
!allPartiesSigned() ||
dirty()
"
(click)="finalize()"
>
{{ finalizing() ? 'Finalisation…' : 'Finaliser l’état des lieux' }}
</button>
} @else if (value.status === 'FINALIZED') {
<p>État des lieux finalisé.</p>
<button type="button" (click)="downloadBundle()">Télécharger le dossier ZIP</button>
<h3>Attestations de remise</h3>
@for (participant of value.participants; track participant.partyId) {
@if (hasAttested(participant.partyId)) {
<p>{{ participant.name }} : remise attestée.</p>
} @else {
<button
type="button"
[disabled]="!!busyAttestationPartyId()"
(click)="attestDelivery(participant.partyId)"
>
{{
busyAttestationPartyId() === participant.partyId
? 'Attestation…'
: 'Attester la remise à ' + participant.name
}}
</button>
}
}
@if (type === 'ENTRY') {
<label>
Date de demande du complément
<input
type="date"
required
[value]="supplementRequestedOn()"
(input)="supplementRequestedOn.set($any($event.target).value)"
/>
</label>
<label>
Ajouts acceptés
<textarea
required
[value]="supplementAcceptedAdditions()"
(input)="supplementAcceptedAdditions.set($any($event.target).value)"
></textarea>
</label>
<label>
Début de la période de chauffe (facultatif)
<input
type="date"
[value]="heatingPeriodStartedOn()"
(input)="heatingPeriodStartedOn.set($any($event.target).value)"
/>
</label>
<label>
Motif du dépassement de délai
<textarea
[value]="supplementReason()"
(input)="supplementReason.set($any($event.target).value)"
></textarea>
</label>
<label>
Photos du complément (facultatives)
<input
type="file"
accept="image/jpeg,image/png,image/webp,image/heic,image/heif"
multiple
[disabled]="supplementPending()"
(change)="selectSupplementPhotos($event)"
/>
</label>
<label>
Complément signé PDF (obligatoire)
<input
type="file"
accept="application/pdf,.pdf"
[disabled]="supplementPending()"
(change)="selectSupplementSignedPdf($event)"
/>
</label>
@if (supplementSignedPdf(); as pdf) {
<p>{{ pdf.name }} · {{ supplementPhotos().length }} photo(s).</p>
}
<button
type="button"
[disabled]="supplementPending() || !supplementSignedPdf()"
(click)="appendSupplement()"
>
{{ supplementPending() ? 'Ajout…' : 'Ajouter le complément signé' }}
</button>
}
} @else {
<p role="status">Ce brouillon est obsolète car la révision du bail a changé.</p>
}
</section>
@if (type === 'EXIT') {
<section>
<h2>Comparaison structurée</h2>
<p>Les constats sont alignés sans responsabilité ni montant.</p>
<button type="button" [disabled]="comparisonPending()" (click)="loadComparison()">
{{ comparisonPending() ? 'Comparaison…' : 'Comparer entrée et sortie' }}
</button>
@for (section of comparisonSections(); track section.title) {
<h3>{{ section.title }}</h3>
<table>
<thead>
<tr>
<th>Référence</th>
<th>Entrée</th>
<th>Sortie</th>
<th>Changement</th>
<th>Motif d’absence</th>
</tr>
</thead>
<tbody>
@for (line of section.lines; track line.reference) {
<tr>
<th>{{ line.reference }}</th>
<td>{{ line.entry | json }}</td>
<td>{{ line.exit | json }}</td>
<td>{{ line.changed ? 'Oui' : 'Non' }}</td>
<td>{{ line.absence | json }}</td>
</tr>
}
</tbody>
</table>
}
@if (comparison(); as compared) {
<h3>Photos</h3>
@for (line of compared.photos; track line.reference) {
<article class="photo-comparison">
<div>
<h4>Entrée</h4>
@if (line.entry) {
<img [src]="photoUrl('ENTRY', line.entry.reference)" alt="Photo d’entrée" />
} @else {
<p>Aucune photo d’entrée.</p>
}
</div>
<div>
<h4>Sortie</h4>
@if (line.exit) {
<img [src]="photoUrl('EXIT', line.exit.reference)" alt="Photo de sortie" />
} @else {
<p>Aucune photo de sortie.</p>
}
</div>
@if (line.absence) {
<p>Motif d’absence : {{ line.absence.reason }}</p>
}
</article>
}
}
</section>
}
}
</section>