From 27a38366c372b3abf7a4c1a8ceb31cb47d155eaa Mon Sep 17 00:00:00 2001 From: khalil-bot Date: Wed, 15 Apr 2026 19:30:32 +0200 Subject: [PATCH] feat(ui): implement interactive room map with REST services and unit tests - Add SensorReading model with CO2, temperature, humidity, window state, timestamp - Add RoomService and SensorService with HTTP calls and mock data fallback - Rewrite RoomMapComponent: SVG floor plan, CO2-based coloring, tooltips, navigation - Rewrite RoomDetailsPanelComponent: sensor history table with pagination (8 rows/page) - Add FooterComponent with current year, fix header/footer sticky layout - Update LegendComponent template and styles for two-line item layout - Add provideHttpClient() to app.config.ts - Add getCO2Color() and getCO2Level() helpers to co2-levels.config.ts - Add 85 unit tests across all components, services, and config helpers Closes #28 Closes #29 --- ui/src/app/app.component.html | 1 + ui/src/app/app.component.scss | 11 +- ui/src/app/app.component.spec.ts | 7 + ui/src/app/app.component.ts | 3 +- ui/src/app/app.config.ts | 2 + .../components/footer/footer.component.html | 6 + .../components/footer/footer.component.scss | 28 +++ .../footer/footer.component.spec.ts | 38 ++++ .../app/components/footer/footer.component.ts | 12 ++ .../components/header/header.component.scss | 4 +- .../header/header.component.spec.ts | 43 ++++ .../components/legend/legend.component.html | 6 +- .../components/legend/legend.component.scss | 23 +- .../legend/legend.component.spec.ts | 55 +++++ .../room-details-panel.component.html | 105 ++++++++- .../room-details-panel.component.scss | 202 +++++++++++++++++- .../room-details-panel.component.spec.ts | 174 +++++++++++++++ .../room-details-panel.component.ts | 64 +++++- .../room-map/room-map.component.html | 94 +++++++- .../room-map/room-map.component.scss | 165 +++++++++++++- .../room-map/room-map.component.spec.ts | 107 ++++++++++ .../components/room-map/room-map.component.ts | 65 +++++- ui/src/app/config/co2-levels.config.spec.ts | 82 +++++++ ui/src/app/config/co2-levels.config.ts | 21 +- ui/src/app/models/sensor-reading.model.ts | 11 + ui/src/app/services/room.service.spec.ts | 90 ++++++++ ui/src/app/services/room.service.ts | 25 +++ ui/src/app/services/sensor.service.spec.ts | 170 +++++++++++++++ ui/src/app/services/sensor.service.ts | 45 ++++ ui/src/styles.scss | 8 + 30 files changed, 1625 insertions(+), 42 deletions(-) create mode 100644 ui/src/app/components/footer/footer.component.html create mode 100644 ui/src/app/components/footer/footer.component.scss create mode 100644 ui/src/app/components/footer/footer.component.spec.ts create mode 100644 ui/src/app/components/footer/footer.component.ts create mode 100644 ui/src/app/components/header/header.component.spec.ts create mode 100644 ui/src/app/components/legend/legend.component.spec.ts create mode 100644 ui/src/app/components/room-details-panel/room-details-panel.component.spec.ts create mode 100644 ui/src/app/components/room-map/room-map.component.spec.ts create mode 100644 ui/src/app/config/co2-levels.config.spec.ts create mode 100644 ui/src/app/models/sensor-reading.model.ts create mode 100644 ui/src/app/services/room.service.spec.ts create mode 100644 ui/src/app/services/room.service.ts create mode 100644 ui/src/app/services/sensor.service.spec.ts create mode 100644 ui/src/app/services/sensor.service.ts diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 0791e0c..7edd313 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -2,3 +2,4 @@
+ diff --git a/ui/src/app/app.component.scss b/ui/src/app/app.component.scss index a1d32de..1c34f52 100644 --- a/ui/src/app/app.component.scss +++ b/ui/src/app/app.component.scss @@ -1,4 +1,13 @@ +:host { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + main { - min-height: calc(100vh - 64px); + flex: 1; + overflow-y: auto; + overflow-x: hidden; background: #fafafa; } diff --git a/ui/src/app/app.component.spec.ts b/ui/src/app/app.component.spec.ts index f495654..a79495a 100644 --- a/ui/src/app/app.component.spec.ts +++ b/ui/src/app/app.component.spec.ts @@ -29,4 +29,11 @@ describe('AppComponent', () => { const compiled = fixture.nativeElement as HTMLElement; expect(compiled.querySelector('app-header')).toBeTruthy(); }); + + it('should render footer', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('app-footer')).toBeTruthy(); + }); }); diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index d02beed..8c546b7 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,11 +1,12 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { HeaderComponent } from './components/header/header.component'; +import { FooterComponent } from './components/footer/footer.component'; @Component({ selector: 'app-root', standalone: true, - imports: [RouterOutlet, HeaderComponent], + imports: [RouterOutlet, HeaderComponent, FooterComponent], templateUrl: './app.component.html', styleUrl: './app.component.scss', }) diff --git a/ui/src/app/app.config.ts b/ui/src/app/app.config.ts index ec69408..fff2d5f 100644 --- a/ui/src/app/app.config.ts +++ b/ui/src/app/app.config.ts @@ -1,5 +1,6 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; import { routes } from './app.routes'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; @@ -9,5 +10,6 @@ export const appConfig: ApplicationConfig = { provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), + provideHttpClient(), ], }; diff --git a/ui/src/app/components/footer/footer.component.html b/ui/src/app/components/footer/footer.component.html new file mode 100644 index 0000000..4821060 --- /dev/null +++ b/ui/src/app/components/footer/footer.component.html @@ -0,0 +1,6 @@ + diff --git a/ui/src/app/components/footer/footer.component.scss b/ui/src/app/components/footer/footer.component.scss new file mode 100644 index 0000000..2296b10 --- /dev/null +++ b/ui/src/app/components/footer/footer.component.scss @@ -0,0 +1,28 @@ +footer { + background: #263238; + color: #90a4ae; + font-size: 13px; + flex-shrink: 0; + z-index: 100; + box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.15); +} + +.footer-content { + max-width: 1200px; + margin: 0 auto; + padding: 14px 24px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.project { + color: #cfd8dc; + font-weight: 500; +} + +.credits { + color: #78909c; +} diff --git a/ui/src/app/components/footer/footer.component.spec.ts b/ui/src/app/components/footer/footer.component.spec.ts new file mode 100644 index 0000000..db64482 --- /dev/null +++ b/ui/src/app/components/footer/footer.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FooterComponent } from './footer.component'; + +describe('FooterComponent', () => { + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FooterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + fixture.detectChanges(); + compiled = fixture.nativeElement as HTMLElement; + }); + + it('should create', () => { + expect(fixture.componentInstance).toBeTruthy(); + }); + + it('should render a