Files
lmnp-organizer/frontend/src/app/components/condition-report-page/condition-report-evidence-section.ts
T
nessar 878da155bd chore: add husky pre-commit hook and lint-staged configuration
- Created a pre-commit hook in .husky to run lint-staged.
- Added .lintstagedrc.mjs to format frontend files with Prettier and apply Spotless to backend Java files.
- Introduced .prettierignore to exclude specific directories from formatting.
2026-08-17 14:18:16 +02:00

188 lines
5.5 KiB
TypeScript

import { Component, OnDestroy, inject, input, output, signal } from '@angular/core';
import { finalize as rxFinalize } from 'rxjs';
import {
ConditionPhoto,
ConditionReportService,
ConditionReportType,
EntryPhotoDisposition,
PropertyConditionReport,
} from '../../services/condition-report.service';
import { httpErrorMessage } from '../../services/http-error-message';
interface PendingPhoto {
file: File;
previewUrl: string;
}
@Component({
selector: 'app-condition-report-evidence-section',
templateUrl: './condition-report-evidence-section.html',
styleUrl: './condition-report-page.css',
})
export class ConditionReportEvidenceSection implements OnDestroy {
private readonly reports = inject(ConditionReportService);
readonly leaseId = input.required<string>();
readonly type = input.required<ConditionReportType>();
readonly report = input.required<PropertyConditionReport>();
readonly entryPhotos = input<ConditionPhoto[]>([]);
readonly editingDirty = input(false);
readonly busyPhotoZone = signal('');
readonly pendingPhotos = signal<Record<string, PendingPhoto>>({});
readonly reportChanged = output<PropertyConditionReport>();
readonly errorOccurred = output<string>();
readonly successOccurred = output<string>();
readonly reloadRequested = output<void>();
ngOnDestroy(): void {
Object.values(this.pendingPhotos()).forEach(({ previewUrl }) =>
URL.revokeObjectURL(previewUrl),
);
}
setZoneAbsence(zoneId: string, event: Event): void {
const reason = this.value(event);
this.update((report) => ({
...report,
zonePhotoAbsences: [
...report.zonePhotoAbsences.filter((absence) => absence.zoneId !== zoneId),
...(reason.trim()
? [{ zoneId, absence: { reason, author: '', recordedAt: '' } }]
: []),
],
}));
}
setEntryPhotoDisposition(
entryPhotoId: string,
field: keyof EntryPhotoDisposition,
event: Event,
): void {
const value = this.value(event);
this.update((report) => {
const current = report.entryPhotoDispositions.find(
(item) => item.entryPhotoId === entryPhotoId,
) ?? { entryPhotoId };
const next = { ...current, [field]: value || undefined };
return {
...report,
entryPhotoDispositions: [
...report.entryPhotoDispositions.filter(
(item) => item.entryPhotoId !== entryPhotoId,
),
next,
],
};
});
}
selectPhoto(zoneId: string, event: Event): void {
const input = event.target instanceof HTMLInputElement ? event.target : null;
const file = input?.files?.item(0) ?? null;
if (input) input.value = '';
if (!file) return;
const previous = this.pendingPhotos()[zoneId];
if (previous) URL.revokeObjectURL(previous.previewUrl);
this.pendingPhotos.update((photos) => ({
...photos,
[zoneId]: { file, previewUrl: URL.createObjectURL(file) },
}));
}
retakePhoto(zoneId: string): void {
const photo = this.pendingPhotos()[zoneId];
if (!photo) return;
URL.revokeObjectURL(photo.previewUrl);
this.pendingPhotos.update((photos) => {
const remaining = { ...photos };
delete remaining[zoneId];
return remaining;
});
}
uploadPhoto(zoneId: string): void {
const pending = this.pendingPhotos()[zoneId];
if (!pending || this.busyPhotoZone() || this.editingDirty()) return;
this.busyPhotoZone.set(zoneId);
this.reports
.uploadPhoto(this.leaseId(), this.type(), pending.file, zoneId)
.pipe(rxFinalize(() => this.busyPhotoZone.set('')))
.subscribe({
next: () => {
this.retakePhoto(zoneId);
this.reloadRequested.emit();
this.successOccurred.emit('Photo ajoutée.');
},
error: (error) =>
this.errorOccurred.emit(
httpErrorMessage(
error,
'La photo a été refusée. Reprenez le cliché puis réessayez.',
),
),
});
}
removePhoto(photo: ConditionPhoto): void {
if (this.busyPhotoZone() || this.editingDirty()) return;
this.busyPhotoZone.set(photo.zoneId);
this.reports
.deletePhoto(this.leaseId(), this.type(), photo.reference, this.report().version)
.pipe(rxFinalize(() => this.busyPhotoZone.set('')))
.subscribe({
next: () => this.reloadRequested.emit(),
error: (error) =>
this.errorOccurred.emit(
httpErrorMessage(error, 'Impossible de supprimer cette photo.'),
),
});
}
absenceReason(zoneId: string): string {
return (
this.report().zonePhotoAbsences.find((absence) => absence.zoneId === zoneId)?.absence
.reason ?? ''
);
}
disposition(entryPhotoId: string): EntryPhotoDisposition {
return (
this.report().entryPhotoDispositions.find(
(item) => item.entryPhotoId === entryPhotoId,
) ?? {
entryPhotoId,
}
);
}
exitPhotosFor(entryPhoto: ConditionPhoto): ConditionPhoto[] {
const report = this.report();
const current = this.disposition(entryPhoto.reference).exitPhotoId;
const used = new Set(
report.entryPhotoDispositions
.filter((item) => item.entryPhotoId !== entryPhoto.reference)
.map((item) => item.exitPhotoId)
.filter((reference): reference is string => Boolean(reference)),
);
return report.photos.filter(
(photo) =>
photo.zoneId === entryPhoto.zoneId &&
(photo.reference === current || !used.has(photo.reference)),
);
}
photoUrl(type: ConditionReportType, reference: string): string {
return this.reports.photoUrl(this.leaseId(), type, reference);
}
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
: '';
}
}