feat(rental): add inventory and condition reports (#7)
This commit is contained in:
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>
|
||||
Reference in new issue
Block a user