From 32155cbfc5c801399fc546c39709c608b4db2034 Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Mon, 21 Sep 2026 14:41:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20section=20recommandations=20s?= =?UTF-8?q?ur=20la=20vue=20d=C3=A9tail=20d'un=20site?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La vue détail pose app-recommendation-list restreint au site consulté (les alertes sont demandées avec site_id) et renvoie vers la vue complète préfiltrée sur ce site. Le spec fournit les deux services du composant enfant et vérifie le filtre transmis. --- .../sites/site-detail/site-detail.html | 11 +++++++ .../sites/site-detail/site-detail.scss | 9 ++++++ .../sites/site-detail/site-detail.spec.ts | 31 ++++++++++++++++++- .../features/sites/site-detail/site-detail.ts | 17 +++++++++- 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/app/features/sites/site-detail/site-detail.html b/apps/frontend/src/app/features/sites/site-detail/site-detail.html index 9c4a4bc..e10d1db 100644 --- a/apps/frontend/src/app/features/sites/site-detail/site-detail.html +++ b/apps/frontend/src/app/features/sites/site-detail/site-detail.html @@ -81,5 +81,16 @@ } } +
+

Recommandations

+ + Voir dans la vue recommandations +
+ Retour aux sites diff --git a/apps/frontend/src/app/features/sites/site-detail/site-detail.scss b/apps/frontend/src/app/features/sites/site-detail/site-detail.scss index a0032be..5cfcad0 100644 --- a/apps/frontend/src/app/features/sites/site-detail/site-detail.scss +++ b/apps/frontend/src/app/features/sites/site-detail/site-detail.scss @@ -117,3 +117,12 @@ h2 { .chart-section { margin-bottom: 2rem; } + +.recommendations-section { + margin: 2.5rem 0 1.5rem; +} + +.recommendations-section__link { + display: inline-block; + margin-top: 1rem; +} diff --git a/apps/frontend/src/app/features/sites/site-detail/site-detail.spec.ts b/apps/frontend/src/app/features/sites/site-detail/site-detail.spec.ts index 5e3cf46..0e7c35a 100644 --- a/apps/frontend/src/app/features/sites/site-detail/site-detail.spec.ts +++ b/apps/frontend/src/app/features/sites/site-detail/site-detail.spec.ts @@ -5,6 +5,8 @@ import { BehaviorSubject, of, throwError } from 'rxjs'; import { SiteDetail } from './site-detail'; import { SitesService } from '../../../core/services/sites.service'; import { ReadingsService } from '../../../core/services/readings.service'; +import { AlertsService } from '../../../core/services/alerts.service'; +import { RecommendationsService } from '../../../core/services/recommendations.service'; const SITE = { site_id: 'SITE001', @@ -69,6 +71,7 @@ function setup( readingsMock: Partial, ) { const paramMap = new BehaviorSubject(convertToParamMap({ siteId })); + const getAlerts = vi.fn().mockReturnValue(of([])); TestBed.configureTestingModule({ imports: [SiteDetail], providers: [ @@ -76,9 +79,14 @@ function setup( { provide: ActivatedRoute, useValue: { paramMap } }, { provide: SitesService, useValue: sitesMock }, { provide: ReadingsService, useValue: readingsMock }, + { provide: AlertsService, useValue: { getAlerts } }, + { + provide: RecommendationsService, + useValue: { getRecommendations: vi.fn().mockReturnValue(of([])) }, + }, ], }); - return { fixture: TestBed.createComponent(SiteDetail), paramMap }; + return { fixture: TestBed.createComponent(SiteDetail), paramMap, getAlerts }; } describe('SiteDetail', () => { @@ -261,6 +269,27 @@ describe('SiteDetail', () => { ); }); + it('demande les recommandations du site consulté à travers ses alertes', () => { + const { fixture, getAlerts } = setup( + 'SITE001', + { + getSite: vi.fn().mockReturnValue(of(SITE)), + getCurrent: vi.fn().mockReturnValue(of(CURRENT_COMPLET)), + }, + { getHistory: vi.fn().mockReturnValue(of([])) }, + ); + + fixture.detectChanges(); + fixture.detectChanges(); + + expect(getAlerts).toHaveBeenCalledWith({ site_id: 'SITE001' }); + expect(fixture.nativeElement.querySelector('app-recommendation-list')).not.toBeNull(); + expect(fixture.nativeElement.textContent).toContain('Recommandations'); + expect( + fixture.nativeElement.querySelector('a[href="/recommendations?site=SITE001"]'), + ).not.toBeNull(); + }); + it("annonce l'absence de mesure sans interroger l'historique quand timestamp est null", () => { const getHistory = vi.fn().mockReturnValue(of([])); const { fixture } = setup( diff --git a/apps/frontend/src/app/features/sites/site-detail/site-detail.ts b/apps/frontend/src/app/features/sites/site-detail/site-detail.ts index 21e716d..4fa7896 100644 --- a/apps/frontend/src/app/features/sites/site-detail/site-detail.ts +++ b/apps/frontend/src/app/features/sites/site-detail/site-detail.ts @@ -13,6 +13,7 @@ import { Badge, BadgeTone } from '../../../shared/components/ui/badge/badge'; import { Brand } from '../../../shared/components/ui/brand/brand'; import { ConsumptionGauge } from '../../../shared/components/consumption-gauge/consumption-gauge'; import { ReadingHistoryChart } from '../../../shared/components/reading-history-chart/reading-history-chart'; +import { RecommendationList } from '../../../shared/components/recommendation-list/recommendation-list'; const UNAVAILABLE_MESSAGE = 'Détail du site indisponible, réessayez plus tard.'; const NO_MEASUREMENT_MESSAGE = 'Aucune mesure remontée pour ce site.'; @@ -96,7 +97,16 @@ export interface MetricView { @Component({ selector: 'app-site-detail', standalone: true, - imports: [RouterLink, Card, Alert, Badge, Brand, ConsumptionGauge, ReadingHistoryChart], + imports: [ + RouterLink, + Card, + Alert, + Badge, + Brand, + ConsumptionGauge, + ReadingHistoryChart, + RecommendationList, + ], templateUrl: './site-detail.html', styleUrl: './site-detail.scss', }) @@ -117,6 +127,11 @@ export class SiteDetail { hasMeasurement = computed(() => this.current()?.timestamp != null); + siteAsList = computed(() => { + const site = this.site(); + return site ? [site] : []; + }); + consumptionKw = computed(() => this.current()?.consumption_kw ?? null); consumptionLabel = computed(() => {