refactor: simplify lease card and rent payments page logic
This commit is contained in:
1 parent
a039243d03
commit
6ce0719a20
4 files changed
+10
-177
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
<article class="lease-card" [attr.aria-busy]="deleting() || downloading() || activating()">
|
<article class="lease-card" [attr.aria-busy]="deleting() || downloading() || activating()">
|
||||||
<div class="lease-topline">
|
<div class="lease-topline">
|
||||||
<span class="status-dot"></span><span>{{ statusLabel() }}</span
|
<span class="status-dot"></span><span>{{ statusLabel() }}</span><strong>{{
|
||||||
><strong>{{ lease().monthlyRent.toLocaleString('fr-FR') }} €</strong>
|
lease().monthlyRent.toLocaleString('fr-FR') }} €</strong>
|
||||||
</div>
|
</div>
|
||||||
<h2>{{ apartmentName() }}</h2>
|
<h2>{{ apartmentName() }}</h2>
|
||||||
<p class="tenant-name">{{ tenantNames() }}</p>
|
<p class="tenant-name">{{ tenantNames() }}</p>
|
||||||
@@ -30,50 +30,14 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (rentDues().length) {
|
|
||||||
<section class="rent-schedule" aria-label="Échéancier de loyer">
|
|
||||||
<h3>Échéancier</h3>
|
|
||||||
@for (rentDue of rentDues(); track rentDue.id) {
|
|
||||||
<article class="rent-due">
|
|
||||||
<strong>{{ rentDue.month }}</strong>
|
|
||||||
<dl>
|
|
||||||
<div>
|
|
||||||
<dt>Loyer</dt>
|
|
||||||
<dd>{{ rentDue.rent.toLocaleString('fr-FR') }} €</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Charges</dt>
|
|
||||||
<dd>{{ rentDue.charges.toLocaleString('fr-FR') }} €</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Total</dt>
|
|
||||||
<dd>{{ rentDue.total.toLocaleString('fr-FR') }} €</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Restant</dt>
|
|
||||||
<dd>{{ rentDue.remainingAmount.toLocaleString('fr-FR') }} €</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
<div class="rent-due-labels">
|
|
||||||
<span>Règlement : {{ settlementLabel(rentDue.settlement) }}</span>
|
|
||||||
<span>Temporalité : {{ timingLabel(rentDue.timing) }}</span>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
@if (deleteError() || downloadError() || activationError()) {
|
@if (deleteError() || downloadError() || activationError()) {
|
||||||
<p class="card-error" role="alert">
|
<p class="card-error" role="alert">
|
||||||
{{ deleteError() || downloadError() || activationError() }}
|
{{ deleteError() || downloadError() || activationError() }}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
<div class="card-actions" aria-live="polite">
|
<div class="card-actions" aria-live="polite">
|
||||||
<button
|
<button mat-button type="button" [disabled]="deleting() || downloading() || activating()"
|
||||||
mat-button
|
(click)="pdfRequested.emit(lease())">
|
||||||
type="button"
|
|
||||||
[disabled]="deleting() || downloading() || activating()"
|
|
||||||
(click)="pdfRequested.emit(lease())"
|
|
||||||
>
|
|
||||||
@if (downloading()) {
|
@if (downloading()) {
|
||||||
<mat-spinner diameter="16" aria-hidden="true" /> Téléchargement…
|
<mat-spinner diameter="16" aria-hidden="true" /> Téléchargement…
|
||||||
} @else {
|
} @else {
|
||||||
@@ -81,33 +45,20 @@
|
|||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
@if (lease().id && lease().status === 'DRAFT') {
|
@if (lease().id && lease().status === 'DRAFT') {
|
||||||
<button
|
<button mat-button type="button" [disabled]="deleting() || downloading() || activating()"
|
||||||
mat-button
|
(click)="editRequested.emit(lease())">
|
||||||
type="button"
|
|
||||||
[disabled]="deleting() || downloading() || activating()"
|
|
||||||
(click)="editRequested.emit(lease())"
|
|
||||||
>
|
|
||||||
Modifier
|
Modifier
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button mat-flat-button type="button" [disabled]="deleting() || downloading() || activating()"
|
||||||
mat-flat-button
|
(click)="activationRequested.emit(lease().id!)">
|
||||||
type="button"
|
|
||||||
[disabled]="deleting() || downloading() || activating()"
|
|
||||||
(click)="activationRequested.emit(lease().id!)"
|
|
||||||
>
|
|
||||||
@if (activating()) {
|
@if (activating()) {
|
||||||
<mat-spinner diameter="16" aria-hidden="true" /> Activation…
|
<mat-spinner diameter="16" aria-hidden="true" /> Activation…
|
||||||
} @else {
|
} @else {
|
||||||
Activer
|
Activer
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button mat-button type="button" class="danger-action" [disabled]="deleting() || downloading() || activating()"
|
||||||
mat-button
|
(click)="deleteRequested.emit(lease().id!)">
|
||||||
type="button"
|
|
||||||
class="danger-action"
|
|
||||||
[disabled]="deleting() || downloading() || activating()"
|
|
||||||
(click)="deleteRequested.emit(lease().id!)"
|
|
||||||
>
|
|
||||||
@if (deleting()) {
|
@if (deleting()) {
|
||||||
<mat-spinner diameter="16" aria-hidden="true" /> Suppression…
|
<mat-spinner diameter="16" aria-hidden="true" /> Suppression…
|
||||||
} @else {
|
} @else {
|
||||||
|
|||||||
@@ -115,45 +115,4 @@ describe('LeaseCard', () => {
|
|||||||
expect(fixture.nativeElement.textContent).not.toContain('Activer');
|
expect(fixture.nativeElement.textContent).not.toContain('Activer');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the resulting rent schedule with separate settlement and timing labels', () => {
|
|
||||||
const fixture = TestBed.createComponent(LeaseCard);
|
|
||||||
fixture.componentRef.setInput('apartments', []);
|
|
||||||
fixture.componentRef.setInput('tenants', []);
|
|
||||||
fixture.componentRef.setInput('lease', {
|
|
||||||
id: 'lease-1',
|
|
||||||
apartmentId: 'apartment-1',
|
|
||||||
tenantIds: [],
|
|
||||||
guarantors: [],
|
|
||||||
startDate: '2026-01-01',
|
|
||||||
endDate: '2026-01-31',
|
|
||||||
monthlyRent: 800,
|
|
||||||
monthlyCharges: 50,
|
|
||||||
securityDeposit: 800,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
});
|
|
||||||
fixture.componentRef.setInput('rentDues', [
|
|
||||||
{
|
|
||||||
id: 'due-1',
|
|
||||||
leaseId: 'lease-1',
|
|
||||||
month: '2026-01',
|
|
||||||
dueDate: '2026-01-01',
|
|
||||||
rent: 800,
|
|
||||||
charges: 50,
|
|
||||||
total: 850,
|
|
||||||
remainingAmount: 850,
|
|
||||||
settlement: 'UNPAID',
|
|
||||||
timing: 'OVERDUE',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
const text = fixture.nativeElement.textContent.replace(/\s+/g, ' ');
|
|
||||||
const amounts = [...fixture.nativeElement.querySelectorAll('.rent-due dl div')].map((term) =>
|
|
||||||
[...term.children].map((item) => item.textContent.trim()).join(' '),
|
|
||||||
);
|
|
||||||
expect(text).toContain('2026-01');
|
|
||||||
expect(amounts).toEqual(['Loyer 800 €', 'Charges 50 €', 'Total 850 €', 'Restant 850 €']);
|
|
||||||
expect(text).toContain('Règlement : Non réglée');
|
|
||||||
expect(text).toContain('Temporalité : En retard');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -48,14 +48,4 @@ export class LeaseCard {
|
|||||||
this.lease().status
|
this.lease().status
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
settlementLabel(settlement: RentDue['settlement']): string {
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -107,71 +107,4 @@ describe('LeasesPage', () => {
|
|||||||
expect(workspace.loadRentDues).toHaveBeenCalledOnce();
|
expect(workspace.loadRentDues).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders each échéance only under its matching bail', () => {
|
|
||||||
workspace.apartmentLoadState.set({ loaded: true, loading: false, error: '' });
|
|
||||||
workspace.tenantLoadState.set({ loaded: true, loading: false, error: '' });
|
|
||||||
workspace.leaseLoadState.set({ loaded: true, loading: false, error: '' });
|
|
||||||
workspace.rentDueLoadState.set({ loaded: true, loading: false, error: '' });
|
|
||||||
workspace.leases.set([
|
|
||||||
{
|
|
||||||
id: 'lease-1',
|
|
||||||
apartmentId: 'apartment-1',
|
|
||||||
tenantIds: [],
|
|
||||||
guarantors: [],
|
|
||||||
startDate: '2026-01-01',
|
|
||||||
endDate: '2026-12-31',
|
|
||||||
monthlyRent: 800,
|
|
||||||
monthlyCharges: 50,
|
|
||||||
securityDeposit: 800,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'lease-2',
|
|
||||||
apartmentId: 'apartment-2',
|
|
||||||
tenantIds: [],
|
|
||||||
guarantors: [],
|
|
||||||
startDate: '2027-01-01',
|
|
||||||
endDate: '2027-12-31',
|
|
||||||
monthlyRent: 900,
|
|
||||||
monthlyCharges: 60,
|
|
||||||
securityDeposit: 900,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
workspace.rentDues.set([
|
|
||||||
{
|
|
||||||
id: 'due-1',
|
|
||||||
leaseId: 'lease-1',
|
|
||||||
month: '2026-01',
|
|
||||||
dueDate: '2026-01-01',
|
|
||||||
rent: 800,
|
|
||||||
charges: 50,
|
|
||||||
total: 850,
|
|
||||||
remainingAmount: 850,
|
|
||||||
settlement: 'UNPAID',
|
|
||||||
timing: 'OVERDUE',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'due-2',
|
|
||||||
leaseId: 'lease-2',
|
|
||||||
month: '2027-01',
|
|
||||||
dueDate: '2027-01-01',
|
|
||||||
rent: 900,
|
|
||||||
charges: 60,
|
|
||||||
total: 960,
|
|
||||||
remainingAmount: 960,
|
|
||||||
settlement: 'UNPAID',
|
|
||||||
timing: 'UPCOMING',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const fixture = TestBed.createComponent(LeasesPage);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
const cards = fixture.nativeElement.querySelectorAll('.lease-card');
|
|
||||||
expect(cards).toHaveLength(2);
|
|
||||||
expect(cards[0].textContent).toContain('2026-01');
|
|
||||||
expect(cards[0].textContent).not.toContain('2027-01');
|
|
||||||
expect(cards[1].textContent).toContain('2027-01');
|
|
||||||
expect(cards[1].textContent).not.toContain('2026-01');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user