feat: add rent payment schedules and receipts
This commit is contained in:
1 parent
f876639c02
commit
47cb4bcdaf
26 files changed
+1118
-3
No files matched your search
@@ -8,6 +8,7 @@ describe('routes', () => {
|
||||
'apartments',
|
||||
'tenants',
|
||||
'leases',
|
||||
'rent-payments',
|
||||
'settings',
|
||||
'',
|
||||
'**',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Routes } from '@angular/router';
|
||||
import { ApartmentsPage } from './components/apartments-page/apartments-page';
|
||||
import { DashboardPage } from './components/dashboard-page/dashboard-page';
|
||||
import { LeasesPage } from './components/leases-page/leases-page';
|
||||
import { RentPaymentsPage } from './components/rent-payments-page/rent-payments-page';
|
||||
import { SettingsPage } from './components/settings-page/settings-page';
|
||||
import { TenantsPage } from './components/tenants-page/tenants-page';
|
||||
|
||||
@@ -10,6 +11,7 @@ export const routes: Routes = [
|
||||
{ path: 'apartments', component: ApartmentsPage },
|
||||
{ path: 'tenants', component: TenantsPage },
|
||||
{ path: 'leases', component: LeasesPage },
|
||||
{ path: 'rent-payments', component: RentPaymentsPage },
|
||||
{ path: 'settings', component: SettingsPage },
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'dashboard' },
|
||||
{ path: '**', redirectTo: 'dashboard' },
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
.rent-payment-grid {
|
||||
display: grid;
|
||||
gap: var(--space-4, 1rem);
|
||||
}
|
||||
|
||||
.rent-payment {
|
||||
padding: var(--space-5, 1.25rem);
|
||||
border: 1px solid var(--border, #dce5df);
|
||||
border-radius: var(--radius-xl, 1.375rem);
|
||||
background: var(--surface, #ffffff);
|
||||
}
|
||||
|
||||
.payment-topline,
|
||||
.rent-payment dl > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3, 0.75rem);
|
||||
}
|
||||
|
||||
.payment-topline {
|
||||
margin-bottom: var(--space-4, 1rem);
|
||||
}
|
||||
|
||||
.rent-payment h2 {
|
||||
margin-bottom: var(--space-1, 0.25rem);
|
||||
}
|
||||
|
||||
.payment-month,
|
||||
.rent-payment dt {
|
||||
color: var(--text-muted, #687670);
|
||||
}
|
||||
|
||||
.rent-payment dl {
|
||||
margin: var(--space-5, 1.25rem) 0;
|
||||
}
|
||||
|
||||
.rent-payment dl > div {
|
||||
padding: var(--space-2, 0.5rem) 0;
|
||||
border-bottom: 1px solid var(--border, #dce5df);
|
||||
}
|
||||
|
||||
.rent-payment dd {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.payment-status {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.payment-status.paid { color: var(--accent, #0f766e); }
|
||||
.payment-status.late { color: var(--danger, #b42318); }
|
||||
.payment-status.upcoming { color: var(--text-muted, #687670); }
|
||||
|
||||
@media (min-width: 700px) {
|
||||
.rent-payment-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (min-width: 1100px) {
|
||||
.rent-payment-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<section class="page-section">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Location</p>
|
||||
<h1>Mes loyers</h1>
|
||||
<p>Suivez les échéances et téléchargez les quittances après règlement.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-region" [attr.aria-busy]="contentLoading()">
|
||||
@if (!contentReady() && loadError()) {
|
||||
<article class="load-error" role="alert">
|
||||
<h2>Chargement impossible</h2>
|
||||
<p>{{ loadError() }}</p>
|
||||
<button mat-stroked-button type="button" (click)="load()">Réessayer</button>
|
||||
</article>
|
||||
} @else if (!contentReady()) {
|
||||
<div class="rent-payment-grid" aria-label="Chargement des loyers">
|
||||
@for (placeholder of [1, 2, 3]; track placeholder) {
|
||||
<article class="skeleton-card" aria-hidden="true">
|
||||
<span class="skeleton-line skeleton-line-short"></span>
|
||||
<span class="skeleton-line skeleton-line-title"></span>
|
||||
<span class="skeleton-line skeleton-line-medium"></span>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
@if (contentLoading()) {
|
||||
<mat-progress-bar mode="indeterminate" aria-label="Actualisation des loyers" />
|
||||
}
|
||||
@if (loadError()) {
|
||||
<div class="refresh-error" role="alert">
|
||||
<p>{{ loadError() }}</p>
|
||||
<button mat-button type="button" (click)="load()">Réessayer</button>
|
||||
</div>
|
||||
}
|
||||
@if (workspace.rentPayments().length === 0) {
|
||||
<article class="empty-state">
|
||||
<p class="eyebrow">Échéancier</p>
|
||||
<h2>Aucune échéance de loyer</h2>
|
||||
<p>Les échéances sont créées automatiquement pour les baux actifs.</p>
|
||||
</article>
|
||||
} @else {
|
||||
<div class="rent-payment-grid">
|
||||
@for (payment of workspace.rentPayments(); track payment.id) {
|
||||
<article class="rent-payment" [attr.aria-busy]="workspace.rentPaymentSaving(payment.id)">
|
||||
<div class="payment-topline">
|
||||
<span class="payment-status" [class]="payment.status.toLowerCase()">
|
||||
{{ statusLabel(payment.status) }}
|
||||
</span>
|
||||
<strong>{{ payment.total.toLocaleString('fr-FR') }} €</strong>
|
||||
</div>
|
||||
<h2>{{ apartmentName(payment) }}</h2>
|
||||
<p class="payment-month">{{ monthLabel(payment.month) }}</p>
|
||||
<dl>
|
||||
<div><dt>Loyer</dt><dd>{{ payment.rent.toLocaleString('fr-FR') }} €</dd></div>
|
||||
<div><dt>Charges</dt><dd>{{ payment.charges.toLocaleString('fr-FR') }} €</dd></div>
|
||||
<div><dt>Échéance</dt><dd>{{ payment.dueDate }}</dd></div>
|
||||
</dl>
|
||||
@if (workspace.rentPaymentSaveError(payment.id) || workspace.rentReceiptDownloadError(payment.id)) {
|
||||
<p class="card-error" role="alert">
|
||||
{{ workspace.rentPaymentSaveError(payment.id) || workspace.rentReceiptDownloadError(payment.id) }}
|
||||
</p>
|
||||
}
|
||||
<div class="card-actions" aria-live="polite">
|
||||
@if (payment.status === 'PAID') {
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
[disabled]="workspace.rentReceiptDownloading(payment.id)"
|
||||
(click)="workspace.downloadRentReceipt(payment)"
|
||||
>
|
||||
@if (workspace.rentReceiptDownloading(payment.id)) {
|
||||
<mat-spinner diameter="16" aria-hidden="true" /> Téléchargement…
|
||||
} @else {
|
||||
Télécharger la quittance
|
||||
}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
mat-flat-button
|
||||
type="button"
|
||||
[disabled]="workspace.rentPaymentSaving(payment.id)"
|
||||
(click)="workspace.markRentPaymentPaid(payment.id)"
|
||||
>
|
||||
@if (workspace.rentPaymentSaving(payment.id)) {
|
||||
<mat-spinner diameter="16" aria-hidden="true" /> Enregistrement…
|
||||
} @else {
|
||||
Marquer comme payé
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { WorkspaceService } from '../../services/workspace.service';
|
||||
import { RentPaymentsPage } from './rent-payments-page';
|
||||
|
||||
describe('RentPaymentsPage', () => {
|
||||
it('loads the schedule and exposes payment actions after its dependencies are ready', () => {
|
||||
const workspace = {
|
||||
apartments: signal([{ id: 'apartment-1', name: 'T2' }]),
|
||||
leases: signal([{ id: 'lease-1', apartmentId: 'apartment-1' }]),
|
||||
rentPayments: signal([
|
||||
{
|
||||
id: 'payment-1',
|
||||
leaseId: 'lease-1',
|
||||
month: '2026-07',
|
||||
dueDate: '2026-07-01',
|
||||
rent: 800,
|
||||
charges: 50,
|
||||
total: 850,
|
||||
status: 'LATE',
|
||||
paidAt: null,
|
||||
},
|
||||
]),
|
||||
apartmentLoadState: signal({ loaded: true, loading: false, error: '' }),
|
||||
leaseLoadState: signal({ loaded: true, loading: false, error: '' }),
|
||||
rentPaymentLoadState: signal({ loaded: true, loading: false, error: '' }),
|
||||
loadApartments: vi.fn(),
|
||||
loadLeases: vi.fn(),
|
||||
loadRentPayments: vi.fn(),
|
||||
markRentPaymentPaid: vi.fn(),
|
||||
rentPaymentSaving: vi.fn(() => false),
|
||||
rentPaymentSaveError: vi.fn(() => ''),
|
||||
downloadRentReceipt: vi.fn(),
|
||||
rentReceiptDownloading: vi.fn(() => false),
|
||||
rentReceiptDownloadError: vi.fn(() => ''),
|
||||
};
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: WorkspaceService, useValue: workspace }],
|
||||
});
|
||||
|
||||
const fixture = TestBed.createComponent(RentPaymentsPage);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(workspace.loadRentPayments).toHaveBeenCalled();
|
||||
expect(fixture.nativeElement.textContent).toContain('T2');
|
||||
fixture.nativeElement.querySelector('button').click();
|
||||
expect(workspace.markRentPaymentPaid).toHaveBeenCalledWith('payment-1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { RentPayment, RentPaymentStatus } from '../../services/rent-payment.service';
|
||||
import { WorkspaceService } from '../../services/workspace.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rent-payments-page',
|
||||
imports: [MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule],
|
||||
templateUrl: './rent-payments-page.html',
|
||||
styleUrl: './rent-payments-page.css',
|
||||
})
|
||||
export class RentPaymentsPage {
|
||||
readonly workspace = inject(WorkspaceService);
|
||||
readonly contentReady = computed(
|
||||
() =>
|
||||
this.workspace.rentPaymentLoadState().loaded &&
|
||||
this.workspace.leaseLoadState().loaded &&
|
||||
this.workspace.apartmentLoadState().loaded,
|
||||
);
|
||||
readonly contentLoading = computed(
|
||||
() =>
|
||||
this.workspace.rentPaymentLoadState().loading ||
|
||||
this.workspace.leaseLoadState().loading ||
|
||||
this.workspace.apartmentLoadState().loading,
|
||||
);
|
||||
readonly loadError = computed(
|
||||
() =>
|
||||
this.workspace.rentPaymentLoadState().error ||
|
||||
this.workspace.leaseLoadState().error ||
|
||||
this.workspace.apartmentLoadState().error,
|
||||
);
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
apartmentName(payment: RentPayment): string {
|
||||
const lease = this.workspace.leases().find((item) => item.id === payment.leaseId);
|
||||
return this.workspace.apartments().find((item) => item.id === lease?.apartmentId)?.name ?? 'Bien inconnu';
|
||||
}
|
||||
|
||||
monthLabel(month: string): string {
|
||||
return new Intl.DateTimeFormat('fr-FR', { month: 'long', year: 'numeric' }).format(
|
||||
new Date(`${month}-01T12:00:00`),
|
||||
);
|
||||
}
|
||||
|
||||
statusLabel(status: RentPaymentStatus): string {
|
||||
return { PAID: 'Payé', LATE: 'En retard', UPCOMING: 'À venir' }[status];
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.workspace.loadRentPayments();
|
||||
this.workspace.loadLeases();
|
||||
this.workspace.loadApartments();
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,16 @@
|
||||
<fa-icon [icon]="faFileLines" aria-hidden="true" />
|
||||
<span>Baux</span>
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="navigation-item"
|
||||
routerLink="/rent-payments"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<fa-icon [icon]="faReceipt" aria-hidden="true" />
|
||||
<span>Loyers</span>
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
faBuilding,
|
||||
faFileLines,
|
||||
faGear,
|
||||
faReceipt,
|
||||
faTableCellsLarge,
|
||||
faUsers,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
@@ -24,6 +25,7 @@ export class WorkspaceNavigation {
|
||||
readonly faBuilding = faBuilding;
|
||||
readonly faUsers = faUsers;
|
||||
readonly faFileLines = faFileLines;
|
||||
readonly faReceipt = faReceipt;
|
||||
readonly faGear = faGear;
|
||||
readonly userInitial = input.required<string>();
|
||||
readonly logoutRequested = output<void>();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { RentPaymentService } from './rent-payment.service';
|
||||
|
||||
describe('RentPaymentService', () => {
|
||||
it('uses the rent payment schedule, payment, and receipt endpoints', () => {
|
||||
const http = {
|
||||
get: vi.fn(() => of([])),
|
||||
put: vi.fn(() => of({})),
|
||||
};
|
||||
TestBed.configureTestingModule({
|
||||
providers: [RentPaymentService, { provide: HttpClient, useValue: http }],
|
||||
});
|
||||
const service = TestBed.inject(RentPaymentService);
|
||||
|
||||
service.getRentPayments().subscribe();
|
||||
service.markPaid('payment-1').subscribe();
|
||||
service.downloadReceipt('payment-1').subscribe();
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/api/rent-payments');
|
||||
expect(http.put).toHaveBeenCalledWith('/api/rent-payments/payment-1/paid', {});
|
||||
expect(http.get).toHaveBeenCalledWith('/api/rent-payments/payment-1/receipt', {
|
||||
responseType: 'blob',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export type RentPaymentStatus = 'PAID' | 'LATE' | 'UPCOMING';
|
||||
|
||||
export interface RentPayment {
|
||||
id: string;
|
||||
leaseId: string;
|
||||
month: string;
|
||||
dueDate: string;
|
||||
rent: number;
|
||||
charges: number;
|
||||
total: number;
|
||||
status: RentPaymentStatus;
|
||||
paidAt: string | null;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RentPaymentService {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
getRentPayments(): Observable<RentPayment[]> {
|
||||
return this.http.get<RentPayment[]>('/api/rent-payments');
|
||||
}
|
||||
|
||||
markPaid(id: string): Observable<RentPayment> {
|
||||
return this.http.put<RentPayment>(`/api/rent-payments/${id}/paid`, {});
|
||||
}
|
||||
|
||||
downloadReceipt(id: string): Observable<Blob> {
|
||||
return this.http.get(`/api/rent-payments/${id}/receipt`, { responseType: 'blob' });
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { of, Subject, throwError } from 'rxjs';
|
||||
import { vi } from 'vitest';
|
||||
import { ApartmentService } from './apartment.service';
|
||||
import { LeaseService } from './lease.service';
|
||||
import { RentPaymentService } from './rent-payment.service';
|
||||
import { TenantService } from './tenant.service';
|
||||
import { WorkspaceService } from './workspace.service';
|
||||
import { ConfirmationDialog } from '../components/confirmation-dialog/confirmation-dialog';
|
||||
@@ -37,6 +38,17 @@ describe('WorkspaceService', () => {
|
||||
securityDeposit: 800,
|
||||
status: 'ACTIVE' as const,
|
||||
};
|
||||
const rentPayment = {
|
||||
id: 'payment-1',
|
||||
leaseId: 'lease-1',
|
||||
month: '2026-01',
|
||||
dueDate: '2026-01-01',
|
||||
rent: 800,
|
||||
charges: 50,
|
||||
total: 850,
|
||||
status: 'LATE' as const,
|
||||
paidAt: null,
|
||||
};
|
||||
const apartments = {
|
||||
getApartments: vi.fn(),
|
||||
createApartment: vi.fn(),
|
||||
@@ -58,6 +70,11 @@ describe('WorkspaceService', () => {
|
||||
deleteLease: vi.fn(),
|
||||
downloadPdf: vi.fn(),
|
||||
};
|
||||
const rentPayments = {
|
||||
getRentPayments: vi.fn(),
|
||||
markPaid: vi.fn(),
|
||||
downloadReceipt: vi.fn(),
|
||||
};
|
||||
const dialog = {
|
||||
open: vi.fn(),
|
||||
};
|
||||
@@ -79,6 +96,9 @@ describe('WorkspaceService', () => {
|
||||
leases.createLease.mockReturnValue(of(lease));
|
||||
leases.deleteLease.mockReturnValue(of(undefined));
|
||||
leases.downloadPdf.mockReturnValue(of(new Blob(['pdf'])));
|
||||
rentPayments.getRentPayments.mockReturnValue(of([rentPayment]));
|
||||
rentPayments.markPaid.mockReturnValue(of({ ...rentPayment, status: 'PAID', paidAt: '2026-01-02' }));
|
||||
rentPayments.downloadReceipt.mockReturnValue(of(new Blob(['receipt'])));
|
||||
dialog.open.mockImplementation(() => ({ afterClosed: () => of(true) }));
|
||||
vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:test'), revokeObjectURL: vi.fn() });
|
||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
|
||||
@@ -88,6 +108,7 @@ describe('WorkspaceService', () => {
|
||||
{ provide: ApartmentService, useValue: apartments },
|
||||
{ provide: TenantService, useValue: tenants },
|
||||
{ provide: LeaseService, useValue: leases },
|
||||
{ provide: RentPaymentService, useValue: rentPayments },
|
||||
{ provide: MatDialog, useValue: dialog },
|
||||
],
|
||||
});
|
||||
@@ -326,13 +347,17 @@ describe('WorkspaceService', () => {
|
||||
service.loadLeases();
|
||||
service.saveLease(lease);
|
||||
service.deleteLease(lease.id);
|
||||
service.loadRentPayments();
|
||||
service.markRentPaymentPaid(rentPayment.id);
|
||||
|
||||
expect(service.apartments()).toEqual([apartment]);
|
||||
expect(service.tenants()).toEqual([tenant]);
|
||||
expect(service.leases()).toEqual([lease]);
|
||||
expect(service.rentPayments()).toEqual([rentPayment]);
|
||||
expect(apartments.updateApartment).toHaveBeenCalledWith('apartment-1', apartment);
|
||||
expect(tenants.updateTenant).toHaveBeenCalledWith('tenant-1', tenant);
|
||||
expect(leases.deleteLease).toHaveBeenCalledWith('lease-1');
|
||||
expect(rentPayments.markPaid).toHaveBeenCalledWith('payment-1');
|
||||
});
|
||||
|
||||
it('opens a confirmation pop-in before deleting a resource', () => {
|
||||
@@ -367,12 +392,13 @@ describe('WorkspaceService', () => {
|
||||
service.uploadTenantAttachment('tenant-1', new File(['id'], 'id.pdf'));
|
||||
service.downloadTenantAttachment('tenant-1', { id: 'attachment-1', fileName: 'id.pdf' });
|
||||
service.downloadLeasePdf(lease);
|
||||
service.downloadRentReceipt(rentPayment);
|
||||
service.downloadLeasePdf({ ...lease, id: undefined });
|
||||
|
||||
expect(tenants.deleteTenant).toHaveBeenCalledWith('tenant-1');
|
||||
expect(leases.deleteLease).toHaveBeenCalledWith('lease-1');
|
||||
expect(URL.createObjectURL).toHaveBeenCalledTimes(2);
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(2);
|
||||
expect(URL.createObjectURL).toHaveBeenCalledTimes(3);
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('displays a server attachment error or its fallback', () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { Apartment, ApartmentService } from './apartment.service';
|
||||
import { FileDownloadService } from './file-download.service';
|
||||
import { Lease, LeaseService } 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';
|
||||
@@ -19,6 +20,7 @@ export class WorkspaceService {
|
||||
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);
|
||||
|
||||
@@ -40,6 +42,14 @@ export class WorkspaceService {
|
||||
save: 'Impossible d’enregistrer ce bail.',
|
||||
delete: 'Impossible de supprimer ce bail.',
|
||||
});
|
||||
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;
|
||||
@@ -53,6 +63,8 @@ export class WorkspaceService {
|
||||
readonly leaseLoadState = this.leaseResource.loadState;
|
||||
readonly deletingLeaseIds = this.leaseResource.deletingIds;
|
||||
readonly leaseDeleteErrors = this.leaseResource.deleteErrors;
|
||||
readonly rentPayments = this.rentPaymentResource.items;
|
||||
readonly rentPaymentLoadState = this.rentPaymentResource.loadState;
|
||||
|
||||
readonly tenantAttachmentErrors = this.attachmentTransfers.errors;
|
||||
readonly uploadingTenantIds = this.attachmentTransfers.uploadingTenantIds;
|
||||
@@ -226,6 +238,39 @@ export class WorkspaceService {
|
||||
return this.downloads.error(this.leaseDownloadKey(leaseId));
|
||||
}
|
||||
|
||||
loadRentPayments(): void {
|
||||
this.rentPaymentResource.load();
|
||||
}
|
||||
|
||||
markRentPaymentPaid(id: string): void {
|
||||
this.rentPaymentResource.save(id, this.rentPaymentService.markPaid(id));
|
||||
}
|
||||
|
||||
rentPaymentSaving(id: string): boolean {
|
||||
return this.rentPaymentResource.savingKeys().has(id);
|
||||
}
|
||||
|
||||
rentPaymentSaveError(id: string): string {
|
||||
return this.rentPaymentResource.saveErrors()[id] ?? '';
|
||||
}
|
||||
|
||||
downloadRentReceipt(payment: RentPayment): void {
|
||||
this.downloads.download(
|
||||
this.rentReceiptDownloadKey(payment.id),
|
||||
this.rentPaymentService.downloadReceipt(payment.id),
|
||||
`quittance-${payment.month}.pdf`,
|
||||
'Impossible de télécharger cette quittance.',
|
||||
);
|
||||
}
|
||||
|
||||
rentReceiptDownloading(paymentId: string): boolean {
|
||||
return this.downloads.isPending(this.rentReceiptDownloadKey(paymentId));
|
||||
}
|
||||
|
||||
rentReceiptDownloadError(paymentId: string): string {
|
||||
return this.downloads.error(this.rentReceiptDownloadKey(paymentId));
|
||||
}
|
||||
|
||||
private resourceKey(id?: string): string {
|
||||
return id ?? newResourceKey;
|
||||
}
|
||||
@@ -234,6 +279,10 @@ export class WorkspaceService {
|
||||
return `lease:${leaseId}`;
|
||||
}
|
||||
|
||||
private rentReceiptDownloadKey(paymentId: string): string {
|
||||
return `rent-receipt:${paymentId}`;
|
||||
}
|
||||
|
||||
private requestConfirmation(data: ConfirmationDialogData, onConfirmed: () => void): void {
|
||||
this.dialog
|
||||
.open(ConfirmationDialog, {
|
||||
|
||||
Reference in new issue
Block a user