feat(#21): show current rent schedule
This commit is contained in:
1 parent
9cfb55d79c
commit
990792c8c2
5 files changed
+179
-5
No files matched your search
@@ -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);
|
||||
|
||||
@@ -41,8 +41,19 @@
|
||||
<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) {
|
||||
<div class="schedule-actions">
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
aria-controls="rent-payment-schedule"
|
||||
[attr.aria-pressed]="showFullSchedule()"
|
||||
(click)="toggleSchedule()"
|
||||
>
|
||||
{{ showFullSchedule() ? 'Afficher l’échéancier courant' : 'Afficher toutes les échéances' }}
|
||||
</button>
|
||||
</div>
|
||||
<div id="rent-payment-schedule" class="rent-payment-grid">
|
||||
@for (payment of displayedPayments(); 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()">
|
||||
@@ -98,4 +109,4 @@
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
@@ -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<typeof createWorkspace>) {
|
||||
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<typeof payment>[],
|
||||
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<typeof createFixture>): string[] {
|
||||
return [...fixture.nativeElement.querySelectorAll('.payment-month')].map((element) => element.textContent.trim());
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user