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 MeterReadingState = 'RECORDED' | 'INACCESSIBLE'; 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; state: MeterReadingState; index: number | null; 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; type: 'GENERAL' | 'HEATING'; signedPdf: { reference: string; sha256: string }; photos: ConditionPhoto[]; uploadedBy: string; uploadedAt: string; requestedOn: string; heatingPeriodStartedOn: string | null; acceptedAdditions: string; audit: SupplementAudit; } export type SupplementAudit = | { status: 'ON_TIME'; deadlineOn: string } | { status: 'LATE'; deadlineOn: string; ruleVersion: string; reason: string; auditedBy: string; auditedAt: string; }; 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; equipmentKind: 'NONE' | 'CLIMATISATION' | 'OTHER'; 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: File; zoneId: string }[]; requestedOn: string; acceptedAdditions: string; supplementType: 'GENERAL' | 'HEATING'; heatingPeriodStartedOn?: string; lateWarningConfirmed: boolean; reason?: string; } export interface ConditionReportComparisonLine { reference: string; entry: T | null; exit: T | null; changed: boolean; absence?: { reason: string; auditedBy: string; auditedAt: string } | null; } export interface ConditionReportComparison { observations: ConditionReportComparisonLine[]; zones: ConditionReportComparisonLine[]; inventory: ConditionReportComparisonLine[]; meters: ConditionReportComparisonLine[]; accessMeans: ConditionReportComparisonLine[]; supplements: ConditionReportComparisonLine[]; photos: ConditionReportComparisonLine[]; } @Injectable({ providedIn: 'root' }) export class ConditionReportService { private readonly http = inject(HttpClient); getInventory(leaseId: string): Observable { return this.http.get(`/api/leases/${leaseId}/inventory`); } saveInventory( leaseId: string, request: SaveFurnitureInventoryRequest, ): Observable { return this.http.put(`/api/leases/${leaseId}/inventory`, request); } getReport( leaseId: string, type: ConditionReportType, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone, ): Observable { return this.http.get( `/api/leases/${leaseId}/condition-reports/${type}`, { params: new HttpParams().set('timeZone', timeZone) }, ); } saveReport( leaseId: string, type: ConditionReportType, report: SaveConditionReportRequest, ): Observable { return this.http.put( `/api/leases/${leaseId}/condition-reports/${type}`, report, ); } uploadPhoto( leaseId: string, type: ConditionReportType, file: File, zoneId: string, ): Observable { const formData = new FormData(); formData.append('file', file); formData.append('zoneId', zoneId); return this.http.post( `/api/leases/${leaseId}/condition-reports/${type}/photos`, formData, ); } deletePhoto( leaseId: string, type: ConditionReportType, reference: string, version: number, ): Observable { return this.http.delete( `/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 { return this.http.post( `/api/leases/${leaseId}/condition-reports/${type}/revisions?version=${version}`, {}, ); } downloadRevision(leaseId: string, type: ConditionReportType, number: number): Observable { return this.http.get(`/api/leases/${leaseId}/condition-reports/${type}/revisions/${number}`, { responseType: 'blob', }); } finalize( leaseId: string, type: ConditionReportType, request: ReportFinalizationRequest, ): Observable { 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( `/api/leases/${leaseId}/condition-reports/${type}/finalization`, formData, ); } attestDelivery( leaseId: string, type: ConditionReportType, version: number, partyId: string, ): Observable { return this.http.post( `/api/leases/${leaseId}/condition-reports/${type}/delivery-attestations`, null, { params: new HttpParams().set('version', version).set('partyId', partyId) }, ); } appendSupplement( leaseId: string, request: AppendConditionReportSupplementRequest, ): Observable { const formData = new FormData(); formData.append('signedPdf', request.signedPdf); request.photos.forEach((photo) => { formData.append('photos', photo.file); formData.append('photoZoneIds', photo.zoneId); }); formData.append('requestedOn', request.requestedOn); formData.append('acceptedAdditions', request.acceptedAdditions); formData.append('supplementType', request.supplementType); formData.append('lateWarningConfirmed', String(request.lateWarningConfirmed)); if (request.heatingPeriodStartedOn) { formData.append('heatingPeriodStartedOn', request.heatingPeriodStartedOn); } if (request.reason?.trim()) formData.append('reason', request.reason.trim()); return this.http.post( `/api/leases/${leaseId}/condition-reports/ENTRY/supplements`, formData, ); } getComparison(leaseId: string): Observable { return this.http.get( `/api/leases/${leaseId}/condition-report-comparison`, ); } downloadBundle(leaseId: string, type: ConditionReportType): Observable { return this.http.get(`/api/leases/${leaseId}/condition-reports/${type}/bundle`, { responseType: 'blob', }); } }