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,405 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export type ConditionReportType = 'ENTRY' | 'EXIT';
|
||||
export type ConditionReportStatus = 'DRAFT' | 'OBSOLETE' | 'FINALIZED';
|
||||
export type ConditionState =
|
||||
'NEUF' | 'BON_ETAT' | 'ETAT_USAGE' | 'DEGRADE' | 'ABSENT' | 'NON_APPLICABLE';
|
||||
export type ReportPartyRole = 'LANDLORD' | 'TENANT';
|
||||
export type ParticipantAttendance = 'PRESENT' | 'REPRESENTED';
|
||||
export type ZoneEquipment = 'NONE' | 'CLIMATISATION' | 'OTHER';
|
||||
export type MeterType = 'ELECTRICITE' | 'GAZ' | 'EAU_FROIDE' | 'EAU_CHAUDE' | 'OTHER';
|
||||
export type AccessMeansType = 'CLE' | 'BADGE' | 'TELECOMMANDE' | 'OTHER';
|
||||
export type FurniturePresence = 'PRESENT' | 'ABSENT';
|
||||
export type FurnitureCategory =
|
||||
| 'BEDDING'
|
||||
| 'SHUTTERS'
|
||||
| 'HOTPLATES'
|
||||
| 'OVEN_OR_MICROWAVE'
|
||||
| 'REFRIGERATOR_WITH_FREEZER'
|
||||
| 'DISHES'
|
||||
| 'UTENSILS'
|
||||
| 'TABLE_AND_SEATING'
|
||||
| 'STORAGE'
|
||||
| 'LIGHTING'
|
||||
| 'CLEANING';
|
||||
|
||||
export interface ReportParticipant {
|
||||
partyId: string;
|
||||
role: ReportPartyRole;
|
||||
name: string;
|
||||
attendance: ParticipantAttendance;
|
||||
representativeName: string;
|
||||
}
|
||||
|
||||
export interface ForwardingAddress {
|
||||
tenantPartyId: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface ConditionZone {
|
||||
id: string;
|
||||
privateAreaId: string;
|
||||
label: string;
|
||||
equipment: ZoneEquipment;
|
||||
}
|
||||
|
||||
export interface ConditionObservation {
|
||||
subjectId: string;
|
||||
state: ConditionState;
|
||||
observation: string;
|
||||
}
|
||||
|
||||
export interface MeterReading {
|
||||
id: string;
|
||||
type: MeterType;
|
||||
index: number;
|
||||
unit: string;
|
||||
label: string;
|
||||
photoId: string;
|
||||
observation: string;
|
||||
}
|
||||
|
||||
export interface AccessMeans {
|
||||
id: string;
|
||||
type: AccessMeansType;
|
||||
destination: string;
|
||||
quantity: number;
|
||||
observation: string;
|
||||
}
|
||||
|
||||
export interface InventoryQuantitySplit {
|
||||
inventoryLineId: string;
|
||||
state: ConditionState;
|
||||
quantity: number;
|
||||
observation: string;
|
||||
}
|
||||
|
||||
export interface InventoryCondition {
|
||||
inventoryLineId: string;
|
||||
privateAreaId: string;
|
||||
privateAreaLabel: string;
|
||||
designation: string;
|
||||
contractualQuantity: number;
|
||||
splits: InventoryQuantitySplit[];
|
||||
}
|
||||
|
||||
export interface ConditionPhoto {
|
||||
reference: string;
|
||||
zoneId: string;
|
||||
sha256: string;
|
||||
width: number;
|
||||
height: number;
|
||||
uploadedBy: string;
|
||||
uploadedAt: string;
|
||||
}
|
||||
|
||||
export interface ZonePhotoAbsence {
|
||||
zoneId: string;
|
||||
absence: { reason: string; author: string; recordedAt: string };
|
||||
}
|
||||
|
||||
export interface EntryPhotoDisposition {
|
||||
entryPhotoId: string;
|
||||
exitPhotoId?: string;
|
||||
absenceReason?: string;
|
||||
}
|
||||
|
||||
export interface PropertyConditionReportSupplement {
|
||||
id: string;
|
||||
signedPdf: { reference: string; sha256: string };
|
||||
photos: { reference: string; sha256: string }[];
|
||||
uploadedBy: string;
|
||||
uploadedAt: string;
|
||||
requestedOn: string;
|
||||
acceptedAdditions: string;
|
||||
audit: unknown;
|
||||
}
|
||||
|
||||
export interface PropertyConditionReportDeliveryAttestation {
|
||||
partyId: string;
|
||||
attestedBy: string;
|
||||
attestedAt: string;
|
||||
}
|
||||
|
||||
export interface PropertyConditionReportRevision {
|
||||
number: number;
|
||||
sha256: string;
|
||||
sourceFingerprint: string;
|
||||
obsolete: boolean;
|
||||
}
|
||||
|
||||
export interface EntryReportSnapshot {
|
||||
reportId: string;
|
||||
participants: ReportParticipant[];
|
||||
zones: ConditionZone[];
|
||||
observations: ConditionObservation[];
|
||||
meterReadings: MeterReading[];
|
||||
accessMeans: AccessMeans[];
|
||||
inventoryConditions: InventoryCondition[];
|
||||
photos: ConditionPhoto[];
|
||||
supplements: PropertyConditionReportSupplement[];
|
||||
}
|
||||
|
||||
export interface PropertyConditionReportFinalization {
|
||||
revisionNumber: number;
|
||||
signedSha256: string;
|
||||
authorId: string;
|
||||
finalizedAt: string;
|
||||
}
|
||||
|
||||
export interface PropertyConditionReport {
|
||||
id: string;
|
||||
type: ConditionReportType;
|
||||
status: ConditionReportStatus;
|
||||
occurredOn: string;
|
||||
timeZone: string;
|
||||
participants: ReportParticipant[];
|
||||
forwardingAddresses: ForwardingAddress[];
|
||||
zones: ConditionZone[];
|
||||
observations: ConditionObservation[];
|
||||
meterReadings: MeterReading[];
|
||||
accessMeans: AccessMeans[];
|
||||
inventoryConditions: InventoryCondition[];
|
||||
photos: ConditionPhoto[];
|
||||
zonePhotoAbsences: ZonePhotoAbsence[];
|
||||
entryPhotoDispositions: EntryPhotoDisposition[];
|
||||
supplements: PropertyConditionReportSupplement[];
|
||||
entrySnapshot: EntryReportSnapshot | null;
|
||||
revisions: PropertyConditionReportRevision[];
|
||||
currentRevision: PropertyConditionReportRevision | null;
|
||||
finalization: PropertyConditionReportFinalization | null;
|
||||
deliveryAttestations: PropertyConditionReportDeliveryAttestation[];
|
||||
leaseRevisionNumber: number | null;
|
||||
leaseRevisionFingerprint: string | null;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface SaveConditionReportRequest {
|
||||
version: number;
|
||||
occurredOn: string;
|
||||
timeZone: string;
|
||||
participants: ReportParticipant[];
|
||||
forwardingAddresses: ForwardingAddress[];
|
||||
zones: ConditionZone[];
|
||||
observations: ConditionObservation[];
|
||||
meterReadings: MeterReading[];
|
||||
accessMeans: AccessMeans[];
|
||||
inventoryConditions: InventoryCondition[];
|
||||
zonePhotoAbsences: { zoneId: string; reason: string }[];
|
||||
entryPhotoDispositions: EntryPhotoDisposition[];
|
||||
}
|
||||
|
||||
export interface FurnitureInventoryLine {
|
||||
id: string;
|
||||
category: FurnitureCategory | null;
|
||||
presence: FurniturePresence | null;
|
||||
privateAreaId: string;
|
||||
designation: string;
|
||||
quantity: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface FurnitureInventoryWarning {
|
||||
code: string;
|
||||
ruleVersion: string;
|
||||
category: FurnitureCategory;
|
||||
}
|
||||
|
||||
export interface FurnitureInventory {
|
||||
id: string;
|
||||
templateVersion: string;
|
||||
lines: FurnitureInventoryLine[];
|
||||
missingCategories: FurnitureCategory[];
|
||||
absentCategories: FurnitureCategory[];
|
||||
legalWarnings: FurnitureInventoryWarning[];
|
||||
readyForLeaseDocument: boolean;
|
||||
frozen: boolean;
|
||||
privateAreas: { id: string; label: string }[];
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface SaveFurnitureInventoryRequest {
|
||||
version: number;
|
||||
lines: FurnitureInventoryLine[];
|
||||
}
|
||||
|
||||
export interface ReportFinalizationRequest {
|
||||
signedReport: File;
|
||||
version: number;
|
||||
allPartiesSigned: boolean;
|
||||
}
|
||||
|
||||
export interface AppendConditionReportSupplementRequest {
|
||||
signedPdf: File;
|
||||
photos: File[];
|
||||
requestedOn: string;
|
||||
acceptedAdditions: string;
|
||||
heatingPeriodStartedOn?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ConditionReportComparisonLine<T> {
|
||||
reference: string;
|
||||
entry: T | null;
|
||||
exit: T | null;
|
||||
changed: boolean;
|
||||
absence?: { reason: string; auditedBy: string; auditedAt: string } | null;
|
||||
}
|
||||
|
||||
export interface ConditionReportComparison {
|
||||
observations: ConditionReportComparisonLine<ConditionObservation>[];
|
||||
zones: ConditionReportComparisonLine<ConditionZone>[];
|
||||
inventory: ConditionReportComparisonLine<InventoryCondition>[];
|
||||
meters: ConditionReportComparisonLine<MeterReading>[];
|
||||
accessMeans: ConditionReportComparisonLine<AccessMeans>[];
|
||||
supplements: ConditionReportComparisonLine<PropertyConditionReportSupplement>[];
|
||||
photos: ConditionReportComparisonLine<ConditionPhoto>[];
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ConditionReportService {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
getInventory(leaseId: string): Observable<FurnitureInventory> {
|
||||
return this.http.get<FurnitureInventory>(`/api/leases/${leaseId}/inventory`);
|
||||
}
|
||||
|
||||
saveInventory(
|
||||
leaseId: string,
|
||||
request: SaveFurnitureInventoryRequest,
|
||||
): Observable<FurnitureInventory> {
|
||||
return this.http.put<FurnitureInventory>(`/api/leases/${leaseId}/inventory`, request);
|
||||
}
|
||||
|
||||
getReport(
|
||||
leaseId: string,
|
||||
type: ConditionReportType,
|
||||
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
): Observable<PropertyConditionReport> {
|
||||
return this.http.get<PropertyConditionReport>(
|
||||
`/api/leases/${leaseId}/condition-reports/${type}`,
|
||||
{ params: new HttpParams().set('timeZone', timeZone) },
|
||||
);
|
||||
}
|
||||
|
||||
saveReport(
|
||||
leaseId: string,
|
||||
type: ConditionReportType,
|
||||
report: SaveConditionReportRequest,
|
||||
): Observable<PropertyConditionReport> {
|
||||
return this.http.put<PropertyConditionReport>(
|
||||
`/api/leases/${leaseId}/condition-reports/${type}`,
|
||||
report,
|
||||
);
|
||||
}
|
||||
|
||||
uploadPhoto(
|
||||
leaseId: string,
|
||||
type: ConditionReportType,
|
||||
file: File,
|
||||
zoneId: string,
|
||||
): Observable<PropertyConditionReport> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('zoneId', zoneId);
|
||||
return this.http.post<PropertyConditionReport>(
|
||||
`/api/leases/${leaseId}/condition-reports/${type}/photos`,
|
||||
formData,
|
||||
);
|
||||
}
|
||||
|
||||
deletePhoto(
|
||||
leaseId: string,
|
||||
type: ConditionReportType,
|
||||
reference: string,
|
||||
version: number,
|
||||
): Observable<void> {
|
||||
return this.http.delete<void>(
|
||||
`/api/leases/${leaseId}/condition-reports/${type}/photos/${reference}`,
|
||||
{ params: new HttpParams().set('version', version) },
|
||||
);
|
||||
}
|
||||
|
||||
photoUrl(leaseId: string, type: ConditionReportType, reference: string): string {
|
||||
return `/api/leases/${encodeURIComponent(leaseId)}/condition-reports/${type}/photos/${encodeURIComponent(reference)}`;
|
||||
}
|
||||
|
||||
createRevision(
|
||||
leaseId: string,
|
||||
type: ConditionReportType,
|
||||
version: number,
|
||||
): Observable<PropertyConditionReportRevision> {
|
||||
return this.http.post<PropertyConditionReportRevision>(
|
||||
`/api/leases/${leaseId}/condition-reports/${type}/revisions?version=${version}`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
downloadRevision(leaseId: string, type: ConditionReportType, number: number): Observable<Blob> {
|
||||
return this.http.get(`/api/leases/${leaseId}/condition-reports/${type}/revisions/${number}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
|
||||
finalize(
|
||||
leaseId: string,
|
||||
type: ConditionReportType,
|
||||
request: ReportFinalizationRequest,
|
||||
): Observable<PropertyConditionReport> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', request.signedReport);
|
||||
formData.append('version', String(request.version));
|
||||
formData.append('attestation', 'true');
|
||||
formData.append('allPartiesSigned', String(request.allPartiesSigned));
|
||||
return this.http.post<PropertyConditionReport>(
|
||||
`/api/leases/${leaseId}/condition-reports/${type}/finalization`,
|
||||
formData,
|
||||
);
|
||||
}
|
||||
|
||||
attestDelivery(
|
||||
leaseId: string,
|
||||
type: ConditionReportType,
|
||||
version: number,
|
||||
partyId: string,
|
||||
): Observable<PropertyConditionReport> {
|
||||
return this.http.post<PropertyConditionReport>(
|
||||
`/api/leases/${leaseId}/condition-reports/${type}/delivery-attestations`,
|
||||
null,
|
||||
{ params: new HttpParams().set('version', version).set('partyId', partyId) },
|
||||
);
|
||||
}
|
||||
|
||||
appendSupplement(
|
||||
leaseId: string,
|
||||
request: AppendConditionReportSupplementRequest,
|
||||
): Observable<PropertyConditionReport> {
|
||||
const formData = new FormData();
|
||||
formData.append('signedPdf', request.signedPdf);
|
||||
request.photos.forEach((photo) => formData.append('photos', photo));
|
||||
formData.append('requestedOn', request.requestedOn);
|
||||
formData.append('acceptedAdditions', request.acceptedAdditions);
|
||||
if (request.heatingPeriodStartedOn) {
|
||||
formData.append('heatingPeriodStartedOn', request.heatingPeriodStartedOn);
|
||||
}
|
||||
if (request.reason?.trim()) formData.append('reason', request.reason.trim());
|
||||
return this.http.post<PropertyConditionReport>(
|
||||
`/api/leases/${leaseId}/condition-reports/ENTRY/supplements`,
|
||||
formData,
|
||||
);
|
||||
}
|
||||
|
||||
getComparison(leaseId: string): Observable<ConditionReportComparison> {
|
||||
return this.http.get<ConditionReportComparison>(
|
||||
`/api/leases/${leaseId}/condition-report-comparison`,
|
||||
);
|
||||
}
|
||||
|
||||
downloadBundle(leaseId: string, type: ConditionReportType): Observable<Blob> {
|
||||
return this.http.get(`/api/leases/${leaseId}/condition-reports/${type}/bundle`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user