Align core lease and amendment validation (#49)
This commit is contained in:
1 parent
96bd02e3a6
commit
ca3e28d3a2
20 files changed
+1361
-56
No files matched your search
@@ -0,0 +1,165 @@
|
||||
<section class="form-page">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="back-action"
|
||||
[disabled]="saving()"
|
||||
(click)="cancelled.emit()"
|
||||
>
|
||||
← Retour
|
||||
</button>
|
||||
<p class="eyebrow">Bail actif</p>
|
||||
<h1>Préparer un avenant</h1>
|
||||
<p>
|
||||
Seuls les locataires, garants, terme et conditions de paiement peuvent être
|
||||
modifiés.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<form
|
||||
class="entity-form"
|
||||
[formRoot]="amendmentForm"
|
||||
(ngSubmit)="save()"
|
||||
[attr.aria-busy]="saving()"
|
||||
>
|
||||
@if (showSaveError()) {
|
||||
<div class="alert-error" role="alert">{{ saveError() }}</div>
|
||||
}
|
||||
<fieldset class="busy-fieldset" [disabled]="saving()" [attr.inert]="saving() ? '' : null">
|
||||
<div class="form-grid">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Prise d’effet</mat-label>
|
||||
<input
|
||||
matInput
|
||||
id="effectiveDate"
|
||||
type="date"
|
||||
[formField]="amendmentForm.effectiveDate"
|
||||
(change)="fieldWasChanged('effectiveDate')"
|
||||
/>
|
||||
@if (fieldError('effectiveDate')) {
|
||||
<mat-error>{{ fieldError('effectiveDate') }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Locataires</mat-label>
|
||||
<mat-select
|
||||
id="tenantIds"
|
||||
[formField]="amendmentForm.tenantIds"
|
||||
multiple
|
||||
(selectionChange)="fieldWasChanged('tenantIds')"
|
||||
>
|
||||
@for (tenant of tenants(); track tenant.id) {
|
||||
<mat-option [value]="tenant.id"
|
||||
>{{ tenant.firstName }} {{ tenant.lastName }}</mat-option
|
||||
>
|
||||
}
|
||||
</mat-select>
|
||||
@if (fieldError('tenantIds')) {
|
||||
<mat-error>{{ fieldError('tenantIds') }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fin de bail</mat-label>
|
||||
<input
|
||||
matInput
|
||||
id="endDate"
|
||||
type="date"
|
||||
[formField]="amendmentForm.endDate"
|
||||
(change)="fieldWasChanged('endDate')"
|
||||
/>
|
||||
@if (fieldError('endDate')) {
|
||||
<mat-error>{{ fieldError('endDate') }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Loyer mensuel</mat-label>
|
||||
<input
|
||||
matInput
|
||||
id="monthlyRent"
|
||||
type="number"
|
||||
step="0.01"
|
||||
[formField]="amendmentForm.monthlyRent"
|
||||
(change)="fieldWasChanged('monthlyRent')"
|
||||
/>
|
||||
<span matTextSuffix>€</span>
|
||||
@if (fieldError('monthlyRent')) {
|
||||
<mat-error>{{ fieldError('monthlyRent') }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Charges mensuelles</mat-label>
|
||||
<input
|
||||
matInput
|
||||
id="monthlyCharges"
|
||||
type="number"
|
||||
step="0.01"
|
||||
[formField]="amendmentForm.monthlyCharges"
|
||||
(change)="fieldWasChanged('monthlyCharges')"
|
||||
/>
|
||||
<span matTextSuffix>€</span>
|
||||
@if (fieldError('monthlyCharges')) {
|
||||
<mat-error>{{ fieldError('monthlyCharges') }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Mode de récupération des charges</mat-label>
|
||||
<mat-select
|
||||
id="chargeMethod"
|
||||
[formField]="amendmentForm.chargeMethod"
|
||||
(selectionChange)="fieldWasChanged('chargeMethod')"
|
||||
>
|
||||
<mat-option value="FLAT_RATE">Forfait de charges</mat-option>
|
||||
<mat-option value="PROVISION"
|
||||
>Provision avec régularisation annuelle</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Jour d’échéance</mat-label>
|
||||
<input
|
||||
matInput
|
||||
id="dueDay"
|
||||
type="number"
|
||||
step="1"
|
||||
[formField]="amendmentForm.dueDay"
|
||||
(change)="fieldWasChanged('dueDay')"
|
||||
/>
|
||||
@if (fieldError('dueDay')) {
|
||||
<mat-error>{{ fieldError('dueDay') }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@if (amendmentModel().chargeMethod === 'FLAT_RATE') {
|
||||
<mat-checkbox id="indexFlatRate" [formField]="amendmentForm.indexFlatRate"
|
||||
>Indexer le forfait de charges avec le loyer</mat-checkbox
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<section class="guarantor-section">
|
||||
<h2>Garants</h2>
|
||||
@for (guarantor of guarantors(); track $index) {
|
||||
<div class="guarantor-chip">
|
||||
<span>{{ guarantor.firstName }} {{ guarantor.lastName }}</span>
|
||||
<button mat-button type="button" (click)="removeGuarantor($index)">
|
||||
Retirer
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<app-guarantor-form (submitted)="addGuarantor($event)" />
|
||||
</section>
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" [disabled]="saving()" (click)="cancelled.emit()">
|
||||
Annuler
|
||||
</button>
|
||||
<button mat-flat-button type="submit" class="primary-action" [disabled]="saving()">
|
||||
@if (saving()) {
|
||||
<mat-spinner diameter="18" aria-hidden="true" /> Enregistrement…
|
||||
} @else {
|
||||
Enregistrer l’avenant
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
@@ -0,0 +1,182 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { emptyLeaseLegalTerms } from '../../services/lease.service';
|
||||
import { LeaseAmendmentForm } from './lease-amendment-form';
|
||||
|
||||
describe('LeaseAmendmentForm', () => {
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it('submits only mutable contract facts', () => {
|
||||
const fixture = TestBed.createComponent(LeaseAmendmentForm);
|
||||
fixture.componentRef.setInput('tenants', [
|
||||
{
|
||||
id: 'tenant-1',
|
||||
firstName: 'Camille',
|
||||
lastName: 'Martin',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
},
|
||||
]);
|
||||
fixture.componentRef.setInput('lease', {
|
||||
id: 'lease-1',
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
guarantors: [],
|
||||
specialClauses: [],
|
||||
legalTerms: emptyLeaseLegalTerms(),
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-09-01',
|
||||
endDate: '2027-08-31',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: false,
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
fixture.componentInstance.amendmentModel.update((model) => ({
|
||||
...model,
|
||||
effectiveDate: '2026-10-01',
|
||||
monthlyRent: 0.01,
|
||||
monthlyCharges: 0,
|
||||
dueDay: 28,
|
||||
}));
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.save();
|
||||
|
||||
expect(submitted).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
effectiveDate: '2026-10-01',
|
||||
tenantIds: ['tenant-1'],
|
||||
endDate: '2027-08-31',
|
||||
monthlyRent: 0.01,
|
||||
monthlyCharges: 0,
|
||||
dueDay: 28,
|
||||
}),
|
||||
);
|
||||
expect(submitted.mock.calls[0][0]).not.toHaveProperty('securityDeposit');
|
||||
expect(submitted.mock.calls[0][0]).not.toHaveProperty('apartmentId');
|
||||
});
|
||||
|
||||
it('blocks a date before the amendment takes effect', () => {
|
||||
const fixture = TestBed.createComponent(LeaseAmendmentForm);
|
||||
fixture.componentRef.setInput('tenants', []);
|
||||
fixture.componentRef.setInput('lease', {
|
||||
id: 'lease-1',
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
guarantors: [],
|
||||
specialClauses: [],
|
||||
legalTerms: emptyLeaseLegalTerms(),
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-09-01',
|
||||
endDate: '2027-08-31',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: false,
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
fixture.componentInstance.amendmentModel.update((model) => ({
|
||||
...model,
|
||||
effectiveDate: '2026-10-01',
|
||||
endDate: '2026-09-30',
|
||||
}));
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.save();
|
||||
|
||||
expect(submitted).not.toHaveBeenCalled();
|
||||
expect(fixture.componentInstance.fieldError('endDate')).toContain('ne peut pas précéder');
|
||||
});
|
||||
|
||||
it('blocks a past or out-of-period effective date and focuses it', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-07-24T12:00:00'));
|
||||
const fixture = TestBed.createComponent(LeaseAmendmentForm);
|
||||
fixture.componentRef.setInput('tenants', []);
|
||||
fixture.componentRef.setInput('lease', {
|
||||
id: 'lease-1',
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
guarantors: [],
|
||||
specialClauses: [],
|
||||
legalTerms: emptyLeaseLegalTerms(),
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-07-01',
|
||||
endDate: '2027-06-30',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: false,
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
fixture.componentInstance.amendmentModel.update((model) => ({
|
||||
...model,
|
||||
effectiveDate: '2026-07-23',
|
||||
endDate: '2027-06-30',
|
||||
}));
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.save();
|
||||
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
||||
expect(fixture.componentInstance.fieldError('effectiveDate')).toContain(
|
||||
'entre aujourd’hui',
|
||||
);
|
||||
expect(document.activeElement?.id).toBe('effectiveDate');
|
||||
|
||||
fixture.componentInstance.amendmentModel.update((model) => ({
|
||||
...model,
|
||||
effectiveDate: '2027-07-01',
|
||||
}));
|
||||
expect(fixture.componentInstance.fieldError('effectiveDate')).toContain('fin du bail');
|
||||
});
|
||||
|
||||
it('clears virtual tenant and end-date error paths with their visible controls', () => {
|
||||
const fixture = TestBed.createComponent(LeaseAmendmentForm);
|
||||
fixture.componentRef.setInput('tenants', []);
|
||||
fixture.componentRef.setInput('lease', {
|
||||
id: 'lease-1',
|
||||
apartmentId: 'apartment-1',
|
||||
tenantIds: ['tenant-1'],
|
||||
guarantors: [],
|
||||
specialClauses: [],
|
||||
legalTerms: emptyLeaseLegalTerms(),
|
||||
leaseType: 'STANDARD',
|
||||
startDate: '2026-09-01',
|
||||
endDate: '2027-08-31',
|
||||
monthlyRent: 850,
|
||||
monthlyCharges: 50,
|
||||
securityDeposit: 850,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE',
|
||||
indexFlatRate: false,
|
||||
status: 'ACTIVE',
|
||||
});
|
||||
fixture.componentRef.setInput('saveFieldErrors', [
|
||||
{ field: 'tenantIdsDistinct', code: 'assert_true' },
|
||||
{ field: 'endDateOnOrAfterEffectiveDate', code: 'assert_true' },
|
||||
]);
|
||||
const changed = vi.fn();
|
||||
fixture.componentInstance.fieldChanged.subscribe(changed);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.fieldError('tenantIds')).toContain('sans doublon');
|
||||
expect(fixture.componentInstance.fieldError('endDate')).toContain('ne peut pas précéder');
|
||||
fixture.componentInstance.fieldWasChanged('tenantIds');
|
||||
fixture.componentInstance.fieldWasChanged('endDate');
|
||||
expect(changed).toHaveBeenCalledWith('tenantIdsDistinct');
|
||||
expect(changed).toHaveBeenCalledWith('endDateOnOrAfterEffectiveDate');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,265 @@
|
||||
import { Component, effect, ElementRef, inject, input, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormField, FormRoot, form, max, min, required, validate } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { ChargeMethod, Guarantor, Lease, LeaseAmendmentInput } from '../../services/lease.service';
|
||||
import { FieldValidationError } from '../../services/http-error-message';
|
||||
import { Tenant } from '../../services/tenant.service';
|
||||
import { GuarantorForm } from '../guarantor-form/guarantor-form';
|
||||
|
||||
type AmendmentField =
|
||||
'effectiveDate' | 'tenantIds' | 'endDate' | 'monthlyRent' | 'monthlyCharges' | 'dueDay';
|
||||
|
||||
const serverErrorCodes: Record<AmendmentField, ReadonlySet<string>> = {
|
||||
effectiveDate: new Set(['not_null', 'not_past']),
|
||||
tenantIds: new Set(['not_empty', 'assert_true']),
|
||||
endDate: new Set(['not_null', 'assert_true']),
|
||||
monthlyRent: new Set(['positive']),
|
||||
monthlyCharges: new Set(['positive_or_zero']),
|
||||
dueDay: new Set(['min', 'max']),
|
||||
};
|
||||
|
||||
const amendmentFields: AmendmentField[] = [
|
||||
'effectiveDate',
|
||||
'tenantIds',
|
||||
'endDate',
|
||||
'monthlyRent',
|
||||
'monthlyCharges',
|
||||
'dueDay',
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'app-lease-amendment-form',
|
||||
imports: [
|
||||
FormsModule,
|
||||
FormRoot,
|
||||
FormField,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatSelectModule,
|
||||
GuarantorForm,
|
||||
],
|
||||
templateUrl: './lease-amendment-form.html',
|
||||
})
|
||||
export class LeaseAmendmentForm {
|
||||
private readonly element = inject<ElementRef<HTMLElement>>(ElementRef);
|
||||
readonly lease = input.required<Lease>();
|
||||
readonly tenants = input.required<Tenant[]>();
|
||||
readonly saving = input(false);
|
||||
readonly saveError = input('');
|
||||
readonly saveFieldErrors = input<FieldValidationError[]>([]);
|
||||
readonly cancelled = output<void>();
|
||||
readonly submitted = output<LeaseAmendmentInput>();
|
||||
readonly fieldChanged = output<string>();
|
||||
private previousServerErrorSignature = '';
|
||||
|
||||
readonly amendmentModel = signal({
|
||||
effectiveDate: '',
|
||||
tenantIds: [] as string[],
|
||||
endDate: '',
|
||||
monthlyRent: 0,
|
||||
monthlyCharges: 0,
|
||||
dueDay: 5,
|
||||
chargeMethod: 'FLAT_RATE' as ChargeMethod,
|
||||
indexFlatRate: false,
|
||||
});
|
||||
readonly guarantors = signal<Guarantor[]>([]);
|
||||
readonly amendmentForm = form(this.amendmentModel, (f) => {
|
||||
required(f.effectiveDate);
|
||||
validate(f.effectiveDate, ({ value, valueOf }) => {
|
||||
const effectiveDate = value();
|
||||
const endDate = valueOf(f.endDate);
|
||||
return !effectiveDate ||
|
||||
(effectiveDate >= browserToday() &&
|
||||
effectiveDate >= this.lease().startDate &&
|
||||
effectiveDate <= this.lease().endDate &&
|
||||
(!endDate || effectiveDate <= endDate))
|
||||
? null
|
||||
: {
|
||||
kind: 'amendment-effective-date',
|
||||
message:
|
||||
'La prise d’effet doit être comprise entre aujourd’hui et la fin du bail, sans dépasser sa nouvelle fin.',
|
||||
};
|
||||
});
|
||||
required(f.tenantIds);
|
||||
required(f.endDate);
|
||||
validate(f.endDate, ({ value, valueOf }) => {
|
||||
const endDate = value();
|
||||
const effectiveDate = valueOf(f.effectiveDate);
|
||||
return !endDate ||
|
||||
(endDate >= this.lease().startDate && (!effectiveDate || endDate >= effectiveDate))
|
||||
? null
|
||||
: {
|
||||
kind: 'amendment-end-date',
|
||||
message:
|
||||
'La fin du bail ne peut pas précéder sa prise d’effet ni son début.',
|
||||
};
|
||||
});
|
||||
required(f.monthlyRent);
|
||||
min(f.monthlyRent, Number.MIN_VALUE);
|
||||
required(f.monthlyCharges);
|
||||
min(f.monthlyCharges, 0);
|
||||
required(f.dueDay);
|
||||
min(f.dueDay, 1);
|
||||
max(f.dueDay, 28);
|
||||
validate(f.dueDay, ({ value }) =>
|
||||
Number.isInteger(value())
|
||||
? null
|
||||
: { kind: 'whole-day', message: 'Le jour d’échéance doit être un nombre entier.' },
|
||||
);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => this.reset(this.lease()));
|
||||
effect(() => {
|
||||
const errors = this.saveFieldErrors();
|
||||
const signature = errors
|
||||
.map((error) => `${error.field}:${error.code}`)
|
||||
.sort()
|
||||
.join('|');
|
||||
if (signature && signature !== this.previousServerErrorSignature) {
|
||||
queueMicrotask(() => this.focusFirstInvalid());
|
||||
}
|
||||
this.previousServerErrorSignature = signature;
|
||||
});
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (this.saving()) return;
|
||||
this.amendmentForm().markAsTouched();
|
||||
if (this.amendmentForm().invalid()) {
|
||||
queueMicrotask(() => this.focusFirstInvalid());
|
||||
return;
|
||||
}
|
||||
const model = this.amendmentModel();
|
||||
this.submitted.emit({
|
||||
...model,
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Paris',
|
||||
guarantors: this.guarantors(),
|
||||
indexFlatRate: model.chargeMethod === 'FLAT_RATE' && model.indexFlatRate,
|
||||
});
|
||||
}
|
||||
|
||||
addGuarantor(guarantor: Guarantor): void {
|
||||
this.guarantors.update((guarantors) => [...guarantors, guarantor]);
|
||||
}
|
||||
|
||||
removeGuarantor(index: number): void {
|
||||
this.guarantors.update((guarantors) => guarantors.filter((_, item) => item !== index));
|
||||
}
|
||||
|
||||
fieldWasChanged(field: string): void {
|
||||
this.fieldChanged.emit(field);
|
||||
if (field === 'tenantIds') this.fieldChanged.emit('tenantIdsDistinct');
|
||||
if (field === 'endDate') this.fieldChanged.emit('endDateOnOrAfterEffectiveDate');
|
||||
}
|
||||
|
||||
showSaveError(): boolean {
|
||||
return (
|
||||
Boolean(this.saveError()) &&
|
||||
(this.saveFieldErrors().length === 0 ||
|
||||
this.saveFieldErrors().some((error) => !this.isInlineServerError(error)))
|
||||
);
|
||||
}
|
||||
|
||||
fieldError(field: AmendmentField): string {
|
||||
const model = this.amendmentModel();
|
||||
const invalid =
|
||||
(field === 'effectiveDate' &&
|
||||
(!model.effectiveDate ||
|
||||
model.effectiveDate < browserToday() ||
|
||||
model.effectiveDate < this.lease().startDate ||
|
||||
model.effectiveDate > this.lease().endDate ||
|
||||
(model.endDate !== '' && model.effectiveDate > model.endDate))) ||
|
||||
(field === 'tenantIds' && !model.tenantIds.length) ||
|
||||
(field === 'endDate' &&
|
||||
(!model.endDate ||
|
||||
model.endDate < this.lease().startDate ||
|
||||
(model.effectiveDate !== '' && model.endDate < model.effectiveDate))) ||
|
||||
(field === 'monthlyRent' && model.monthlyRent <= 0) ||
|
||||
(field === 'monthlyCharges' && model.monthlyCharges < 0) ||
|
||||
(field === 'dueDay' &&
|
||||
(!Number.isInteger(model.dueDay) || model.dueDay < 1 || model.dueDay > 28));
|
||||
if ((!invalid || !this.isTouched(field)) && !this.hasKnownServerError(field)) {
|
||||
return '';
|
||||
}
|
||||
return {
|
||||
effectiveDate:
|
||||
'La prise d’effet doit être comprise entre aujourd’hui et la fin du bail, sans dépasser sa nouvelle fin.',
|
||||
tenantIds: 'Sélectionnez au moins un locataire, sans doublon.',
|
||||
endDate: 'La fin du bail ne peut pas précéder sa prise d’effet ni son début.',
|
||||
monthlyRent: 'Le loyer mensuel doit être strictement positif.',
|
||||
monthlyCharges: 'Les charges mensuelles doivent être positives ou nulles.',
|
||||
dueDay: 'Le jour d’échéance doit être un entier entre 1 et 28.',
|
||||
}[field];
|
||||
}
|
||||
|
||||
private hasKnownServerError(field: AmendmentField): boolean {
|
||||
return this.saveFieldErrors().some(
|
||||
(error) => matchesField(field, error.field) && serverErrorCodes[field].has(error.code),
|
||||
);
|
||||
}
|
||||
|
||||
private isTouched(field: AmendmentField): boolean {
|
||||
switch (field) {
|
||||
case 'effectiveDate':
|
||||
return this.amendmentForm.effectiveDate().touched();
|
||||
case 'tenantIds':
|
||||
return this.amendmentForm.tenantIds().touched();
|
||||
case 'endDate':
|
||||
return this.amendmentForm.endDate().touched();
|
||||
case 'monthlyRent':
|
||||
return this.amendmentForm.monthlyRent().touched();
|
||||
case 'monthlyCharges':
|
||||
return this.amendmentForm.monthlyCharges().touched();
|
||||
case 'dueDay':
|
||||
return this.amendmentForm.dueDay().touched();
|
||||
}
|
||||
}
|
||||
|
||||
private isInlineServerError(error: FieldValidationError): boolean {
|
||||
return amendmentFields.some(
|
||||
(field) => matchesField(field, error.field) && serverErrorCodes[field].has(error.code),
|
||||
);
|
||||
}
|
||||
|
||||
private focusFirstInvalid(): void {
|
||||
const field = amendmentFields.find((item) => this.fieldError(item));
|
||||
this.element.nativeElement.querySelector<HTMLElement>(`#${field ?? ''}`)?.focus();
|
||||
}
|
||||
|
||||
private reset(lease: Lease): void {
|
||||
this.amendmentModel.set({
|
||||
effectiveDate: '',
|
||||
tenantIds: lease.tenantIds,
|
||||
endDate: lease.endDate,
|
||||
monthlyRent: lease.monthlyRent,
|
||||
monthlyCharges: lease.monthlyCharges,
|
||||
dueDay: lease.dueDay,
|
||||
chargeMethod: lease.chargeMethod,
|
||||
indexFlatRate: lease.indexFlatRate,
|
||||
});
|
||||
this.guarantors.set(lease.guarantors);
|
||||
}
|
||||
}
|
||||
|
||||
function matchesField(field: AmendmentField, path: string): boolean {
|
||||
return (
|
||||
path === field ||
|
||||
(field === 'tenantIds' && path === 'tenantIdsDistinct') ||
|
||||
(field === 'endDate' && path === 'endDateOnOrAfterEffectiveDate')
|
||||
);
|
||||
}
|
||||
|
||||
function browserToday(): string {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
Reference in new issue
Block a user