From a0f86bb511fcd098118bf56b45e4efa55a42a423 Mon Sep 17 00:00:00 2001 From: Nessar
{{ withdrawalDateError() }}
+ {{ serverError(withdrawalFieldErrors(), 'acceptedOn') }} +
- {{ withdrawalError() || withdrawalOperationError() }} + {{ withdrawalReasonError() }} +
+ {{ serverError(withdrawalFieldErrors(), 'reason') }} +
+ {{ withdrawalOperationError() }}
+ {{ invalidationDateError() }} +
+ {{ serverError(invalidationFieldErrors(), 'invalidatedOn') }} +
- {{ invalidationError() || invalidationOperationError() }} + {{ invalidationReasonError() }} +
+ {{ serverError(invalidationFieldErrors(), 'reason') }} +
+ {{ invalidationOperationError() }}
Les dates sont enregistrées comme des dates civiles dans le fuseau {{ timeZone() }}.
Les dates restent des dates civiles dans le fuseau {{ timeZone() }}.
{{ receivedOnError() }}
{{ serverError('receivedOn') }}
+ {{ requestedDepartureOnError() }} +
+ {{ serverError('requestedDepartureOn') }} +
{{ deliveryModeError() }}
{{ otherDeliveryModeError() }}
+ {{ serverError('otherDeliveryMode') }} +
{{ nonStandardDeliveryError() }}
+ {{ serverError('nonStandardDeliveryConfirmed') }} +
{{ lateRegistrationError() }}
+ {{ serverError('lateRegistrationConfirmed') }} +
Aucun justificatif ne sera associé à ce congé lors de son enregistrement.
+ {{ serverError('missingEvidenceConfirmed') }} +
- {{ validationError() || operationError() }} + {{ evidenceError() || operationError() }}
{{ agreementDateError() }}
+ {{ serverError(agreementFieldErrors(), 'endOn') }} +
{{ agreementReasonError() }}
- {{ agreementError() || agreementOperationError() }} + {{ serverError(agreementFieldErrors(), 'reason') }} +
+ {{ agreementOperationError() }}
+ {{ serverError(closureFieldErrors(), 'confirmed') }} +
{{ closureError() || closureOperationError() }} diff --git a/frontend/src/app/components/lease-end-resolution-forms/lease-end-resolution-forms.spec.ts b/frontend/src/app/components/lease-end-resolution-forms/lease-end-resolution-forms.spec.ts index 02d8efa..be53726 100644 --- a/frontend/src/app/components/lease-end-resolution-forms/lease-end-resolution-forms.spec.ts +++ b/frontend/src/app/components/lease-end-resolution-forms/lease-end-resolution-forms.spec.ts @@ -6,6 +6,7 @@ describe('LeaseEndResolutionForms', () => { function createFixture() { const fixture = TestBed.createComponent(LeaseEndResolutionForms); fixture.componentRef.setInput('timeZone', 'America/Los_Angeles'); + fixture.componentRef.setInput('leaseStartOn', '2026-01-01'); fixture.componentRef.setInput('agreementPending', false); fixture.componentRef.setInput('closurePending', false); fixture.detectChanges(); diff --git a/frontend/src/app/components/lease-end-resolution-forms/lease-end-resolution-forms.ts b/frontend/src/app/components/lease-end-resolution-forms/lease-end-resolution-forms.ts index 3816860..4cbd009 100644 --- a/frontend/src/app/components/lease-end-resolution-forms/lease-end-resolution-forms.ts +++ b/frontend/src/app/components/lease-end-resolution-forms/lease-end-resolution-forms.ts @@ -1,8 +1,9 @@ -import { Component, input, output, signal } from '@angular/core'; +import { Component, effect, ElementRef, inject, input, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { FormField, FormRoot, form, maxLength } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { AgreedEarlyEndRequest, CloseLeaseRequest } from '../../services/lease-end.service'; +import { FieldValidationError } from '../../services/http-error-message'; @Component({ selector: 'app-lease-end-resolution-forms', @@ -11,34 +12,64 @@ import { AgreedEarlyEndRequest, CloseLeaseRequest } from '../../services/lease-e styleUrl: './lease-end-resolution-forms.css', }) export class LeaseEndResolutionForms { + private readonly element = inject(ElementRef); readonly timeZone = input.required(); + readonly leaseStartOn = input.required(); + readonly currentEndOn = input(null); readonly agreementPending = input(false); readonly closurePending = input(false); readonly agreementOperationError = input(''); readonly closureOperationError = input(''); + readonly agreementFieldErrors = input([]); + readonly closureFieldErrors = input([]); readonly showAgreement = input(true); readonly showClosure = input(true); readonly keysReturnedOn = input(null); readonly closureEnabled = input(false); readonly agreedEndRequested = output(); readonly closureRequested = output(); + readonly agreementFieldChanged = output(); + readonly closureFieldChanged = output(); readonly agreementModel = signal({ endOn: '', reason: '' }); readonly closureModel = signal({ confirmed: false }); readonly agreementForm = form(this.agreementModel, (fields) => { maxLength(fields.reason, 500); }); readonly closureForm = form(this.closureModel); - readonly agreementError = signal(''); readonly closureError = signal(''); + readonly agreementSubmitted = signal(false); + + constructor() { + effect(() => { + const agreementField = this.agreementFieldErrors()[0]?.field; + if (agreementField) { + queueMicrotask(() => + (this.element.nativeElement as HTMLElement) + .querySelector( + agreementField === 'reason' ? '#agreement-reason' : '#agreement-end-on', + ) + ?.focus(), + ); + } + const closureField = this.closureFieldErrors()[0]?.field; + if (closureField) { + queueMicrotask(() => + (this.element.nativeElement as HTMLElement) + .querySelector('#closure-confirmed') + ?.focus(), + ); + } + }); + } submitAgreement(): void { if (this.agreementPending()) return; + this.agreementSubmitted.set(true); + this.agreementForm().markAsTouched(); const model = this.agreementModel(); - if (!model.endOn || !model.reason.trim()) { - this.agreementError.set('Renseignez la date et la raison de la fin anticipée.'); + if (this.agreementDateError() || this.agreementReasonError()) { return; } - this.agreementError.set(''); this.agreedEndRequested.emit({ timeZone: this.timeZone(), endOn: model.endOn, @@ -46,6 +77,40 @@ export class LeaseEndResolutionForms { }); } + agreementDateError(): string { + if (!this.show(this.agreementForm.endOn())) return ''; + const endOn = this.agreementModel().endOn; + if (!endOn) return 'Renseignez la date de fin convenue.'; + if (endOn < this.leaseStartOn()) { + return 'La fin convenue ne peut pas précéder le début du bail.'; + } + const currentEndOn = this.currentEndOn(); + return currentEndOn && endOn >= currentEndOn + ? 'La fin convenue doit être antérieure à la fin actuelle du bail.' + : ''; + } + + agreementReasonError(): string { + if (!this.show(this.agreementForm.reason())) return ''; + const reason = this.agreementModel().reason.trim(); + if (!reason) return 'Renseignez la raison de la fin anticipée.'; + return reason.length > 500 ? 'La raison ne peut pas dépasser 500 caractères.' : ''; + } + + serverError(errors: FieldValidationError[], field: string): string { + const code = errors.find((error) => error.field === field)?.code; + if (field === 'endOn' && code === 'after_or_equal_lease_start') { + return 'La fin convenue ne peut pas précéder le début du bail.'; + } + if (field === 'endOn' && code === 'before_current_end') { + return 'La fin convenue doit être antérieure à la fin actuelle du bail.'; + } + if (field === 'reason' && code === 'max_length') + return 'La raison ne peut pas dépasser 500 caractères.'; + if (field === 'confirmed' && code) return 'Confirmez la restitution réelle des clés.'; + return ''; + } + submitClosure(): void { if (this.closurePending()) return; const model = this.closureModel(); @@ -65,4 +130,8 @@ export class LeaseEndResolutionForms { confirmed: true, }); } + + private show(field: { touched(): boolean }): boolean { + return this.agreementSubmitted() || field.touched(); + } } diff --git a/frontend/src/app/services/lease-end.service.ts b/frontend/src/app/services/lease-end.service.ts index 0ec21f6..8ee5814 100644 --- a/frontend/src/app/services/lease-end.service.ts +++ b/frontend/src/app/services/lease-end.service.ts @@ -48,8 +48,10 @@ export interface LeaseEndState { unsupportedReason: string | null; phase: LeaseEndPhase; leaseType: 'STANDARD' | 'STUDENT'; + leaseStartOn: string; fixedEndOn: string | null; effectiveEndOn: string | null; + maxEvidenceBytes: number; correctionRequired: boolean; exitConditionReport: { occurredOn: string;