refactor: split tenant and lease page modules
This commit is contained in:
1 parent
ebfc62a5e2
commit
3ca8514231
27 files changed
+1199
-1143
No files matched your search
@@ -0,0 +1,115 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.tenant-card {
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 22px;
|
||||
background: var(--surface);
|
||||
}
|
||||
.tenant-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
.tenant-identity h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.tenant-avatar {
|
||||
display: grid;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
flex: 0 0 46px;
|
||||
place-items: center;
|
||||
border-radius: 15px;
|
||||
background: var(--surface-accent);
|
||||
color: var(--accent);
|
||||
font-weight: 800;
|
||||
}
|
||||
.contact-list {
|
||||
display: grid;
|
||||
gap: 11px;
|
||||
margin: 22px 0;
|
||||
}
|
||||
.contact-list div {
|
||||
min-width: 0;
|
||||
}
|
||||
.contact-list dt {
|
||||
color: var(--text-subtle);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 750;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.contact-list dd {
|
||||
overflow-wrap: anywhere;
|
||||
margin: 3px 0 0;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.attachments {
|
||||
padding: 15px;
|
||||
border-radius: 15px;
|
||||
background: var(--surface-soft);
|
||||
}
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.section-title span {
|
||||
display: grid;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--surface);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.attachment-row.mat-mdc-button {
|
||||
--mdc-text-button-label-text-color: var(--text);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
justify-content: flex-start;
|
||||
padding: 0 5px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.upload-action {
|
||||
display: block;
|
||||
min-height: 40px;
|
||||
padding: 11px 5px 4px;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 750;
|
||||
}
|
||||
.upload-action input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
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;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.card-actions .mat-mdc-button {
|
||||
--mdc-text-button-label-text-color: var(--accent);
|
||||
}
|
||||
.card-actions .danger-action.mat-mdc-button {
|
||||
--mdc-text-button-label-text-color: var(--danger);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<article class="tenant-card">
|
||||
<div class="tenant-identity">
|
||||
<span class="tenant-avatar"
|
||||
>{{ tenant().firstName.charAt(0) }}{{ tenant().lastName.charAt(0) }}</span
|
||||
>
|
||||
<div>
|
||||
<p class="property-label">Locataire</p>
|
||||
<h2>{{ tenant().firstName }} {{ tenant().lastName }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="contact-list">
|
||||
<div>
|
||||
<dt>E-mail</dt>
|
||||
<dd>{{ tenant().email }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Téléphone</dt>
|
||||
<dd>{{ tenant().phone || 'Non renseigné' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Adresse</dt>
|
||||
<dd>{{ tenant().address || 'Non renseignée' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="attachments">
|
||||
<div class="section-title">
|
||||
<strong>Pièces jointes</strong><span>{{ tenant().attachments?.length ?? 0 }}</span>
|
||||
</div>
|
||||
@for (attachment of tenant().attachments ?? []; track attachment.id) {
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="attachment-row"
|
||||
[attr.aria-label]="'Télécharger la pièce jointe ' + attachment.fileName"
|
||||
(click)="attachmentDownloadRequested.emit({ tenantId: tenant().id!, attachment })"
|
||||
>
|
||||
<span aria-hidden="true">↧</span>{{ attachment.fileName }}
|
||||
</button>
|
||||
}
|
||||
@if (tenant().id) {
|
||||
@if (uploadError() || attachmentError()) {
|
||||
<p class="attachment-error" role="alert">{{ uploadError() || attachmentError() }}</p>
|
||||
}
|
||||
<label class="upload-action"
|
||||
><input
|
||||
type="file"
|
||||
[attr.aria-label]="
|
||||
'Ajouter une pièce jointe pour ' + tenant().firstName + ' ' + tenant().lastName
|
||||
"
|
||||
(change)="selectFile($event)"
|
||||
/>+ Ajouter une pièce jointe <small>· 250 Mo maximum</small></label
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button mat-button type="button" (click)="editRequested.emit(tenant())">Modifier</button>
|
||||
@if (tenant().id) {
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="danger-action"
|
||||
(click)="deleteRequested.emit(tenant().id!)"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TenantCard } from './tenant-card';
|
||||
|
||||
describe('TenantCard', () => {
|
||||
it('rejects files larger than the upload limit before emitting', () => {
|
||||
const fixture = TestBed.createComponent(TenantCard);
|
||||
fixture.componentRef.setInput('tenant', {
|
||||
id: 'tenant-1',
|
||||
firstName: 'Camille',
|
||||
lastName: 'Martin',
|
||||
email: 'camille@example.com',
|
||||
phone: '',
|
||||
address: '',
|
||||
});
|
||||
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 } });
|
||||
|
||||
fixture.componentInstance.selectFile({ target: input } as unknown as Event);
|
||||
|
||||
expect(fixture.componentInstance.uploadError()).toContain('250 Mo');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Component, input, output, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { Tenant, TenantAttachment } from '../../services/tenant.service';
|
||||
|
||||
export interface TenantAttachmentRequest {
|
||||
tenantId: string;
|
||||
attachment: TenantAttachment;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-tenant-card',
|
||||
imports: [MatButtonModule],
|
||||
templateUrl: './tenant-card.html',
|
||||
styleUrl: './tenant-card.css',
|
||||
})
|
||||
export class TenantCard {
|
||||
readonly tenant = input.required<Tenant>();
|
||||
readonly attachmentError = input('');
|
||||
readonly editRequested = output<Tenant>();
|
||||
readonly deleteRequested = output<string>();
|
||||
readonly attachmentUploadRequested = output<{ tenantId: string; file: File }>();
|
||||
readonly attachmentDownloadRequested = output<TenantAttachmentRequest>();
|
||||
readonly uploadError = signal('');
|
||||
|
||||
selectFile(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.item(0);
|
||||
const tenantId = this.tenant().id;
|
||||
if (file && file.size > 250 * 1024 * 1024) {
|
||||
this.uploadError.set('Une pièce jointe ne peut pas dépasser 250 Mo.');
|
||||
} else if (tenantId && file) {
|
||||
this.uploadError.set('');
|
||||
this.attachmentUploadRequested.emit({ tenantId, file });
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user