@for (parameter of definition.parameters; track parameter.name) {
-
+
{{ parameter.label }}
@switch (parameter.type) {
@case ('CHOICE') {
@@ -127,6 +138,11 @@
/>
}
}
+ @if (parameterErrorFor(definition.id, parameter)) {
+ {{
+ parameterErrorFor(definition.id, parameter)
+ }}
+ }
}
@@ -150,7 +166,10 @@
@for (item of customClauses(); track item.index) {
-
+
Clause personnalisée {{ $index + 1 }}
+ @if (customClauseError(item.index, item.clause)) {
+ {{ customClauseError(item.index, item.clause) }}
+ }
Retirer
diff --git a/frontend/src/app/components/lease-clause-editor/lease-clause-editor.spec.ts b/frontend/src/app/components/lease-clause-editor/lease-clause-editor.spec.ts
index 1adbd25..e4405eb 100644
--- a/frontend/src/app/components/lease-clause-editor/lease-clause-editor.spec.ts
+++ b/frontend/src/app/components/lease-clause-editor/lease-clause-editor.spec.ts
@@ -130,4 +130,22 @@ describe('LeaseClauseEditor', () => {
expect(getLeaseClauses).toHaveBeenNthCalledWith(1, 'STANDARD');
expect(getLeaseClauses).toHaveBeenNthCalledWith(2, 'COLOCATION');
});
+
+ it('clears only the corrected indexed parameter error', () => {
+ const fixture = createEditor();
+ const editor = fixture.componentInstance;
+ const changed = vi.fn();
+ editor.fieldChanged.subscribe(changed);
+ editor.toggleCatalog('VISITS', true);
+ fixture.componentRef.setInput('saveFieldErrors', [
+ { field: 'specialClauses[0].parameters.startTime', code: 'required' },
+ { field: 'specialClauses[0].parameters.day', code: 'invalid' },
+ ]);
+
+ editor.setParameter('VISITS', 'startTime', '18:00');
+
+ expect(changed).toHaveBeenCalledWith('specialClauses[0].parameters.startTime');
+ expect(changed).not.toHaveBeenCalledWith('specialClauses[0].parameters.day');
+ expect(editor.parameterErrorFor('VISITS', definitions[0].parameters[3])).toContain('valide');
+ });
});
diff --git a/frontend/src/app/components/lease-clause-editor/lease-clause-editor.ts b/frontend/src/app/components/lease-clause-editor/lease-clause-editor.ts
index fa5585e..6f88d12 100644
--- a/frontend/src/app/components/lease-clause-editor/lease-clause-editor.ts
+++ b/frontend/src/app/components/lease-clause-editor/lease-clause-editor.ts
@@ -1,6 +1,7 @@
import {
Component,
DestroyRef,
+ ElementRef,
computed,
effect,
inject,
@@ -21,6 +22,7 @@ import {
LeaseService,
LeaseSpecialClause,
} from '../../services/lease.service';
+import { FieldValidationError } from '../../services/http-error-message';
@Component({
selector: 'app-lease-clause-editor',
@@ -38,11 +40,14 @@ import {
export class LeaseClauseEditor {
private readonly leaseService = inject(LeaseService);
private readonly destroyRef = inject(DestroyRef);
+ private readonly element = inject
>(ElementRef);
private requestId = 0;
readonly context = input.required();
readonly specialClauses = input([]);
readonly specialClausesChange = output();
+ readonly fieldChanged = output();
+ readonly saveFieldErrors = input([]);
readonly definitions = signal([]);
readonly loading = signal(true);
@@ -79,6 +84,10 @@ export class LeaseClauseEditor {
const definition = this.definitions().find(({ id }) => id === clause.catalogId);
if (!definition)
return `La clause « ${clause.catalogId} » n'est plus disponible. Retirez-la.`;
+ const invalid = definition.parameters.find((parameter) =>
+ Boolean(this.parameterError(clause, parameter, true)),
+ );
+ if (invalid) return 'Corrigez « ' + invalid.label + ' ».';
const missing = definition.parameters.find(
(parameter) => parameter.required && !clause.parameters[parameter.name]?.trim(),
);
@@ -104,15 +113,25 @@ export class LeaseClauseEditor {
return !this.validationMessage();
}
+ focusInvalid(): boolean {
+ const control = this.element.nativeElement.querySelector(
+ '[data-clause-invalid=true] input, [data-clause-invalid=true] textarea, [data-clause-invalid=true], .validation-error',
+ );
+ control?.focus();
+ return control !== null;
+ }
+
isSelected(id: string): boolean {
return this.specialClauses().some(({ catalogId }) => catalogId === id);
}
toggleCatalog(id: string, selected: boolean): void {
+ const index = this.clauseIndex(id);
const clauses = this.specialClauses().filter(({ catalogId }) => catalogId !== id);
this.emit(
selected ? [...clauses, { catalogId: id, parameters: {}, customText: '' }] : clauses,
);
+ this.fieldChanged.emit('specialClauses[' + index + '].catalogId');
}
parameterValue(catalogId: string, parameterName: string): string {
@@ -131,6 +150,9 @@ export class LeaseClauseEditor {
: clause,
),
);
+ this.fieldChanged.emit(
+ 'specialClauses[' + this.clauseIndex(catalogId) + '].parameters.' + parameterName,
+ );
}
setParameterFromEvent(catalogId: string, parameterName: string, event: Event): void {
@@ -147,6 +169,7 @@ export class LeaseClauseEditor {
clauseIndex === index ? { ...clause, customText } : clause,
),
);
+ this.fieldChanged.emit('specialClauses[' + index + '].customText');
}
updateCustomClauseFromEvent(index: number, event: Event): void {
@@ -180,10 +203,80 @@ export class LeaseClauseEditor {
}
private emit(clauses: LeaseSpecialClause[]): void {
- this.showValidationError.set(false);
this.specialClausesChange.emit(clauses);
}
+ parameterError(
+ clause: LeaseSpecialClause,
+ parameter: LeaseClauseDefinition['parameters'][number],
+ show = this.showValidationError(),
+ ): string {
+ const path =
+ 'specialClauses[' + this.clauseIndex(clause.catalogId) + '].parameters.' + parameter.name;
+ if (this.saveFieldErrors().some((error) => error.field === path)) {
+ return this.parameterMessage(parameter);
+ }
+ if (!show) return '';
+ const value = clause.parameters[parameter.name]?.trim() ?? '';
+ if (parameter.required && !value) return this.parameterMessage(parameter, true);
+ return value && !this.parameterValid(parameter, value) ? this.parameterMessage(parameter) : '';
+ }
+
+ parameterErrorFor(
+ catalogId: string,
+ parameter: LeaseClauseDefinition['parameters'][number],
+ ): string {
+ const clause = this.specialClauses().find((item) => item.catalogId === catalogId);
+ return clause ? this.parameterError(clause, parameter) : '';
+ }
+
+ catalogError(catalogId: string): string {
+ const path = 'specialClauses[' + this.clauseIndex(catalogId) + '].catalogId';
+ return this.saveFieldErrors().some((error) => error.field === path)
+ ? 'Cette clause n’est pas disponible pour ce bail.'
+ : '';
+ }
+
+ customClauseError(index: number, clause: LeaseSpecialClause): string {
+ const serverError = this.saveFieldErrors().some(
+ (error) => error.field === 'specialClauses[' + index + '].customText',
+ );
+ if (serverError || (this.showValidationError() && !clause.customText.trim())) {
+ return 'Renseignez le texte de la clause personnalisée ou retirez-la.';
+ }
+ return '';
+ }
+
+ private parameterMessage(
+ parameter: LeaseClauseDefinition['parameters'][number],
+ required = false,
+ ): string {
+ if (required) return 'Renseignez « ' + parameter.label + ' ».';
+ return parameter.type === 'CHOICE'
+ ? 'Sélectionnez une valeur proposée pour « ' + parameter.label + ' ».'
+ : 'Renseignez une valeur valide pour « ' + parameter.label + ' ».';
+ }
+
+ private parameterValid(
+ parameter: LeaseClauseDefinition['parameters'][number],
+ value: string,
+ ): boolean {
+ if (parameter.type === 'TEXT' || parameter.type === 'TIME') return true;
+ if (parameter.type === 'CHOICE') return parameter.choices.some((choice) => choice.value === value);
+ if (parameter.type === 'NUMBER') return Number.isFinite(Number(value));
+ const [year, month, day] = value.split('-').map(Number);
+ const date = new Date(Date.UTC(year, month - 1, day));
+ return (
+ date.getUTCFullYear() === year &&
+ date.getUTCMonth() === month - 1 &&
+ date.getUTCDate() === day
+ );
+ }
+
+ private clauseIndex(catalogId: string | null): number {
+ return this.specialClauses().findIndex((clause) => clause.catalogId === catalogId);
+ }
+
private eventValue(event: Event): string {
const target = event.target;
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement
diff --git a/frontend/src/app/components/lease-form/lease-form.html b/frontend/src/app/components/lease-form/lease-form.html
index 250f08f..3fd8f58 100644
--- a/frontend/src/app/components/lease-form/lease-form.html
+++ b/frontend/src/app/components/lease-form/lease-form.html
@@ -201,11 +201,15 @@
[legalTerms]="leaseModel().legalTerms"
[tenantCount]="leaseModel().tenantIds.length"
[energyRating]="selectedEnergyRating()"
+ [saveFieldErrors]="saveFieldErrors()"
+ (fieldChanged)="fieldWasChanged($event)"
/>
@@ -218,10 +222,16 @@
Ajoutez un ou plusieurs contacts de garantie.
- @for (guarantor of guarantors(); track $index) {
+ @for (guarantor of guarantors(); track guarantor.id ?? $index) {