- 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.
188 lines
5.5 KiB
TypeScript
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
|
|
: '';
|
|
}
|
|
}
|