fix: validate tenant and lease contact details

This commit is contained in:
nessar committed 2026-08-17 14:18:14 +02:00
1 parent 8603b98714
commit bf6d8e6bb6
27 files changed
+444 -54

No files matched your search

@@ -127,6 +127,15 @@
overflow: hidden;
clip: rect(0 0 0 0);
}
.upload-action small {
color: var(--text-muted);
font-weight: 500;
}
.attachment-error {
margin: 8px 0 0;
color: var(--danger);
font-size: 0.78rem;
}
.card-actions {
display: flex;
justify-content: flex-end;
@@ -63,16 +63,20 @@
</button>
}
@if (tenant.id) {
@if (attachmentUploadError() || attachmentError()) {
<p class="attachment-error" role="alert">
{{ attachmentUploadError() || attachmentError() }}
</p>
}
<label class="upload-action">
<input
type="file"
accept="application/pdf,image/*"
[attr.aria-label]="
'Ajouter une pièce jointe pour ' + tenant.firstName + ' ' + tenant.lastName
"
(change)="selectFile(tenant, $event)"
/>
+ Ajouter une pièce jointe
+ Ajouter une pièce jointe <small>· 250 Mo maximum</small>
</label>
}
</div>
@@ -136,6 +140,7 @@
[ngModel]="email()"
(ngModelChange)="email.set($event)"
required
autocomplete="email"
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Téléphone</mat-label
@@ -145,6 +150,7 @@
name="phone"
[ngModel]="phone()"
(ngModelChange)="phone.set($event)"
autocomplete="tel"
/></mat-form-field>
<mat-form-field appearance="outline" class="full-field"
><mat-label>Adresse complète</mat-label
@@ -21,6 +21,23 @@ describe('TenantsPage', () => {
expect(component.showForm()).toBe(false);
});
it('rejects malformed tenant contact details', () => {
const fixture = TestBed.createComponent(TenantsPage);
fixture.componentRef.setInput('tenants', []);
const component = fixture.componentInstance;
component.firstName.set('Camille');
component.lastName.set('Martin');
component.email.set('camille-at-example.com');
component.save();
expect(component.error()).toContain('e-mail valide');
component.email.set('camille@example.com');
component.phone.set('invalid');
component.save();
expect(component.error()).toContain('numéro français');
});
it('emits a selected attachment for an existing tenant', () => {
const fixture = TestBed.createComponent(TenantsPage);
fixture.componentRef.setInput('tenants', []);
@@ -46,6 +63,23 @@ describe('TenantsPage', () => {
expect(uploadedFile).toBe(file);
});
it('rejects an attachment larger than 250 Mo before upload', () => {
const fixture = TestBed.createComponent(TenantsPage);
fixture.componentRef.setInput('tenants', []);
const component = fixture.componentInstance;
const file = new File(['identity'], 'archive.zip');
Object.defineProperty(file, 'size', { value: 250 * 1024 * 1024 + 1 });
const input = document.createElement('input');
Object.defineProperty(input, 'files', { value: { item: () => file } });
component.selectFile(
{ id: 'tenant-1', firstName: 'Camille', lastName: 'Martin', email: 'camille@example.com', phone: '', address: '' },
{ target: input } as unknown as Event,
);
expect(component.attachmentUploadError()).toContain('250 Mo');
});
it('opens, fills, and closes the edit form', () => {
const fixture = TestBed.createComponent(TenantsPage);
fixture.componentRef.setInput('tenants', []);
@@ -3,6 +3,7 @@ import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { isValidEmail, isValidPhone } from '../../shared/contact-validation';
import { Tenant, TenantAttachment } from '../../services/tenant.service';
export interface TenantSaveRequest {
@@ -23,6 +24,7 @@ export interface TenantAttachmentRequest {
})
export class TenantsPage {
readonly tenants = input.required<Tenant[]>();
readonly attachmentError = input('');
readonly tenantSaved = output<TenantSaveRequest>();
readonly deleteRequested = output<string>();
readonly attachmentUploadRequested = output<{ tenantId: string; file: File }>();
@@ -36,6 +38,7 @@ export class TenantsPage {
readonly phone = signal('');
readonly address = signal('');
readonly error = signal('');
readonly attachmentUploadError = signal('');
constructor() {
effect(() => this.fillForm(this.editingTenant()));
@@ -62,6 +65,14 @@ export class TenantsPage {
this.error.set('Le prénom, le nom et l’e-mail sont obligatoires.');
return;
}
if (!isValidEmail(this.email())) {
this.error.set('Saisissez une adresse e-mail valide.');
return;
}
if (!isValidPhone(this.phone())) {
this.error.set('Saisissez un numéro français ou international valide.');
return;
}
this.tenantSaved.emit({
tenant: {
firstName: this.firstName().trim(),
@@ -77,7 +88,10 @@ export class TenantsPage {
selectFile(tenant: Tenant, event: Event): void {
const file = (event.target as HTMLInputElement).files?.item(0);
if (tenant.id && file) {
if (file && file.size > 250 * 1024 * 1024) {
this.attachmentUploadError.set('Une pièce jointe ne peut pas dépasser 250 Mo.');
} else if (tenant.id && file) {
this.attachmentUploadError.set('');
this.attachmentUploadRequested.emit({ tenantId: tenant.id, file });
}
(event.target as HTMLInputElement).value = '';