From 983a080cc7489e67efa8907e8c8bb54b91d8e42b Mon Sep 17 00:00:00 2001 From: Nessar Date: Thu, 23 Jul 2026 20:01:15 +0200 Subject: [PATCH] feat(private-area-editor): enhance UI with Material components and improve area management --- .vscode/launch.json | 5 +- .vscode/tasks.json | 92 ++++++++--------- compose.dev.yml | 2 + .../private-area-editor.css | 64 +++++++++--- .../private-area-editor.html | 98 ++++++++++++------- .../private-area-editor.spec.ts | 15 +++ .../private-area-editor.ts | 20 ++-- 7 files changed, 191 insertions(+), 105 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e396348..dabc1b2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,15 +5,12 @@ "type": "java", "name": "Backend: Spring Boot", "request": "launch", - "presentation": { - "hidden": true - }, "mainClass": "fr.nessar.lmnporganizer.LmnpOrganizerApplication", "projectName": "backend", "cwd": "${workspaceFolder}/backend", "envFile": "${workspaceFolder}/.env", "env": { - "SPRING_DATA_MONGODB_URI": "mongodb://admin:adminpassword@localhost:27017/lmnp?authSource=admin" + "SPRING_DATA_MONGODB_URI": "mongodb://${MONGO_ROOT_USERNAME:lmnp}:${MONGO_ROOT_PASSWORD}@localhost:${MONGO_PORT:27017}/lmnp?authSource=admin&replicaSet=rs0&directConnection=true" }, "preLaunchTask": "Start MongoDB", "postDebugTask": "Stop MongoDB" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 89bb1fb..57072b8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,47 +1,47 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "Start MongoDB", - "type": "shell", - "command": "docker compose up --detach --wait mongodb", - "options": { - "cwd": "${workspaceFolder}" - }, - "problemMatcher": [] - }, - { - "label": "Stop MongoDB", - "type": "shell", - "command": "docker compose stop mongodb", - "options": { - "cwd": "${workspaceFolder}" - }, - "problemMatcher": [] - }, - { - "label": "Start Angular", - "type": "shell", - "command": "bun run start -- --proxy-config proxy.conf.json", - "options": { - "cwd": "${workspaceFolder}/frontend" - }, - "isBackground": true, - "problemMatcher": { - "owner": "angular", - "pattern": { - "regexp": "^$" - }, - "background": { - "activeOnStart": true, - "beginsPattern": ".*", - "endsPattern": "^\\s*➜\\s+Local:\\s+http://localhost:4200/\\s*$" - } - }, - "presentation": { - "reveal": "always", - "panel": "dedicated" - } - } - ] -} + "version": "2.0.0", + "tasks": [ + { + "label": "Start MongoDB", + "type": "shell", + "command": "docker compose --file=compose.dev.yml up --detach --wait mongodb", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "Stop MongoDB", + "type": "shell", + "command": "docker compose --file=compose.dev.yml stop mongodb", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "Start Angular", + "type": "shell", + "command": "bun run start -- --proxy-config proxy.conf.json", + "options": { + "cwd": "${workspaceFolder}/frontend" + }, + "isBackground": true, + "problemMatcher": { + "owner": "angular", + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": ".*", + "endsPattern": "^\\s*➜\\s+Local:\\s+http://localhost:4200/\\s*$" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated" + } + } + ] +} \ No newline at end of file diff --git a/compose.dev.yml b/compose.dev.yml index a8de085..1b02063 100644 --- a/compose.dev.yml +++ b/compose.dev.yml @@ -4,6 +4,8 @@ services: mongodb: image: mongo:8.0 restart: unless-stopped + ports: + - "${MONGO_PORT:-27017}:27017" entrypoint: - /bin/bash - -c diff --git a/frontend/src/app/components/private-area-editor/private-area-editor.css b/frontend/src/app/components/private-area-editor/private-area-editor.css index 13947c2..43c02dd 100644 --- a/frontend/src/app/components/private-area-editor/private-area-editor.css +++ b/frontend/src/app/components/private-area-editor/private-area-editor.css @@ -1,17 +1,57 @@ .private-areas { - display: grid; - gap: 12px; + display: grid; + gap: var(--space-3, 0.75rem); } -.private-areas fieldset { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: 12px; + +.private-area { + overflow: hidden; + border: 1px solid var(--border, #dce5df); + background: var(--surface-raised, #ffffff); + box-shadow: none; } -.private-areas label { - display: grid; - gap: 4px; + +.private-area-heading { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: var(--space-2, 0.5rem); + margin-bottom: var(--space-4, 1rem); } -.private-areas input, -.private-areas select { - min-height: 40px; + +.private-area-heading h3 { + min-width: 0; + margin: 0; + overflow-wrap: break-word; + font-size: 1rem; } + +.danger-action.mat-mdc-button { + --mdc-text-button-label-text-color: var(--danger, #b42318); +} + +.main-room-count { + color: var(--accent, #0f766e); + font-size: 0.875rem; + font-weight: 700; +} + +.empty-private-areas { + display: grid; + place-items: center; + gap: var(--space-3, 0.75rem); + padding: var(--space-6, 1.5rem); + border: 1px dashed var(--border-strong, #c7d3cd); + border-radius: var(--radius-md, 0.8125rem); + background: var(--surface-soft, #e9efeb); + color: var(--text-muted, #687670); + text-align: center; +} + +.empty-private-areas p { + margin: 0; +} + +.add-private-area { + margin-top: var(--space-4, 1rem); +} \ No newline at end of file diff --git a/frontend/src/app/components/private-area-editor/private-area-editor.html b/frontend/src/app/components/private-area-editor/private-area-editor.html index 151158d..ddba006 100644 --- a/frontend/src/app/components/private-area-editor/private-area-editor.html +++ b/frontend/src/app/components/private-area-editor/private-area-editor.html @@ -1,38 +1,62 @@
-
-

Espaces privatifs

-

- Le nombre réglementaire de pièces principales est calculé depuis ces espaces : - {{ mainRoomCount() }}. -

-
-
- @for (area of areas(); track area.id) { -
- {{ area.label || 'Espace privatif' }} - - - - -
- } -
- -
+
+

Espaces privatifs

+

Le nombre réglementaire de pièces principales est calculé depuis ces espaces.

+

+ {{ mainRoomCount() }} pièce{{ mainRoomCount() === 1 ? '' : 's' }} principale{{ + mainRoomCount() === 1 ? '' : 's' + }} +

+
+ @if (areas().length === 0) { +
+

Aucun espace privatif renseigné.

+ +
+ } @else { +
+ @for (area of areas(); track area.id) { + + +
+

{{ area.label || 'Nouvel espace privatif' }}

+ +
+
+ + Famille + + @for (family of families; track family.value) { + {{ family.label }} + } + + + + Sous-type + + @for (subtype of subtypesFor(area.family); track subtype.value) { + {{ subtype.label }} + } + + + + Libellé + + +
+
+
+ } +
+ } + @if (areas().length !== 0) { + + } + \ No newline at end of file diff --git a/frontend/src/app/components/private-area-editor/private-area-editor.spec.ts b/frontend/src/app/components/private-area-editor/private-area-editor.spec.ts index cb3c589..ad76c11 100644 --- a/frontend/src/app/components/private-area-editor/private-area-editor.spec.ts +++ b/frontend/src/app/components/private-area-editor/private-area-editor.spec.ts @@ -9,6 +9,21 @@ function eventFor(target: EventTarget): Event { } describe('PrivateAreaEditor', () => { + it('renders each private area with Material controls', () => { + const fixture = TestBed.createComponent(PrivateAreaEditor); + fixture.componentRef.setInput('areas', [ + { id: 'area-1', family: 'PIECE_PRINCIPALE', subtype: 'BEDROOM', label: 'Chambre' }, + ]); + + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + expect(element.querySelector('mat-card')).not.toBeNull(); + expect(element.querySelectorAll('mat-form-field')).toHaveLength(3); + expect(element.querySelectorAll('mat-select')).toHaveLength(2); + expect(element.textContent).toContain('1 pièce principale'); + }); + it('adds a stable private area and derives the main-room count from its family', () => { const fixture = TestBed.createComponent(PrivateAreaEditor); fixture.componentRef.setInput('areas', []); diff --git a/frontend/src/app/components/private-area-editor/private-area-editor.ts b/frontend/src/app/components/private-area-editor/private-area-editor.ts index 072f048..ce31c3a 100644 --- a/frontend/src/app/components/private-area-editor/private-area-editor.ts +++ b/frontend/src/app/components/private-area-editor/private-area-editor.ts @@ -1,4 +1,9 @@ import { Component, computed, input, output } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; import { PrivateArea, PrivateAreaFamily, @@ -35,6 +40,7 @@ const subtypeFamilies: Record @Component({ selector: 'app-private-area-editor', + imports: [MatButtonModule, MatCardModule, MatFormFieldModule, MatInputModule, MatSelectModule], templateUrl: './private-area-editor.html', styleUrl: './private-area-editor.css', }) @@ -58,17 +64,19 @@ export class PrivateAreaEditor { ]); } - updateArea(id: string, field: keyof Omit, event: Event): void { - const value = - event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement - ? event.target.value - : ''; + updateArea(id: string, field: keyof Omit, value: string | Event): void { + const nextValue = + typeof value === 'string' + ? value + : value.target instanceof HTMLInputElement || value.target instanceof HTMLSelectElement + ? value.target.value + : ''; this.areasChanged.emit( this.areas().map((area) => area.id === id ? { ...area, - [field]: value, + [field]: nextValue, ...(field === 'family' ? { subtype: 'OTHER' as const } : {}), } : area,