diff --git a/frontend/src/app/components/rent-payments-page/rent-payments-page.css b/frontend/src/app/components/rent-payments-page/rent-payments-page.css
index e6c5cce..3dad636 100644
--- a/frontend/src/app/components/rent-payments-page/rent-payments-page.css
+++ b/frontend/src/app/components/rent-payments-page/rent-payments-page.css
@@ -3,6 +3,10 @@
gap: var(--space-4, 1rem);
}
+.schedule-actions {
+ margin-bottom: var(--space-4, 1rem);
+}
+
.rent-payment {
padding: var(--space-5, 1.25rem);
border: 1px solid var(--border, #dce5df);
diff --git a/frontend/src/app/components/rent-payments-page/rent-payments-page.html b/frontend/src/app/components/rent-payments-page/rent-payments-page.html
index f8ff2ad..45d4bce 100644
--- a/frontend/src/app/components/rent-payments-page/rent-payments-page.html
+++ b/frontend/src/app/components/rent-payments-page/rent-payments-page.html
@@ -41,8 +41,19 @@
Les échéances sont créées automatiquement pour les baux actifs.
} @else {
-
- @for (payment of workspace.rentPayments(); track payment.id) {
+
+
+
+
+ @for (payment of displayedPayments(); track payment.id) {
@@ -98,4 +109,4 @@
}
}
-
\ No newline at end of file
+
diff --git a/frontend/src/app/components/rent-payments-page/rent-payments-page.spec.ts b/frontend/src/app/components/rent-payments-page/rent-payments-page.spec.ts
index 1d5d862..f176171 100644
--- a/frontend/src/app/components/rent-payments-page/rent-payments-page.spec.ts
+++ b/frontend/src/app/components/rent-payments-page/rent-payments-page.spec.ts
@@ -51,8 +51,126 @@ describe('RentPaymentsPage', () => {
expect(workspace.loadRentPayments).toHaveBeenCalled();
expect(fixture.nativeElement.textContent).toContain('T2');
- fixture.nativeElement.querySelector('button').click();
+ fixture.nativeElement.querySelector('.card-actions button').click();
expect(dialog.open).toHaveBeenCalled();
expect(workspace.markRentPaymentPaid).toHaveBeenCalledWith('payment-1', 800, 50);
});
+
+ it('shows the current schedule from an older unpaid rent through the next rent due', () => {
+ const workspace = createWorkspace([
+ payment('paid-january', '2026-01', 'PAID'),
+ payment('late-february', '2026-02', 'LATE'),
+ payment('paid-march', '2026-03', 'PAID'),
+ payment('late-april', '2026-04', 'LATE'),
+ payment('upcoming-may', '2026-05', 'UPCOMING'),
+ payment('upcoming-june', '2026-06', 'UPCOMING'),
+ ]);
+ const fixture = createFixture(workspace);
+
+ expect(paymentMonths(fixture)).toEqual(['février 2026', 'mars 2026', 'avril 2026', 'mai 2026']);
+ });
+
+ it('starts the current schedule at the first unpaid rent when none has been paid', () => {
+ const workspace = createWorkspace([
+ payment('late-january', '2026-01', 'LATE'),
+ payment('late-february', '2026-02', 'LATE'),
+ payment('upcoming-march', '2026-03', 'UPCOMING'),
+ payment('upcoming-april', '2026-04', 'UPCOMING'),
+ ]);
+ const fixture = createFixture(workspace);
+
+ expect(paymentMonths(fixture)).toEqual(['janvier 2026', 'février 2026', 'mars 2026']);
+ });
+
+ it('reveals and hides the full chronological schedule', () => {
+ const workspace = createWorkspace([
+ payment('upcoming-april', '2026-04', 'UPCOMING'),
+ payment('paid-january', '2026-01', 'PAID'),
+ payment('upcoming-march', '2026-03', 'UPCOMING'),
+ payment('late-february', '2026-02', 'LATE'),
+ ]);
+ const fixture = createFixture(workspace);
+
+ const toggle = fixture.nativeElement.querySelector('[aria-pressed]') as HTMLButtonElement;
+ expect(toggle.textContent).toContain('Afficher toutes les échéances');
+ expect(paymentMonths(fixture)).toEqual(['janvier 2026', 'février 2026', 'mars 2026']);
+
+ toggle.click();
+ fixture.detectChanges();
+
+ expect(toggle.getAttribute('aria-pressed')).toBe('true');
+ expect(paymentMonths(fixture)).toEqual(['janvier 2026', 'février 2026', 'mars 2026', 'avril 2026']);
+
+ toggle.click();
+ fixture.detectChanges();
+
+ expect(toggle.getAttribute('aria-pressed')).toBe('false');
+ expect(paymentMonths(fixture)).toEqual(['janvier 2026', 'février 2026', 'mars 2026']);
+ });
+
+ it('shows loading and error states before the schedule is ready', () => {
+ const loadingFixture = createFixture(
+ createWorkspace([], { loaded: false, loading: true, error: '' }),
+ );
+ expect(loadingFixture.nativeElement.querySelector('.skeleton-card')).not.toBeNull();
+
+ const errorFixture = createFixture(
+ createWorkspace([], { loaded: false, loading: false, error: 'Erreur de chargement.' }),
+ );
+ expect(errorFixture.nativeElement.textContent).toContain('Erreur de chargement.');
+ });
});
+
+function createFixture(workspace: ReturnType) {
+ TestBed.resetTestingModule();
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: WorkspaceService, useValue: workspace },
+ { provide: MatDialog, useValue: { open: vi.fn() } },
+ ],
+ });
+ const fixture = TestBed.createComponent(RentPaymentsPage);
+ fixture.detectChanges();
+ return fixture;
+}
+
+function createWorkspace(
+ rentPayments: ReturnType[],
+ loadState = { loaded: true, loading: false, error: '' },
+) {
+ return {
+ apartments: signal([{ id: 'apartment-1', name: 'T2' }]),
+ leases: signal([{ id: 'lease-1', apartmentId: 'apartment-1' }]),
+ rentPayments: signal(rentPayments),
+ apartmentLoadState: signal(loadState),
+ leaseLoadState: signal(loadState),
+ rentPaymentLoadState: signal(loadState),
+ 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(() => ''),
+ };
+}
+
+function payment(id: string, month: string, status: 'PAID' | 'LATE' | 'UPCOMING') {
+ return {
+ id,
+ leaseId: 'lease-1',
+ month,
+ dueDate: `${month}-05`,
+ rent: 800,
+ charges: 50,
+ total: 850,
+ status,
+ paidAt: status === 'PAID' ? `${month}-05` : null,
+ };
+}
+
+function paymentMonths(fixture: ReturnType): string[] {
+ return [...fixture.nativeElement.querySelectorAll('.payment-month')].map((element) => element.textContent.trim());
+}
diff --git a/frontend/src/app/components/rent-payments-page/rent-payments-page.ts b/frontend/src/app/components/rent-payments-page/rent-payments-page.ts
index 8310216..cce9968 100644
--- a/frontend/src/app/components/rent-payments-page/rent-payments-page.ts
+++ b/frontend/src/app/components/rent-payments-page/rent-payments-page.ts
@@ -1,8 +1,9 @@
-import { Component, computed, inject } from '@angular/core';
+import { Component, computed, inject, signal } 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 { currentRentSchedule, sortRentPayments } from '../../services/rent-payment-schedule';
import { WorkspaceService } from '../../services/workspace.service';
import { MatDialog } from '@angular/material/dialog';
import { RentPaymentValidationDialog } from '../rent-payment-validation-dialog/rent-payment-validation-dialog';
@@ -17,6 +18,12 @@ import { DatePipe } from '@angular/common';
export class RentPaymentsPage {
readonly workspace = inject(WorkspaceService);
private readonly dialog = inject(MatDialog);
+ readonly showFullSchedule = signal(false);
+ readonly displayedPayments = computed(() =>
+ this.showFullSchedule()
+ ? sortRentPayments(this.workspace.rentPayments())
+ : currentRentSchedule(this.workspace.rentPayments()),
+ );
readonly contentReady = computed(
() =>
this.workspace.rentPaymentLoadState().loaded &&
@@ -61,6 +68,10 @@ export class RentPaymentsPage {
this.workspace.loadApartments();
}
+ toggleSchedule(): void {
+ this.showFullSchedule.update((showFullSchedule) => !showFullSchedule);
+ }
+
markPaid(payment: RentPayment): void {
this.dialog
.open(RentPaymentValidationDialog, {
@@ -74,4 +85,5 @@ export class RentPaymentsPage {
}
});
}
+
}
diff --git a/frontend/src/app/services/rent-payment-schedule.ts b/frontend/src/app/services/rent-payment-schedule.ts
new file mode 100644
index 0000000..52a5e3f
--- /dev/null
+++ b/frontend/src/app/services/rent-payment-schedule.ts
@@ -0,0 +1,29 @@
+import { RentPayment } from './rent-payment.service';
+
+export function sortRentPayments(payments: readonly RentPayment[]): RentPayment[] {
+ return [...payments].sort((left, right) => left.dueDate.localeCompare(right.dueDate));
+}
+
+export function currentRentSchedule(payments: readonly RentPayment[]): RentPayment[] {
+ const paymentsByLease = new Map();
+ for (const payment of sortRentPayments(payments)) {
+ const schedule = paymentsByLease.get(payment.leaseId);
+ if (schedule) schedule.push(payment);
+ else paymentsByLease.set(payment.leaseId, [payment]);
+ }
+
+ return sortRentPayments([...paymentsByLease.values()].flatMap(currentSchedule));
+}
+
+function currentSchedule(schedule: RentPayment[]): RentPayment[] {
+ const nextDueIndex = schedule.findIndex((payment) => payment.status === 'UPCOMING');
+ const paymentsThroughNextDue = nextDueIndex === -1 ? schedule : schedule.slice(0, nextDueIndex + 1);
+ const lastPaidIndex = paymentsThroughNextDue.reduce(
+ (lastPaid, payment, index) => (payment.status === 'PAID' ? index : lastPaid),
+ -1,
+ );
+ const firstUnpaidIndex = paymentsThroughNextDue.findIndex((payment) => payment.status !== 'PAID');
+ const firstVisibleIndex = Math.min(...[lastPaidIndex, firstUnpaidIndex].filter((index) => index !== -1));
+
+ return firstVisibleIndex === Infinity ? [] : paymentsThroughNextDue.slice(firstVisibleIndex);
+}