288 lines
10 KiB
HTML
288 lines
10 KiB
HTML
<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>
|
||
Nature du complément
|
||
<select
|
||
[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="HEATING">Équipement de chauffage (premier mois de chauffe)</option>
|
||
</select>
|
||
</label>
|
||
<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>
|
||
@if (supplementType() === 'HEATING') {
|
||
<label>
|
||
Début réel de la période de chauffe
|
||
<input
|
||
type="date"
|
||
required
|
||
[value]="heatingPeriodStartedOn()"
|
||
(input)="heatingPeriodStartedOn.set($any($event.target).value)"
|
||
/>
|
||
</label>
|
||
}
|
||
@if (supplementIsLate()) {
|
||
<p role="alert">
|
||
La demande dépasse le délai indicatif (échéance : {{ supplementDeadline() }}). L’ajout
|
||
reste possible avec une confirmation auditée.
|
||
</p>
|
||
<label>
|
||
Motif du dépassement de délai
|
||
<textarea
|
||
required
|
||
[value]="supplementReason()"
|
||
(input)="supplementReason.set($any($event.target).value)"
|
||
></textarea>
|
||
</label>
|
||
<label>
|
||
<input
|
||
type="checkbox"
|
||
[checked]="lateWarningConfirmed()"
|
||
(change)="lateWarningConfirmed.set($any($event.target).checked)"
|
||
/>
|
||
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)"
|
||
>
|
||
<option value="">Choisir une zone</option>
|
||
@for (zone of value.zones; track zone.id) {
|
||
<option [value]="zone.id">{{ zone.label }}</option>
|
||
}
|
||
</select>
|
||
</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>
|
||
@for (photo of supplementPhotos(); track $index; let index = $index) {
|
||
<p>
|
||
{{ photo.file.name }} — {{ photo.zoneId }}
|
||
<button type="button" (click)="removeSupplementPhoto(index)">Retirer</button>
|
||
</p>
|
||
}
|
||
<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 }}</td>
|
||
<td>{{ line.exit }}</td>
|
||
<td>{{ line.changed ? 'Oui' : 'Non' }}</td>
|
||
<td>{{ line.absence }}</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>
|