feat(rental): add inventory and condition reports (#7)

This commit is contained in:
nessar authored and Nessar committed 2026-08-17 14:18:16 +02:00
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',
});
}
}