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
+151
@@ -0,0 +1,151 @@
|
||||
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, reason: string): void {
|
||||
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,
|
||||
value: string,
|
||||
): void {
|
||||
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.successOccurred.emit('Photo ajoutée.');
|
||||
this.reloadRequested.emit();
|
||||
},
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user