Files
lmnp-organizer/frontend/src/app/services/workspace.service.ts
T
nessar b9e99348f7 feat: add rent payment history and reversal dialogs
- Implemented RentPaymentHistoryServiceTest to validate payment retrieval with refunds and reversals.
- Created ReverseRentPaymentServiceTest to ensure correct handling of payment reversals and allocation releases.
- Developed rent-payment-allocation-dialog component with form for surplus allocation.
- Added rent-payment-refund-dialog component for processing refunds with validation.
- Introduced rent-payment-reversal-dialog component for reversing payments with reason input.
- Implemented utility functions for formatting Euro values and handling date formatting.
- Enhanced rent-payment-dialogs.spec.ts to cover new dialog functionalities and error handling.
2026-08-17 14:18:15 +02:00

375 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Injectable, inject, signal } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import {
ConfirmationDialog,
ConfirmationDialogData,
} from '../components/confirmation-dialog/confirmation-dialog';
import { Apartment, ApartmentService } from './apartment.service';
import { FileDownloadService } from './file-download.service';
import { finalize } from 'rxjs';
import { Lease, LeaseInput, LeaseService, RentDue } from './lease.service';
import { RentPayment, RentPaymentService } from './rent-payment.service';
import { Tenant, TenantAttachment, TenantService } from './tenant.service';
import { TenantAttachmentTransferService } from './tenant-attachment-transfer.service';
import { WorkspaceResource } from './workspace-resource';
import { httpErrorMessage } from './http-error-message';
import { withoutRecordKey, withoutSetValue } from './immutable-state';
const newResourceKey = 'new';
@Injectable({ providedIn: 'root' })
export class WorkspaceService {
private readonly dialog = inject(MatDialog);
private readonly apartmentService = inject(ApartmentService);
private readonly tenantService = inject(TenantService);
private readonly leaseService = inject(LeaseService);
private readonly rentPaymentService = inject(RentPaymentService);
private readonly downloads = inject(FileDownloadService);
private readonly attachmentTransfers = inject(TenantAttachmentTransferService);
private readonly apartmentResource = new WorkspaceResource(
() => this.apartmentService.getApartments(),
{
load: 'Impossible de charger les appartements.',
save: 'Impossible d’enregistrer cet appartement.',
delete: 'Impossible de supprimer cet appartement.',
},
);
private readonly tenantResource = new WorkspaceResource(() => this.tenantService.getTenants(), {
load: 'Impossible de charger les locataires.',
save: 'Impossible d’enregistrer ce locataire.',
delete: 'Impossible de supprimer ce locataire.',
});
private readonly leaseResource = new WorkspaceResource(() => this.leaseService.getLeases(), {
load: 'Impossible de charger les baux.',
save: 'Impossible d’enregistrer ce bail.',
delete: 'Impossible de supprimer ce bail.',
});
private readonly rentDueResource = new WorkspaceResource(() => this.leaseService.getRentDues(), {
load: 'Impossible de charger les échéances de loyer.',
save: '',
delete: '',
});
private readonly rentPaymentResource = new WorkspaceResource(
() => this.rentPaymentService.getRentPayments(),
{
load: 'Impossible de charger les échéances de loyer.',
save: 'Impossible d’enregistrer ce paiement.',
delete: '',
},
);
readonly apartments = this.apartmentResource.items;
readonly apartmentLoadState = this.apartmentResource.loadState;
readonly deletingApartmentIds = this.apartmentResource.deletingIds;
readonly apartmentDeleteErrors = this.apartmentResource.deleteErrors;
readonly tenants = this.tenantResource.items;
readonly tenantLoadState = this.tenantResource.loadState;
readonly deletingTenantIds = this.tenantResource.deletingIds;
readonly tenantDeleteErrors = this.tenantResource.deleteErrors;
readonly leases = this.leaseResource.items;
readonly leaseLoadState = this.leaseResource.loadState;
readonly deletingLeaseIds = this.leaseResource.deletingIds;
readonly leaseDeleteErrors = this.leaseResource.deleteErrors;
readonly rentDues = this.rentDueResource.items;
readonly rentDueLoadState = this.rentDueResource.loadState;
private readonly activatingLeaseIds = signal<ReadonlySet<string>>(new Set());
private readonly leaseActivationErrors = signal<Record<string, string>>({});
readonly rentPayments = this.rentPaymentResource.items;
readonly rentPaymentLoadState = this.rentPaymentResource.loadState;
readonly tenantAttachmentErrors = this.attachmentTransfers.errors;
readonly uploadingTenantIds = this.attachmentTransfers.uploadingTenantIds;
readonly tenantUploadProgress = this.attachmentTransfers.uploadProgress;
readonly deletingAttachmentIds = this.attachmentTransfers.deletingIds;
readonly attachmentDeleteErrors = this.attachmentTransfers.deleteErrors;
readonly downloadingAttachmentIds = this.attachmentTransfers.downloadingIds;
readonly attachmentDownloadErrors = this.attachmentTransfers.downloadErrors;
loadApartments(): void {
this.apartmentResource.load();
}
saveApartment(apartment: Apartment, id?: string, onSuccess?: () => void): void {
this.apartmentResource.save(
this.resourceKey(id),
id
? this.apartmentService.updateApartment(id, apartment)
: this.apartmentService.createApartment(apartment),
onSuccess,
);
}
apartmentSaving(id?: string): boolean {
return this.apartmentResource.savingKeys().has(this.resourceKey(id));
}
apartmentSaveError(id?: string): string {
return this.apartmentResource.saveErrors()[this.resourceKey(id)] ?? '';
}
clearApartmentSaveError(id?: string): void {
this.apartmentResource.clearSaveError(this.resourceKey(id));
}
deleteApartment(id: string): void {
this.requestConfirmation(
{
title: 'Supprimer cet appartement ?',
message: 'Êtes-vous sûr de vouloir supprimer cet appartement ?',
confirmLabel: 'Supprimer',
},
() => this.apartmentResource.delete(id, this.apartmentService.deleteApartment(id)),
);
}
loadTenants(): void {
this.tenantResource.load();
}
saveTenant(tenant: Tenant, id?: string, onSuccess?: () => void): void {
this.tenantResource.save(
this.resourceKey(id),
id ? this.tenantService.updateTenant(id, tenant) : this.tenantService.createTenant(tenant),
onSuccess,
);
}
tenantSaving(id?: string): boolean {
return this.tenantResource.savingKeys().has(this.resourceKey(id));
}
tenantSaveError(id?: string): string {
return this.tenantResource.saveErrors()[this.resourceKey(id)] ?? '';
}
clearTenantSaveError(id?: string): void {
this.tenantResource.clearSaveError(this.resourceKey(id));
}
deleteTenant(id: string): void {
this.requestConfirmation(
{
title: 'Supprimer ce locataire ?',
message: 'Êtes-vous sûr de vouloir supprimer ce locataire ?',
confirmLabel: 'Supprimer',
},
() => this.tenantResource.delete(id, this.tenantService.deleteTenant(id)),
);
}
uploadTenantAttachment(tenantId: string, file: File): void {
this.attachmentTransfers.upload(tenantId, file, (attachment) => {
this.tenantResource.update(tenantId, (tenant) => ({
...tenant,
attachments: [
...(tenant.attachments ?? []).filter((item) => item.id !== attachment.id),
attachment,
],
}));
this.tenantResource.load();
});
}
downloadTenantAttachment(tenantId: string, attachment: { id: string; fileName: string }): void {
this.attachmentTransfers.download(tenantId, attachment);
}
attachmentDownloading(attachmentId: string): boolean {
return this.attachmentTransfers.downloading(attachmentId);
}
attachmentDownloadError(attachmentId: string): string {
return this.attachmentTransfers.downloadError(attachmentId);
}
deleteTenantAttachment(tenantId: string, attachment: TenantAttachment): void {
this.requestConfirmation(
{
title: 'Retirer cette pièce jointe ?',
message: `Êtes-vous sûr de vouloir retirer « ${attachment.fileName} » ?`,
confirmLabel: 'Retirer',
},
() =>
this.attachmentTransfers.delete(tenantId, attachment.id, () => {
this.tenantResource.update(tenantId, (tenant) => ({
...tenant,
attachments: (tenant.attachments ?? []).filter((item) => item.id !== attachment.id),
}));
this.tenantResource.load();
}),
);
}
loadLeases(): void {
this.leaseResource.load();
}
loadRentDues(): void {
this.rentDueResource.load();
}
saveLease(lease: LeaseInput, id?: string, onSuccess?: () => void): void {
const input: LeaseInput = {
apartmentId: lease.apartmentId,
tenantIds: lease.tenantIds,
guarantors: lease.guarantors,
startDate: lease.startDate,
endDate: lease.endDate,
monthlyRent: lease.monthlyRent,
monthlyCharges: lease.monthlyCharges,
securityDeposit: lease.securityDeposit,
};
this.leaseResource.save(
this.resourceKey(id),
id ? this.leaseService.updateLease(id, input) : this.leaseService.createLease(input),
onSuccess,
);
}
leaseSaving(id?: string): boolean {
return this.leaseResource.savingKeys().has(this.resourceKey(id));
}
leaseSaveError(id?: string): string {
return this.leaseResource.saveErrors()[this.resourceKey(id)] ?? '';
}
clearLeaseSaveError(id?: string): void {
this.leaseResource.clearSaveError(this.resourceKey(id));
}
activateLease(id: string): void {
if (this.activatingLeaseIds().has(id)) return;
this.activatingLeaseIds.update((ids) => new Set(ids).add(id));
this.leaseActivationErrors.update((errors) => withoutRecordKey(errors, id));
this.leaseService
.activateLease(id)
.pipe(finalize(() => this.activatingLeaseIds.update((ids) => withoutSetValue(ids, id))))
.subscribe({
next: ({ lease, rentDues }) => {
this.leaseResource.upsert(lease);
rentDues.forEach((rentDue) => this.rentDueResource.upsert(rentDue));
this.leaseResource.load();
this.rentDueResource.load();
},
error: (error) =>
this.leaseActivationErrors.update((errors) => ({
...errors,
[id]: httpErrorMessage(error, 'Impossible d’activer ce bail.'),
})),
});
}
leaseActivating(id: string): boolean {
return this.activatingLeaseIds().has(id);
}
leaseActivationError(id: string): string {
return this.leaseActivationErrors()[id] ?? '';
}
deleteLease(id: string): void {
this.requestConfirmation(
{
title: 'Supprimer ce bail ?',
message: 'Êtes-vous sûr de vouloir supprimer ce bail ?',
confirmLabel: 'Supprimer',
},
() => this.leaseResource.delete(id, this.leaseService.deleteLease(id)),
);
}
downloadLeasePdf(lease: Lease): void {
if (!lease.id) return;
this.downloads.download(
this.leaseDownloadKey(lease.id),
this.leaseService.downloadPdf(lease.id),
`bail-${lease.id}.pdf`,
'Impossible de télécharger ce bail.',
);
}
leaseDownloading(leaseId: string): boolean {
return this.downloads.isPending(this.leaseDownloadKey(leaseId));
}
leaseDownloadError(leaseId: string): string {
return this.downloads.error(this.leaseDownloadKey(leaseId));
}
loadRentPayments(): void {
this.rentPaymentResource.load();
}
acceptRentPayment(payment: RentPayment): void {
this.rentPaymentResource.upsert(payment);
this.rentPaymentResource.load();
this.rentDueResource.load();
}
downloadRentReceipt(rentDue: RentDue): void {
this.downloads.download(
this.rentReceiptDownloadKey(rentDue.id),
this.leaseService.downloadRentReceipt(rentDue.id),
`quittance-${rentDue.month}.pdf`,
'Impossible de télécharger cette quittance.',
);
}
downloadPaymentReceipt(payment: RentPayment): void {
this.downloads.download(
this.paymentReceiptDownloadKey(payment.id),
this.rentPaymentService.downloadReceipt(payment.id),
`recu-reglement-${payment.id}.pdf`,
'Impossible de télécharger ce reçu de règlement.',
);
}
rentReceiptDownloading(paymentId: string): boolean {
return this.downloads.isPending(this.rentReceiptDownloadKey(paymentId));
}
rentReceiptDownloadError(paymentId: string): string {
return this.downloads.error(this.rentReceiptDownloadKey(paymentId));
}
paymentReceiptDownloading(paymentId: string): boolean {
return this.downloads.isPending(this.paymentReceiptDownloadKey(paymentId));
}
paymentReceiptDownloadError(paymentId: string): string {
return this.downloads.error(this.paymentReceiptDownloadKey(paymentId));
}
private resourceKey(id?: string): string {
return id ?? newResourceKey;
}
private leaseDownloadKey(leaseId: string): string {
return `lease:${leaseId}`;
}
private rentReceiptDownloadKey(paymentId: string): string {
return `rent-receipt:${paymentId}`;
}
private paymentReceiptDownloadKey(paymentId: string): string {
return `payment-receipt:${paymentId}`;
}
private requestConfirmation(data: ConfirmationDialogData, onConfirmed: () => void): void {
this.dialog
.open(ConfirmationDialog, {
data,
role: 'alertdialog',
width: 'min(420px, calc(100vw - 32px))',
maxWidth: 'calc(100vw - 32px)',
})
.afterClosed()
.subscribe((confirmed) => {
if (confirmed) onConfirmed();
});
}
}