fix: use rent dues in rent schedule (#28)

This commit is contained in:
nessar committed 2026-08-17 14:18:15 +02:00
1 parent c2afe515c0
commit a039243d03
4 files changed
+274 -230

No files matched your search

@@ -49,19 +49,40 @@
font-weight: 700; font-weight: 700;
} }
.payment-status { .rent-due-labels {
display: flex;
flex-wrap: wrap;
gap: var(--space-2, 0.5rem);
}
.settlement-status,
.timing-status {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 700; font-weight: 700;
} }
.payment-status.paid { color: var(--accent, #0f766e); } .settlement-status.paid {
.payment-status.late { color: var(--danger, #b42318); } color: var(--accent, #0f766e);
.payment-status.upcoming { color: var(--text-muted, #687670); } }
.settlement-status.unpaid,
.settlement-status.partially_paid {
color: var(--warning, #9a6700);
}
.timing-status.overdue {
color: var(--danger, #b42318);
}
.timing-status.upcoming {
color: var(--text-muted, #687670);
}
@media (min-width: 700px) { @media (min-width: 700px) {
.rent-payment-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .rent-payment-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
} }
@media (min-width: 1100px) { @media (min-width: 1100px) {
.rent-payment-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .rent-payment-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
} }
@@ -1,112 +1,97 @@
<section class="page-section"> <section class="page-section">
<header class="page-heading"> <header class="page-heading">
<div> <div>
<p class="eyebrow">Location</p> <p class="eyebrow">Location</p>
<h1>Mes loyers</h1> <h1>Mes loyers</h1>
<p>Suivez les échéances et téléchargez les quittances après règlement.</p> <p>Suivez les échéances de vos baux actifs.</p>
</div> </div>
</header> </header>
<div class="content-region" [attr.aria-busy]="contentLoading()"> <div class="content-region" [attr.aria-busy]="contentLoading()">
@if (!contentReady() && loadError()) { @if (!contentReady() && loadError()) {
<article class="load-error" role="alert"> <article class="load-error" role="alert">
<h2>Chargement impossible</h2> <h2>Chargement impossible</h2>
<p>{{ loadError() }}</p> <p>{{ loadError() }}</p>
<button mat-stroked-button type="button" (click)="load()">Réessayer</button> <button mat-stroked-button type="button" (click)="load()">Réessayer</button>
</article> </article>
} @else if (!contentReady()) { } @else if (!contentReady()) {
<div class="rent-payment-grid" aria-label="Chargement des loyers"> <div class="rent-payment-grid" aria-label="Chargement des loyers">
@for (placeholder of [1, 2, 3]; track placeholder) { @for (placeholder of [1, 2, 3]; track placeholder) {
<article class="skeleton-card" aria-hidden="true"> <article class="skeleton-card" aria-hidden="true">
<span class="skeleton-line skeleton-line-short"></span> <span class="skeleton-line skeleton-line-short"></span>
<span class="skeleton-line skeleton-line-title"></span> <span class="skeleton-line skeleton-line-title"></span>
<span class="skeleton-line skeleton-line-medium"></span> <span class="skeleton-line skeleton-line-medium"></span>
</article> </article>
} }
</div> </div>
} @else { } @else {
@if (contentLoading()) { @if (contentLoading()) {
<mat-progress-bar mode="indeterminate" aria-label="Actualisation des loyers" /> <mat-progress-bar mode="indeterminate" aria-label="Actualisation des loyers" />
} }
@if (loadError()) { @if (loadError()) {
<div class="refresh-error" role="alert"> <div class="refresh-error" role="alert">
<p>{{ loadError() }}</p> <p>{{ loadError() }}</p>
<button mat-button type="button" (click)="load()">Réessayer</button> <button mat-button type="button" (click)="load()">Réessayer</button>
</div> </div>
} }
@if (workspace.rentPayments().length === 0) { @if (workspace.rentDues().length === 0) {
<article class="empty-state"> <article class="empty-state">
<p class="eyebrow">Échéancier</p> <p class="eyebrow">Échéancier</p>
<h2>Aucune échéance de loyer</h2> <h2>Aucune échéance de loyer</h2>
<p>Les échéances sont créées automatiquement pour les baux actifs.</p> <p>Les échéances sont créées automatiquement pour les baux actifs.</p>
</article> </article>
} @else { } @else {
<div class="schedule-actions"> <div class="schedule-actions">
<button <button
mat-stroked-button mat-stroked-button
type="button" type="button"
aria-controls="rent-payment-schedule" aria-controls="rent-payment-schedule"
[attr.aria-pressed]="showFullSchedule()" [attr.aria-pressed]="showFullSchedule()"
(click)="toggleSchedule()" (click)="toggleSchedule()"
> >
{{ showFullSchedule() ? 'Afficher l’échéancier courant' : 'Afficher toutes les échéances' }} {{
</button> showFullSchedule() ? 'Afficher l’échéancier courant' : 'Afficher toutes les échéances'
</div> }}
<div id="rent-payment-schedule" class="rent-payment-grid"> </button>
@for (payment of displayedPayments(); track payment.id) { </div>
<article class="rent-payment" [attr.aria-busy]="workspace.rentPaymentSaving(payment.id)"> <div id="rent-payment-schedule" class="rent-payment-grid">
<div class="payment-topline"> @for (rentDue of displayedRentDues(); track rentDue.id) {
<span class="payment-status" [class]="payment.status.toLowerCase()"> <article class="rent-payment">
{{ statusLabel(payment.status) }} <div class="payment-topline">
</span> <div class="rent-due-labels">
<strong>{{ payment.total.toLocaleString('fr-FR') }} €</strong> <span class="settlement-status" [class]="rentDue.settlement.toLowerCase()">
</div> {{ settlementLabel(rentDue.settlement) }}
<h2>{{ apartmentName(payment) }}</h2> </span>
<p class="payment-month">{{ monthLabel(payment.month) }}</p> <span class="timing-status" [class]="rentDue.timing.toLowerCase()">
<dl> {{ timingLabel(rentDue.timing) }}
<div> </span>
<dt>Loyer</dt> </div>
<dd>{{ payment.rent.toLocaleString('fr-FR') }} €</dd> <strong>{{ rentDue.total.toLocaleString('fr-FR') }} €</strong>
</div> </div>
<div> <h2>{{ apartmentName(rentDue) }}</h2>
<dt>Charges</dt> <p class="payment-month">{{ monthLabel(rentDue.month) }}</p>
<dd>{{ payment.charges.toLocaleString('fr-FR') }} €</dd> <dl>
</div> <div>
<div> <dt>Loyer</dt>
<dt>Échéance</dt> <dd>{{ rentDue.rent.toLocaleString('fr-FR') }} €</dd>
<dd>{{ payment.dueDate | date:'dd MMMM yyyy' }}</dd> </div>
</div> <div>
</dl> <dt>Charges</dt>
@if (workspace.rentPaymentSaveError(payment.id) || workspace.rentReceiptDownloadError(payment.id)) { <dd>{{ rentDue.charges.toLocaleString('fr-FR') }} €</dd>
<p class="card-error" role="alert"> </div>
{{ workspace.rentPaymentSaveError(payment.id) || workspace.rentReceiptDownloadError(payment.id) }} <div>
</p> <dt>Restant à régler</dt>
} <dd>{{ rentDue.remainingAmount.toLocaleString('fr-FR') }} €</dd>
<div class="card-actions" aria-live="polite"> </div>
@if (payment.status === 'PAID') { <div>
<button mat-button type="button" [disabled]="workspace.rentReceiptDownloading(payment.id)" <dt>Échéance</dt>
(click)="workspace.downloadRentReceipt(payment)"> <dd>{{ rentDue.dueDate | date: 'dd MMMM yyyy' }}</dd>
@if (workspace.rentReceiptDownloading(payment.id)) { </div>
<mat-spinner diameter="16" aria-hidden="true" /> Téléchargement… </dl>
} @else { </article>
Télécharger la quittance }
} </div>
</button> }
} @else { }
<button mat-flat-button type="button" [disabled]="workspace.rentPaymentSaving(payment.id)" </div>
(click)="markPaid(payment)">
@if (workspace.rentPaymentSaving(payment.id)) {
<mat-spinner diameter="16" aria-hidden="true" /> Enregistrement…
} @else {
Marquer comme payé
}
</button>
}
</div>
</article>
}
</div>
}
}
</div>
</section> </section>
@@ -1,93 +1,93 @@
import { signal } from '@angular/core'; import { signal } from '@angular/core';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog'; import { RentDue } from '../../services/lease.service';
import { of } from 'rxjs';
import { WorkspaceService } from '../../services/workspace.service'; import { WorkspaceService } from '../../services/workspace.service';
import { RentPaymentsPage } from './rent-payments-page'; import { RentPaymentsPage } from './rent-payments-page';
describe('RentPaymentsPage', () => { describe('RentPaymentsPage', () => {
it('loads the schedule and exposes payment actions after its dependencies are ready', () => { it('loads and renders RentDue records instead of the legacy combined model', () => {
const workspace = { const workspace = {
apartments: signal([{ id: 'apartment-1', name: 'T2' }]), ...createWorkspace([rentDue('due-1', '2027-08', 'UPCOMING')]),
leases: signal([{ id: 'lease-1', apartmentId: 'apartment-1' }]), rentPayments: signal([payment('legacy-payment', '2026-07', 'LATE')]),
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(), loadRentPayments: vi.fn(),
markRentPaymentPaid: vi.fn(),
rentPaymentSaving: vi.fn(() => false),
rentPaymentSaveError: vi.fn(() => ''),
downloadRentReceipt: vi.fn(),
rentReceiptDownloading: vi.fn(() => false),
rentReceiptDownloadError: vi.fn(() => ''),
}; };
const dialog = {
open: vi.fn(() => ({ afterClosed: () => of({ rent: 800, charges: 50 }) })),
};
TestBed.configureTestingModule({
providers: [
{ provide: WorkspaceService, useValue: workspace },
{ provide: MatDialog, useValue: dialog },
],
});
const fixture = TestBed.createComponent(RentPaymentsPage);
fixture.detectChanges();
expect(workspace.loadRentPayments).toHaveBeenCalled();
expect(fixture.nativeElement.textContent).toContain('T2');
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); const fixture = createFixture(workspace);
expect(paymentMonths(fixture)).toEqual(['février 2026', 'mars 2026', 'avril 2026', 'mai 2026']); expect(workspace.loadRentDues).toHaveBeenCalledOnce();
expect(workspace.loadRentPayments).not.toHaveBeenCalled();
expect(paymentMonths(fixture)).toEqual(['août 2027']);
expect(fixture.nativeElement.textContent).not.toContain('Marquer comme payé');
}); });
it('starts the current schedule at the first unpaid rent when none has been paid', () => { it('renders amounts, settlement and timing as independent RentDue facts', () => {
const fixture = createFixture(
createWorkspace([
rentDue('due-1', '2026-07', 'OVERDUE', 'PARTIALLY_PAID', {
remainingAmount: 350,
}),
]),
);
const card = fixture.nativeElement.querySelector('.rent-payment') as HTMLElement;
expect(card.textContent).toContain('T2');
expect(card.textContent).toContain('800 €');
expect(card.textContent).toContain('50 €');
expect(card.textContent).toContain('850 €');
expect(card.textContent).toContain('350 €');
expect(card.querySelector('.settlement-status.partially_paid')?.textContent).toContain(
'Partiellement réglée',
);
expect(card.querySelector('.timing-status.overdue')?.textContent).toContain('En retard');
expect(card.querySelector('.card-actions')).toBeNull();
});
it('shows overdue dues through the next upcoming due', () => {
const workspace = createWorkspace([ const workspace = createWorkspace([
payment('late-january', '2026-01', 'LATE'), rentDue('overdue-january', '2026-01', 'OVERDUE'),
payment('late-february', '2026-02', 'LATE'), rentDue('overdue-february', '2026-02', 'OVERDUE'),
payment('upcoming-march', '2026-03', 'UPCOMING'), rentDue('upcoming-march', '2026-03', 'UPCOMING'),
payment('upcoming-april', '2026-04', 'UPCOMING'), rentDue('upcoming-april', '2026-04', 'UPCOMING'),
]); ]);
const fixture = createFixture(workspace); const fixture = createFixture(workspace);
expect(paymentMonths(fixture)).toEqual(['janvier 2026', 'février 2026', 'mars 2026']); expect(paymentMonths(fixture)).toEqual(['janvier 2026', 'février 2026', 'mars 2026']);
}); });
it('starts the current schedule at the older of the last paid and first unpaid due', () => {
const workspace = createWorkspace([
rentDue('paid-january', '2026-01', 'OVERDUE', 'PAID'),
rentDue('unpaid-february', '2026-02', 'OVERDUE'),
rentDue('paid-march', '2026-03', 'OVERDUE', 'PAID'),
rentDue('unpaid-april', '2026-04', 'OVERDUE'),
rentDue('upcoming-may', '2026-05', 'UPCOMING'),
rentDue('upcoming-june', '2026-06', 'UPCOMING'),
]);
const fixture = createFixture(workspace);
expect(paymentMonths(fixture)).toEqual([
'février 2026',
'mars 2026',
'avril 2026',
'mai 2026',
]);
});
it('shows only the next due when none are overdue', () => {
const workspace = createWorkspace([
rentDue('upcoming-march', '2026-03', 'UPCOMING'),
rentDue('upcoming-april', '2026-04', 'UPCOMING'),
]);
const fixture = createFixture(workspace);
expect(paymentMonths(fixture)).toEqual(['mars 2026']);
});
it('reveals and hides the full chronological schedule', () => { it('reveals and hides the full chronological schedule', () => {
const workspace = createWorkspace([ const workspace = createWorkspace([
payment('upcoming-april', '2026-04', 'UPCOMING'), rentDue('upcoming-april', '2026-04', 'UPCOMING'),
payment('paid-january', '2026-01', 'PAID'), rentDue('overdue-january', '2026-01', 'OVERDUE'),
payment('upcoming-march', '2026-03', 'UPCOMING'), rentDue('upcoming-march', '2026-03', 'UPCOMING'),
payment('late-february', '2026-02', 'LATE'), rentDue('overdue-february', '2026-02', 'OVERDUE'),
]); ]);
const fixture = createFixture(workspace); const fixture = createFixture(workspace);
@@ -99,7 +99,12 @@ describe('RentPaymentsPage', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(toggle.getAttribute('aria-pressed')).toBe('true'); expect(toggle.getAttribute('aria-pressed')).toBe('true');
expect(paymentMonths(fixture)).toEqual(['janvier 2026', 'février 2026', 'mars 2026', 'avril 2026']); expect(paymentMonths(fixture)).toEqual([
'janvier 2026',
'février 2026',
'mars 2026',
'avril 2026',
]);
toggle.click(); toggle.click();
fixture.detectChanges(); fixture.detectChanges();
@@ -124,10 +129,7 @@ describe('RentPaymentsPage', () => {
function createFixture(workspace: ReturnType<typeof createWorkspace>) { function createFixture(workspace: ReturnType<typeof createWorkspace>) {
TestBed.resetTestingModule(); TestBed.resetTestingModule();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [{ provide: WorkspaceService, useValue: workspace }],
{ provide: WorkspaceService, useValue: workspace },
{ provide: MatDialog, useValue: { open: vi.fn() } },
],
}); });
const fixture = TestBed.createComponent(RentPaymentsPage); const fixture = TestBed.createComponent(RentPaymentsPage);
fixture.detectChanges(); fixture.detectChanges();
@@ -135,25 +137,41 @@ function createFixture(workspace: ReturnType<typeof createWorkspace>) {
} }
function createWorkspace( function createWorkspace(
rentPayments: ReturnType<typeof payment>[], rentDues: RentDue[],
loadState = { loaded: true, loading: false, error: '' }, loadState = { loaded: true, loading: false, error: '' },
) { ) {
return { return {
apartments: signal([{ id: 'apartment-1', name: 'T2' }]), apartments: signal([{ id: 'apartment-1', name: 'T2' }]),
leases: signal([{ id: 'lease-1', apartmentId: 'apartment-1' }]), leases: signal([{ id: 'lease-1', apartmentId: 'apartment-1' }]),
rentPayments: signal(rentPayments), rentDues: signal(rentDues),
apartmentLoadState: signal(loadState), apartmentLoadState: signal(loadState),
leaseLoadState: signal(loadState), leaseLoadState: signal(loadState),
rentPaymentLoadState: signal(loadState), rentDueLoadState: signal(loadState),
loadApartments: vi.fn(), loadApartments: vi.fn(),
loadLeases: vi.fn(), loadLeases: vi.fn(),
loadRentPayments: vi.fn(), loadRentDues: vi.fn(),
markRentPaymentPaid: vi.fn(), };
rentPaymentSaving: vi.fn(() => false), }
rentPaymentSaveError: vi.fn(() => ''),
downloadRentReceipt: vi.fn(), function rentDue(
rentReceiptDownloading: vi.fn(() => false), id: string,
rentReceiptDownloadError: vi.fn(() => ''), month: string,
timing: RentDue['timing'],
settlement: RentDue['settlement'] = 'UNPAID',
overrides: Partial<RentDue> = {},
): RentDue {
return {
id,
leaseId: 'lease-1',
month,
dueDate: `${month}-05`,
rent: 800,
charges: 50,
total: 850,
remainingAmount: 850,
settlement,
timing,
...overrides,
}; };
} }
@@ -172,5 +190,7 @@ function payment(id: string, month: string, status: 'PAID' | 'LATE' | 'UPCOMING'
} }
function paymentMonths(fixture: ReturnType<typeof createFixture>): string[] { function paymentMonths(fixture: ReturnType<typeof createFixture>): string[] {
return [...fixture.nativeElement.querySelectorAll('.payment-month')].map((element) => element.textContent.trim()); return [...fixture.nativeElement.querySelectorAll('.payment-month')].map((element) =>
element.textContent.trim(),
);
} }
@@ -1,44 +1,39 @@
import { Component, computed, inject, signal } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { RentDue } from '../../services/lease.service';
import { RentPayment, RentPaymentStatus } from '../../services/rent-payment.service';
import { currentRentSchedule, sortRentPayments } from '../../services/rent-payment-schedule';
import { WorkspaceService } from '../../services/workspace.service'; import { WorkspaceService } from '../../services/workspace.service';
import { MatDialog } from '@angular/material/dialog';
import { RentPaymentValidationDialog } from '../rent-payment-validation-dialog/rent-payment-validation-dialog';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@Component({ @Component({
selector: 'app-rent-payments-page', selector: 'app-rent-payments-page',
imports: [MatButtonModule, MatProgressBarModule, MatProgressSpinnerModule, DatePipe], imports: [MatButtonModule, MatProgressBarModule, DatePipe],
templateUrl: './rent-payments-page.html', templateUrl: './rent-payments-page.html',
styleUrl: './rent-payments-page.css', styleUrl: './rent-payments-page.css',
}) })
export class RentPaymentsPage { export class RentPaymentsPage {
readonly workspace = inject(WorkspaceService); readonly workspace = inject(WorkspaceService);
private readonly dialog = inject(MatDialog);
readonly showFullSchedule = signal(false); readonly showFullSchedule = signal(false);
readonly displayedPayments = computed(() => readonly displayedRentDues = computed(() =>
this.showFullSchedule() this.showFullSchedule()
? sortRentPayments(this.workspace.rentPayments()) ? sortRentDues(this.workspace.rentDues())
: currentRentSchedule(this.workspace.rentPayments()), : currentRentDues(this.workspace.rentDues()),
); );
readonly contentReady = computed( readonly contentReady = computed(
() => () =>
this.workspace.rentPaymentLoadState().loaded && this.workspace.rentDueLoadState().loaded &&
this.workspace.leaseLoadState().loaded && this.workspace.leaseLoadState().loaded &&
this.workspace.apartmentLoadState().loaded, this.workspace.apartmentLoadState().loaded,
); );
readonly contentLoading = computed( readonly contentLoading = computed(
() => () =>
this.workspace.rentPaymentLoadState().loading || this.workspace.rentDueLoadState().loading ||
this.workspace.leaseLoadState().loading || this.workspace.leaseLoadState().loading ||
this.workspace.apartmentLoadState().loading, this.workspace.apartmentLoadState().loading,
); );
readonly loadError = computed( readonly loadError = computed(
() => () =>
this.workspace.rentPaymentLoadState().error || this.workspace.rentDueLoadState().error ||
this.workspace.leaseLoadState().error || this.workspace.leaseLoadState().error ||
this.workspace.apartmentLoadState().error, this.workspace.apartmentLoadState().error,
); );
@@ -47,9 +42,12 @@ export class RentPaymentsPage {
this.load(); this.load();
} }
apartmentName(payment: RentPayment): string { apartmentName(rentDue: RentDue): string {
const lease = this.workspace.leases().find((item) => item.id === payment.leaseId); const lease = this.workspace.leases().find((item) => item.id === rentDue.leaseId);
return this.workspace.apartments().find((item) => item.id === lease?.apartmentId)?.name ?? 'Bien inconnu'; return (
this.workspace.apartments().find((item) => item.id === lease?.apartmentId)?.name ??
'Bien inconnu'
);
} }
monthLabel(month: string): string { monthLabel(month: string): string {
@@ -58,12 +56,18 @@ export class RentPaymentsPage {
); );
} }
statusLabel(status: RentPaymentStatus): string { settlementLabel(settlement: RentDue['settlement']): string {
return { PAID: 'Payé', LATE: 'En retard', UPCOMING: 'À venir' }[status]; return { UNPAID: 'Non réglée', PARTIALLY_PAID: 'Partiellement réglée', PAID: 'Réglée' }[
settlement
];
}
timingLabel(timing: RentDue['timing']): string {
return { UPCOMING: 'À venir', OVERDUE: 'En retard' }[timing];
} }
load(): void { load(): void {
this.workspace.loadRentPayments(); this.workspace.loadRentDues();
this.workspace.loadLeases(); this.workspace.loadLeases();
this.workspace.loadApartments(); this.workspace.loadApartments();
} }
@@ -71,19 +75,33 @@ export class RentPaymentsPage {
toggleSchedule(): void { toggleSchedule(): void {
this.showFullSchedule.update((showFullSchedule) => !showFullSchedule); this.showFullSchedule.update((showFullSchedule) => !showFullSchedule);
} }
}
markPaid(payment: RentPayment): void { function sortRentDues(rentDues: readonly RentDue[]): RentDue[] {
this.dialog return [...rentDues].sort((left, right) => left.dueDate.localeCompare(right.dueDate));
.open(RentPaymentValidationDialog, { }
data: { rent: payment.rent, charges: payment.charges },
width: '400px', function currentRentDues(rentDues: readonly RentDue[]): RentDue[] {
}) const rentDuesByLease = new Map<string, RentDue[]>();
.afterClosed() for (const rentDue of sortRentDues(rentDues)) {
.subscribe((result) => { const schedule = rentDuesByLease.get(rentDue.leaseId);
if (result) { if (schedule) schedule.push(rentDue);
this.workspace.markRentPaymentPaid(payment.id, result.rent, result.charges); else rentDuesByLease.set(rentDue.leaseId, [rentDue]);
}
});
} }
return sortRentDues([...rentDuesByLease.values()].flatMap(currentSchedule));
}
function currentSchedule(schedule: RentDue[]): RentDue[] {
const nextDueIndex = schedule.findIndex((rentDue) => rentDue.timing === 'UPCOMING');
const throughNextDue = nextDueIndex === -1 ? schedule : schedule.slice(0, nextDueIndex + 1);
const lastPaidIndex = throughNextDue.reduce(
(lastPaid, rentDue, index) => (rentDue.settlement === 'PAID' ? index : lastPaid),
-1,
);
const firstUnpaidIndex = throughNextDue.findIndex((rentDue) => rentDue.settlement !== 'PAID');
const firstVisibleIndex = Math.min(
...[lastPaidIndex, firstUnpaidIndex].filter((index) => index !== -1),
);
return firstVisibleIndex === Infinity ? [] : throughNextDue.slice(firstVisibleIndex);
} }