From 5581cb1ef359e7c949723702b7ace3574c811fb8 Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Thu, 17 Sep 2026 11:59:54 +0200 Subject: [PATCH 1/3] feat(frontend): vue liste des sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouveau SitesService (GET /sites) et page SiteList consommant le design système (ev-card, ev-badge, ev-alert, ev-brand). Ajoute la route /sites, un lien depuis le dashboard, et une route détail /sites/:siteId pointant vers un placeholder minimal en attendant l'issue #51. Closes #49 --- apps/frontend/src/app/app.routes.ts | 13 +++ .../app/core/services/sites.service.spec.ts | 41 ++++++++++ .../src/app/core/services/sites.service.ts | 13 +++ .../src/app/features/dashboard/dashboard.html | 13 ++- .../src/app/features/dashboard/dashboard.scss | 16 ++++ .../app/features/dashboard/dashboard.spec.ts | 22 +++-- .../src/app/features/dashboard/dashboard.ts | 14 +++- .../site-detail-placeholder.html | 5 ++ .../site-detail-placeholder.scss | 6 ++ .../site-detail-placeholder.spec.ts | 22 +++++ .../site-detail-placeholder.ts | 16 ++++ .../features/sites/site-list/site-list.html | 40 +++++++++ .../features/sites/site-list/site-list.scss | 73 +++++++++++++++++ .../sites/site-list/site-list.spec.ts | 81 +++++++++++++++++++ .../app/features/sites/site-list/site-list.ts | 47 +++++++++++ .../src/app/shared/models/site.model.ts | 8 ++ 16 files changed, 417 insertions(+), 13 deletions(-) create mode 100644 apps/frontend/src/app/core/services/sites.service.spec.ts create mode 100644 apps/frontend/src/app/core/services/sites.service.ts create mode 100644 apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.html create mode 100644 apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.scss create mode 100644 apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts create mode 100644 apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts create mode 100644 apps/frontend/src/app/features/sites/site-list/site-list.html create mode 100644 apps/frontend/src/app/features/sites/site-list/site-list.scss create mode 100644 apps/frontend/src/app/features/sites/site-list/site-list.spec.ts create mode 100644 apps/frontend/src/app/features/sites/site-list/site-list.ts create mode 100644 apps/frontend/src/app/shared/models/site.model.ts diff --git a/apps/frontend/src/app/app.routes.ts b/apps/frontend/src/app/app.routes.ts index 72e20f5..e619268 100644 --- a/apps/frontend/src/app/app.routes.ts +++ b/apps/frontend/src/app/app.routes.ts @@ -12,4 +12,17 @@ export const routes: Routes = [ canActivate: [authGuard], loadComponent: () => import('./features/dashboard/dashboard').then(m => m.Dashboard), }, + { + path: 'sites', + canActivate: [authGuard], + loadComponent: () => import('./features/sites/site-list/site-list').then(m => m.SiteList), + }, + { + path: 'sites/:siteId', + canActivate: [authGuard], + loadComponent: () => + import('./features/sites/site-detail-placeholder/site-detail-placeholder').then( + (m) => m.SiteDetailPlaceholder, + ), + }, ]; diff --git a/apps/frontend/src/app/core/services/sites.service.spec.ts b/apps/frontend/src/app/core/services/sites.service.spec.ts new file mode 100644 index 0000000..45aee2c --- /dev/null +++ b/apps/frontend/src/app/core/services/sites.service.spec.ts @@ -0,0 +1,41 @@ +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing'; +import { SitesService } from './sites.service'; +import { environment } from '../../../environments/environment'; + +describe('SitesService', () => { + let service: SitesService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideHttpClientTesting()], + }); + service = TestBed.inject(SitesService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => httpMock.verify()); + + it('appelle le bon endpoint et retourne la liste des sites', () => { + let result: unknown; + service.getSites().subscribe((r) => (result = r)); + + const req = httpMock.expectOne(`${environment.apiUrl}/sites`); + expect(req.request.method).toBe('GET'); + + req.flush([ + { + site_id: 'SITE001', + site_name: 'Site 1', + site_type: 'industriel', + location: 'Nantes', + capacity_kw: 500, + status: 'actif', + }, + ]); + + expect((result as { site_id: string }[])[0].site_id).toBe('SITE001'); + }); +}); diff --git a/apps/frontend/src/app/core/services/sites.service.ts b/apps/frontend/src/app/core/services/sites.service.ts new file mode 100644 index 0000000..85754cc --- /dev/null +++ b/apps/frontend/src/app/core/services/sites.service.ts @@ -0,0 +1,13 @@ +import { Service, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../../environments/environment'; +import { Site } from '../../shared/models/site.model'; + +@Service() +export class SitesService { + private http = inject(HttpClient); + + getSites() { + return this.http.get(`${environment.apiUrl}/sites`); + } +} diff --git a/apps/frontend/src/app/features/dashboard/dashboard.html b/apps/frontend/src/app/features/dashboard/dashboard.html index 6519fc9..a626229 100644 --- a/apps/frontend/src/app/features/dashboard/dashboard.html +++ b/apps/frontend/src/app/features/dashboard/dashboard.html @@ -7,9 +7,16 @@

Consommation instantanée du parc

- Déconnexion +
+ Voir les sites + Déconnexion +
@if (error(); as message) { diff --git a/apps/frontend/src/app/features/dashboard/dashboard.scss b/apps/frontend/src/app/features/dashboard/dashboard.scss index 715ce8c..d7d5981 100644 --- a/apps/frontend/src/app/features/dashboard/dashboard.scss +++ b/apps/frontend/src/app/features/dashboard/dashboard.scss @@ -34,6 +34,22 @@ color: var(--color-text-muted); } +.dashboard__actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.dashboard__sites-link { + color: var(--color-primary); + font-weight: 600; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + h2 { font-size: 1.1rem; font-weight: 600; diff --git a/apps/frontend/src/app/features/dashboard/dashboard.spec.ts b/apps/frontend/src/app/features/dashboard/dashboard.spec.ts index 4254945..f55c10b 100644 --- a/apps/frontend/src/app/features/dashboard/dashboard.spec.ts +++ b/apps/frontend/src/app/features/dashboard/dashboard.spec.ts @@ -5,7 +5,7 @@ import { Dashboard } from './dashboard'; import { StatsService } from '../../core/services/stats.service'; import { AlertsService } from '../../core/services/alerts.service'; import {AuthService} from '../../core/services/auth.service'; -import {Router} from '@angular/router'; +import {Router, provideRouter} from '@angular/router'; vi.mock('chart.js', () => { class ChartMock { @@ -29,6 +29,7 @@ describe('Dashboard', () => { providers: [ { provide: StatsService, useValue: statsMock }, { provide: AlertsService, useValue: alertsMock }, + provideRouter([]), ], }); @@ -60,6 +61,7 @@ describe('Dashboard', () => { providers: [ { provide: StatsService, useValue: statsMock }, { provide: AlertsService, useValue: alertsMock }, + provideRouter([]), ], }); @@ -86,6 +88,7 @@ describe('Dashboard', () => { providers: [ { provide: StatsService, useValue: statsMock }, { provide: AlertsService, useValue: alertsMock }, + provideRouter([]), ], }); @@ -99,7 +102,6 @@ describe('Dashboard', () => { const statsMock = { getSummary: vi.fn().mockReturnValue(of({ total_sites: 7, sites: [] })) }; const alertsMock = { getAlerts: vi.fn().mockReturnValue(of([])) }; const authMock = { logout: vi.fn().mockReturnValue(of(undefined)), clearSession: vi.fn() }; - const routerMock = { navigate: vi.fn() }; TestBed.configureTestingModule({ imports: [Dashboard], @@ -107,18 +109,21 @@ describe('Dashboard', () => { { provide: StatsService, useValue: statsMock }, { provide: AlertsService, useValue: alertsMock }, { provide: AuthService, useValue: authMock }, - { provide: Router, useValue: routerMock }, + provideRouter([]), ], }); const fixture = TestBed.createComponent(Dashboard); fixture.detectChanges(); + const router = TestBed.inject(Router); + const navigateSpy = vi.spyOn(router, 'navigate').mockResolvedValue(true); + const button = fixture.nativeElement.querySelector('.logout-button'); button.click(); expect(authMock.logout).toHaveBeenCalled(); - expect(routerMock.navigate).toHaveBeenCalledWith(['/login']); + expect(navigateSpy).toHaveBeenCalledWith(['/login']); }); it('déconnecte localement et redirige vers /login même si logout échoue côté réseau', () => { const statsMock = { getSummary: vi.fn().mockReturnValue(of({ total_sites: 7, sites: [] })) }; @@ -127,26 +132,27 @@ describe('Dashboard', () => { logout: vi.fn().mockReturnValue(throwError(() => new Error('réseau indisponible'))), clearSession: vi.fn(), }; - const routerMock = { navigate: vi.fn() }; - TestBed.configureTestingModule({ imports: [Dashboard], providers: [ { provide: StatsService, useValue: statsMock }, { provide: AlertsService, useValue: alertsMock }, { provide: AuthService, useValue: authMock }, - { provide: Router, useValue: routerMock }, + provideRouter([]), ], }); const fixture = TestBed.createComponent(Dashboard); fixture.detectChanges(); + const router = TestBed.inject(Router); + const navigateSpy = vi.spyOn(router, 'navigate').mockResolvedValue(true); + const button = fixture.nativeElement.querySelector('.logout-button'); button.click(); expect(authMock.clearSession).toHaveBeenCalled(); - expect(routerMock.navigate).toHaveBeenCalledWith(['/login']); + expect(navigateSpy).toHaveBeenCalledWith(['/login']); }); it('distingue le ton des sévérités high et critical', () => { diff --git a/apps/frontend/src/app/features/dashboard/dashboard.ts b/apps/frontend/src/app/features/dashboard/dashboard.ts index b2137a1..e9f9792 100644 --- a/apps/frontend/src/app/features/dashboard/dashboard.ts +++ b/apps/frontend/src/app/features/dashboard/dashboard.ts @@ -2,7 +2,7 @@ import { Component, OnInit, inject, signal, DestroyRef } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { timer, switchMap, catchError, EMPTY, Observable } from 'rxjs'; import { DecimalPipe } from '@angular/common'; -import { Router } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { StatsService } from '../../core/services/stats.service'; import { ConsumptionGauge } from '../../shared/components/consumption-gauge/consumption-gauge'; import { SiteLoadChart } from '../../shared/components/site-load-chart/site-load-chart'; @@ -30,7 +30,17 @@ const TON_PAR_SEVERITE: Record = { @Component({ selector: 'app-dashboard', standalone: true, - imports: [DecimalPipe, ConsumptionGauge, SiteLoadChart, Card, EvAlert, Badge, Brand, Button], + imports: [ + DecimalPipe, + RouterLink, + ConsumptionGauge, + SiteLoadChart, + Card, + EvAlert, + Badge, + Brand, + Button, + ], templateUrl: './dashboard.html', styleUrl: './dashboard.scss', }) diff --git a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.html b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.html new file mode 100644 index 0000000..f12212c --- /dev/null +++ b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.html @@ -0,0 +1,5 @@ +
+ +

Détail du site {{ siteId }} : à venir (voir issue #51).

+
+
diff --git a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.scss b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.scss new file mode 100644 index 0000000..b3874ae --- /dev/null +++ b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.scss @@ -0,0 +1,6 @@ +:host { + display: block; + padding: 2.5rem 2rem; + max-width: 640px; + margin: 0 auto; +} diff --git a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts new file mode 100644 index 0000000..386a686 --- /dev/null +++ b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts @@ -0,0 +1,22 @@ +import { TestBed } from '@angular/core/testing'; +import { ActivatedRoute, convertToParamMap } from '@angular/router'; +import { SiteDetailPlaceholder } from './site-detail-placeholder'; + +describe('SiteDetailPlaceholder', () => { + it("affiche l'identifiant du site depuis la route", () => { + TestBed.configureTestingModule({ + imports: [SiteDetailPlaceholder], + providers: [ + { + provide: ActivatedRoute, + useValue: { snapshot: { paramMap: convertToParamMap({ siteId: 'SITE001' }) } }, + }, + ], + }); + + const fixture = TestBed.createComponent(SiteDetailPlaceholder); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain('SITE001'); + }); +}); diff --git a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts new file mode 100644 index 0000000..720557b --- /dev/null +++ b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts @@ -0,0 +1,16 @@ +import { Component, inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Card } from '../../../shared/components/ui/card/card'; + +@Component({ + selector: 'app-site-detail-placeholder', + standalone: true, + imports: [Card], + templateUrl: './site-detail-placeholder.html', + styleUrl: './site-detail-placeholder.scss', +}) +export class SiteDetailPlaceholder { + private route = inject(ActivatedRoute); + + siteId = this.route.snapshot.paramMap.get('siteId'); +} diff --git a/apps/frontend/src/app/features/sites/site-list/site-list.html b/apps/frontend/src/app/features/sites/site-list/site-list.html new file mode 100644 index 0000000..509c08d --- /dev/null +++ b/apps/frontend/src/app/features/sites/site-list/site-list.html @@ -0,0 +1,40 @@ +
+
+
+ + @if (error(); as message) { + + } + + + + + + + + + + + + + + + @for (site of sites(); track site.site_id) { + + + + + + + + + } + +
NomTypeLocalisationCapacité (kW)Statut
{{ site.site_name }}{{ site.site_type }}{{ site.location ?? '-' }}{{ site.capacity_kw ?? '-' }}{{ site.status ?? '-' }}Détail
+
+
diff --git a/apps/frontend/src/app/features/sites/site-list/site-list.scss b/apps/frontend/src/app/features/sites/site-list/site-list.scss new file mode 100644 index 0000000..d253de5 --- /dev/null +++ b/apps/frontend/src/app/features/sites/site-list/site-list.scss @@ -0,0 +1,73 @@ +:host { + display: block; + color: var(--color-text); + padding: 2.5rem 2rem; + max-width: 1100px; + margin: 0 auto; +} + +.site-list__header { + display: flex; + align-items: center; + gap: 0.85rem; + margin-bottom: 2rem; + + h1 { + margin: 0; + font-size: 1.75rem; + font-weight: 700; + } +} + +.site-list__logo { + font-size: 1.3rem; +} + +.site-list__subtitle { + margin: 0.25rem 0 0; + color: var(--color-text-muted); +} + +.banner-error { + display: block; + margin: 0 0 1.5rem; +} + +.table-card { + padding: 0; + overflow: hidden; +} + +.sites-table { + width: 100%; + border-collapse: collapse; + + th, + td { + padding: 0.85rem 1.25rem; + text-align: left; + border-bottom: 1px solid var(--color-border-light); + } + + th { + font-size: 0.8rem; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.02em; + font-weight: 600; + } + + tr:last-child td { + border-bottom: none; + } +} + +.site-list__detail-link { + color: var(--color-primary); + font-weight: 600; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} diff --git a/apps/frontend/src/app/features/sites/site-list/site-list.spec.ts b/apps/frontend/src/app/features/sites/site-list/site-list.spec.ts new file mode 100644 index 0000000..56d8d6c --- /dev/null +++ b/apps/frontend/src/app/features/sites/site-list/site-list.spec.ts @@ -0,0 +1,81 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { vi } from 'vitest'; +import { of, throwError } from 'rxjs'; +import { SiteList } from './site-list'; +import { SitesService } from '../../../core/services/sites.service'; + +describe('SiteList', () => { + it('charge et affiche les sites au démarrage', () => { + const sitesMock = { + getSites: vi.fn().mockReturnValue( + of([ + { + site_id: 'SITE001', + site_name: 'Site 1', + site_type: 'industriel', + location: 'Nantes', + capacity_kw: 500, + status: 'actif', + }, + ]), + ), + }; + + TestBed.configureTestingModule({ + imports: [SiteList], + providers: [{ provide: SitesService, useValue: sitesMock }, provideRouter([])], + }); + + const fixture = TestBed.createComponent(SiteList); + fixture.detectChanges(); + + expect(sitesMock.getSites).toHaveBeenCalled(); + expect(fixture.componentInstance.sites().length).toBe(1); + expect(fixture.componentInstance.error()).toBeNull(); + }); + + it("signale l'indisponibilité quand le chargement échoue", () => { + const sitesMock = { getSites: vi.fn().mockReturnValue(throwError(() => new Error('nope'))) }; + + TestBed.configureTestingModule({ + imports: [SiteList], + providers: [{ provide: SitesService, useValue: sitesMock }, provideRouter([])], + }); + + const fixture = TestBed.createComponent(SiteList); + fixture.detectChanges(); + + expect(fixture.componentInstance.error()).not.toBeNull(); + expect(fixture.componentInstance.sites().length).toBe(0); + }); + + it('affiche un tiret pour les champs nullables', () => { + const sitesMock = { + getSites: vi.fn().mockReturnValue( + of([ + { + site_id: 'SITE002', + site_name: 'Site 2', + site_type: 'bureau', + location: null, + capacity_kw: null, + status: null, + }, + ]), + ), + }; + + TestBed.configureTestingModule({ + imports: [SiteList], + providers: [{ provide: SitesService, useValue: sitesMock }, provideRouter([])], + }); + + const fixture = TestBed.createComponent(SiteList); + fixture.detectChanges(); + + const cells = fixture.nativeElement.querySelectorAll('td'); + expect(cells[2].textContent.trim()).toBe('-'); + expect(cells[3].textContent.trim()).toBe('-'); + }); +}); diff --git a/apps/frontend/src/app/features/sites/site-list/site-list.ts b/apps/frontend/src/app/features/sites/site-list/site-list.ts new file mode 100644 index 0000000..30e9e4e --- /dev/null +++ b/apps/frontend/src/app/features/sites/site-list/site-list.ts @@ -0,0 +1,47 @@ +import { Component, OnInit, inject, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { catchError, EMPTY, Observable } from 'rxjs'; +import { SitesService } from '../../../core/services/sites.service'; +import { Site } from '../../../shared/models/site.model'; +import { Card } from '../../../shared/components/ui/card/card'; +import { Alert } from '../../../shared/components/ui/alert/alert'; +import { Badge, BadgeTone } from '../../../shared/components/ui/badge/badge'; +import { Brand } from '../../../shared/components/ui/brand/brand'; + +const UNAVAILABLE_MESSAGE = 'Liste des sites indisponible, réessayez plus tard.'; + +const TON_PAR_STATUT: Record = { + actif: 'success', + maintenance: 'warning', + hors_service: 'danger', +}; + +@Component({ + selector: 'app-site-list', + standalone: true, + imports: [RouterLink, Card, Alert, Badge, Brand], + templateUrl: './site-list.html', + styleUrl: './site-list.scss', +}) +export class SiteList implements OnInit { + private sitesService = inject(SitesService); + + sites = signal([]); + error = signal(null); + + ngOnInit(): void { + this.sitesService + .getSites() + .pipe(catchError(() => this.reportUnavailable())) + .subscribe((sites) => this.sites.set(sites)); + } + + badgeToneForStatus(status: string | null): BadgeTone { + return status ? (TON_PAR_STATUT[status] ?? 'neutral') : 'neutral'; + } + + private reportUnavailable(): Observable { + this.error.set(UNAVAILABLE_MESSAGE); + return EMPTY; + } +} diff --git a/apps/frontend/src/app/shared/models/site.model.ts b/apps/frontend/src/app/shared/models/site.model.ts new file mode 100644 index 0000000..fcf18f0 --- /dev/null +++ b/apps/frontend/src/app/shared/models/site.model.ts @@ -0,0 +1,8 @@ +export interface Site { + site_id: string; + site_name: string; + site_type: string; + location: string | null; + capacity_kw: number | null; + status: string | null; +} From e22feac2c4fb2ba5f6dc32f56e31fa9b1bc5213a Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Thu, 17 Sep 2026 12:05:06 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(frontend):=20navigation=20coh=C3=A9rent?= =?UTF-8?q?e=20entre=20les=20pages=20authentifi=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logo cliquable vers le tableau de bord (ev-brand-link) et fil d'Ariane (ev-breadcrumb) sur les sous-pages, pour éviter les impasses de navigation entre dashboard, liste des sites et détail de site. --- .../src/app/features/dashboard/dashboard.html | 6 ++-- .../src/app/features/dashboard/dashboard.scss | 10 ------ .../site-detail-placeholder.html | 16 ++++++++- .../site-detail-placeholder.scss | 22 ++++++++++++ .../site-detail-placeholder.spec.ts | 3 +- .../site-detail-placeholder.ts | 5 +-- .../features/sites/site-list/site-list.html | 8 ++++- apps/frontend/src/styles.scss | 1 + apps/frontend/src/styles/_links.scss | 34 +++++++++++++++++++ .../32-design-systeme-frontend.md | 7 ++++ 10 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 apps/frontend/src/styles/_links.scss diff --git a/apps/frontend/src/app/features/dashboard/dashboard.html b/apps/frontend/src/app/features/dashboard/dashboard.html index a626229..08b015e 100644 --- a/apps/frontend/src/app/features/dashboard/dashboard.html +++ b/apps/frontend/src/app/features/dashboard/dashboard.html @@ -1,14 +1,16 @@
-
- Voir les sites + Voir les sites + + +
+ + +

Site {{ siteId }}

+
+ -

Détail du site {{ siteId }} : à venir (voir issue #51).

+

Le détail de ce site est à venir (voir issue #51).

+ Retour aux sites
diff --git a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.scss b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.scss index b3874ae..ca04d4a 100644 --- a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.scss +++ b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.scss @@ -1,6 +1,28 @@ :host { display: block; + color: var(--color-text); padding: 2.5rem 2rem; max-width: 640px; margin: 0 auto; } + +.site-detail-placeholder__header { + display: flex; + align-items: center; + gap: 0.85rem; + margin-bottom: 1.5rem; + + h1 { + margin: 0; + font-size: 1.5rem; + font-weight: 700; + } +} + +.site-detail-placeholder__logo { + font-size: 1.3rem; +} + +ev-card p { + margin: 0 0 0.75rem; +} diff --git a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts index 386a686..ecf28c1 100644 --- a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts +++ b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing'; -import { ActivatedRoute, convertToParamMap } from '@angular/router'; +import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router'; import { SiteDetailPlaceholder } from './site-detail-placeholder'; describe('SiteDetailPlaceholder', () => { @@ -7,6 +7,7 @@ describe('SiteDetailPlaceholder', () => { TestBed.configureTestingModule({ imports: [SiteDetailPlaceholder], providers: [ + provideRouter([]), { provide: ActivatedRoute, useValue: { snapshot: { paramMap: convertToParamMap({ siteId: 'SITE001' }) } }, diff --git a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts index 720557b..2474a7d 100644 --- a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts +++ b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts @@ -1,11 +1,12 @@ import { Component, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, RouterLink } from '@angular/router'; import { Card } from '../../../shared/components/ui/card/card'; +import { Brand } from '../../../shared/components/ui/brand/brand'; @Component({ selector: 'app-site-detail-placeholder', standalone: true, - imports: [Card], + imports: [RouterLink, Card, Brand], templateUrl: './site-detail-placeholder.html', styleUrl: './site-detail-placeholder.scss', }) diff --git a/apps/frontend/src/app/features/sites/site-list/site-list.html b/apps/frontend/src/app/features/sites/site-list/site-list.html index 509c08d..f53b506 100644 --- a/apps/frontend/src/app/features/sites/site-list/site-list.html +++ b/apps/frontend/src/app/features/sites/site-list/site-list.html @@ -1,6 +1,12 @@
+ +
-
diff --git a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts index ecf28c1..f229e36 100644 --- a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts +++ b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.spec.ts @@ -1,17 +1,16 @@ import { TestBed } from '@angular/core/testing'; import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; import { SiteDetailPlaceholder } from './site-detail-placeholder'; describe('SiteDetailPlaceholder', () => { it("affiche l'identifiant du site depuis la route", () => { + const paramMap = new BehaviorSubject(convertToParamMap({ siteId: 'SITE001' })); TestBed.configureTestingModule({ imports: [SiteDetailPlaceholder], providers: [ provideRouter([]), - { - provide: ActivatedRoute, - useValue: { snapshot: { paramMap: convertToParamMap({ siteId: 'SITE001' }) } }, - }, + { provide: ActivatedRoute, useValue: { paramMap } }, ], }); @@ -20,4 +19,24 @@ describe('SiteDetailPlaceholder', () => { expect(fixture.nativeElement.textContent).toContain('SITE001'); }); + + it('met à jour l\'affichage quand le paramètre change sans recréer le composant', () => { + const paramMap = new BehaviorSubject(convertToParamMap({ siteId: 'SITE001' })); + TestBed.configureTestingModule({ + imports: [SiteDetailPlaceholder], + providers: [ + provideRouter([]), + { provide: ActivatedRoute, useValue: { paramMap } }, + ], + }); + + const fixture = TestBed.createComponent(SiteDetailPlaceholder); + fixture.detectChanges(); + + paramMap.next(convertToParamMap({ siteId: 'SITE002' })); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain('SITE002'); + expect(fixture.nativeElement.textContent).not.toContain('SITE001'); + }); }); diff --git a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts index 2474a7d..39e53dc 100644 --- a/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts +++ b/apps/frontend/src/app/features/sites/site-detail-placeholder/site-detail-placeholder.ts @@ -1,5 +1,7 @@ import { Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, RouterLink } from '@angular/router'; +import { map } from 'rxjs'; import { Card } from '../../../shared/components/ui/card/card'; import { Brand } from '../../../shared/components/ui/brand/brand'; @@ -13,5 +15,5 @@ import { Brand } from '../../../shared/components/ui/brand/brand'; export class SiteDetailPlaceholder { private route = inject(ActivatedRoute); - siteId = this.route.snapshot.paramMap.get('siteId'); + siteId = toSignal(this.route.paramMap.pipe(map((params) => params.get('siteId')))); } diff --git a/apps/frontend/src/app/features/sites/site-list/site-list.html b/apps/frontend/src/app/features/sites/site-list/site-list.html index f53b506..9998066 100644 --- a/apps/frontend/src/app/features/sites/site-list/site-list.html +++ b/apps/frontend/src/app/features/sites/site-list/site-list.html @@ -34,10 +34,10 @@ {{ site.site_name }} {{ site.site_type }} - {{ site.location ?? '-' }} + {{ site.location || '-' }} {{ site.capacity_kw ?? '-' }} {{ site.status ?? '-' }} - Détail + Détail } diff --git a/apps/frontend/src/app/features/sites/site-list/site-list.scss b/apps/frontend/src/app/features/sites/site-list/site-list.scss index d253de5..9fa25a2 100644 --- a/apps/frontend/src/app/features/sites/site-list/site-list.scss +++ b/apps/frontend/src/app/features/sites/site-list/site-list.scss @@ -61,13 +61,3 @@ border-bottom: none; } } - -.site-list__detail-link { - color: var(--color-primary); - font-weight: 600; - text-decoration: none; - - &:hover { - text-decoration: underline; - } -}