refactor: simplify lease card and rent payments page logic
This commit is contained in:
1 parent
a039243d03
commit
6ce0719a20
4 files changed
+68
-235
No files matched your search
@@ -1,119 +1,70 @@
|
||||
<article class="lease-card" [attr.aria-busy]="deleting() || downloading() || activating()">
|
||||
<div class="lease-topline">
|
||||
<span class="status-dot"></span><span>{{ statusLabel() }}</span
|
||||
><strong>{{ lease().monthlyRent.toLocaleString('fr-FR') }} €</strong>
|
||||
</div>
|
||||
<h2>{{ apartmentName() }}</h2>
|
||||
<p class="tenant-name">{{ tenantNames() }}</p>
|
||||
<dl class="terms">
|
||||
<div>
|
||||
<dt>Période</dt>
|
||||
<dd>
|
||||
{{ lease().startDate | date: 'dd MMMM yyyy' }} →
|
||||
{{ lease().endDate | date: 'dd MMMM yyyy' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Charges</dt>
|
||||
<dd>{{ lease().monthlyCharges.toLocaleString('fr-FR') }} € / mois</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Dépôt</dt>
|
||||
<dd>{{ lease().securityDeposit.toLocaleString('fr-FR') }} €</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@if (lease().guarantors.length) {
|
||||
<div class="guarantor-summary">
|
||||
<span>Garant</span>
|
||||
@for (guarantor of lease().guarantors; track $index) {
|
||||
<strong>{{ guarantor.firstName }} {{ guarantor.lastName }}</strong>
|
||||
}
|
||||
</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()) {
|
||||
<p class="card-error" role="alert">
|
||||
{{ deleteError() || downloadError() || activationError() }}
|
||||
</p>
|
||||
}
|
||||
<div class="card-actions" aria-live="polite">
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
[disabled]="deleting() || downloading() || activating()"
|
||||
(click)="pdfRequested.emit(lease())"
|
||||
>
|
||||
@if (downloading()) {
|
||||
<mat-spinner diameter="16" aria-hidden="true" /> Téléchargement…
|
||||
} @else {
|
||||
Télécharger le bail
|
||||
}
|
||||
</button>
|
||||
@if (lease().id && lease().status === 'DRAFT') {
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
[disabled]="deleting() || downloading() || activating()"
|
||||
(click)="editRequested.emit(lease())"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
type="button"
|
||||
[disabled]="deleting() || downloading() || activating()"
|
||||
(click)="activationRequested.emit(lease().id!)"
|
||||
>
|
||||
@if (activating()) {
|
||||
<mat-spinner diameter="16" aria-hidden="true" /> Activation…
|
||||
} @else {
|
||||
Activer
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="danger-action"
|
||||
[disabled]="deleting() || downloading() || activating()"
|
||||
(click)="deleteRequested.emit(lease().id!)"
|
||||
>
|
||||
@if (deleting()) {
|
||||
<mat-spinner diameter="16" aria-hidden="true" /> Suppression…
|
||||
} @else {
|
||||
Supprimer
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="lease-topline">
|
||||
<span class="status-dot"></span><span>{{ statusLabel() }}</span><strong>{{
|
||||
lease().monthlyRent.toLocaleString('fr-FR') }} €</strong>
|
||||
</div>
|
||||
<h2>{{ apartmentName() }}</h2>
|
||||
<p class="tenant-name">{{ tenantNames() }}</p>
|
||||
<dl class="terms">
|
||||
<div>
|
||||
<dt>Période</dt>
|
||||
<dd>
|
||||
{{ lease().startDate | date: 'dd MMMM yyyy' }} →
|
||||
{{ lease().endDate | date: 'dd MMMM yyyy' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Charges</dt>
|
||||
<dd>{{ lease().monthlyCharges.toLocaleString('fr-FR') }} € / mois</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Dépôt</dt>
|
||||
<dd>{{ lease().securityDeposit.toLocaleString('fr-FR') }} €</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@if (lease().guarantors.length) {
|
||||
<div class="guarantor-summary">
|
||||
<span>Garant</span>
|
||||
@for (guarantor of lease().guarantors; track $index) {
|
||||
<strong>{{ guarantor.firstName }} {{ guarantor.lastName }}</strong>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (deleteError() || downloadError() || activationError()) {
|
||||
<p class="card-error" role="alert">
|
||||
{{ deleteError() || downloadError() || activationError() }}
|
||||
</p>
|
||||
}
|
||||
<div class="card-actions" aria-live="polite">
|
||||
<button mat-button type="button" [disabled]="deleting() || downloading() || activating()"
|
||||
(click)="pdfRequested.emit(lease())">
|
||||
@if (downloading()) {
|
||||
<mat-spinner diameter="16" aria-hidden="true" /> Téléchargement…
|
||||
} @else {
|
||||
Télécharger le bail
|
||||
}
|
||||
</button>
|
||||
@if (lease().id && lease().status === 'DRAFT') {
|
||||
<button mat-button type="button" [disabled]="deleting() || downloading() || activating()"
|
||||
(click)="editRequested.emit(lease())">
|
||||
Modifier
|
||||
</button>
|
||||
<button mat-flat-button type="button" [disabled]="deleting() || downloading() || activating()"
|
||||
(click)="activationRequested.emit(lease().id!)">
|
||||
@if (activating()) {
|
||||
<mat-spinner diameter="16" aria-hidden="true" /> Activation…
|
||||
} @else {
|
||||
Activer
|
||||
}
|
||||
</button>
|
||||
<button mat-button type="button" class="danger-action" [disabled]="deleting() || downloading() || activating()"
|
||||
(click)="deleteRequested.emit(lease().id!)">
|
||||
@if (deleting()) {
|
||||
<mat-spinner diameter="16" aria-hidden="true" /> Suppression…
|
||||
} @else {
|
||||
Supprimer
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
@@ -115,45 +115,4 @@ describe('LeaseCard', () => {
|
||||
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
|
||||
];
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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