From b6c0c509686bbee929b070b2c2c22513f9134f4e Mon Sep 17 00:00:00 2001 From: sebastian Date: Wed, 1 Jul 2026 19:35:50 +0200 Subject: [PATCH] ADD: Records --- src/app/api/.openapi-generator/FILES | 3 + src/app/api/api/default.service.ts | 77 ++++++++++++ src/app/api/model/measurementRecord.ts | 17 +++ src/app/api/model/measurementRecordItem.ts | 18 +++ src/app/api/model/models.ts | 3 + src/app/api/model/recordResponse.ts | 21 ++++ src/app/app.html | 1 + src/app/app.ts | 3 +- .../records/record-card/record-card.css | 72 +++++++++++ .../records/record-card/record-card.html | 22 ++++ .../records/record-card/record-card.spec.ts | 22 ++++ .../records/record-card/record-card.ts | 113 ++++++++++++++++++ src/app/components/records/records.css | 6 + src/app/components/records/records.html | 18 +++ src/app/components/records/records.spec.ts | 22 ++++ src/app/components/records/records.ts | 73 +++++++++++ 16 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 src/app/api/model/measurementRecord.ts create mode 100644 src/app/api/model/measurementRecordItem.ts create mode 100644 src/app/api/model/recordResponse.ts create mode 100644 src/app/components/records/record-card/record-card.css create mode 100644 src/app/components/records/record-card/record-card.html create mode 100644 src/app/components/records/record-card/record-card.spec.ts create mode 100644 src/app/components/records/record-card/record-card.ts create mode 100644 src/app/components/records/records.css create mode 100644 src/app/components/records/records.html create mode 100644 src/app/components/records/records.spec.ts create mode 100644 src/app/components/records/records.ts diff --git a/src/app/api/.openapi-generator/FILES b/src/app/api/.openapi-generator/FILES index eac0da3..171646f 100644 --- a/src/app/api/.openapi-generator/FILES +++ b/src/app/api/.openapi-generator/FILES @@ -12,10 +12,13 @@ model/hTTPValidationError.ts model/indoorMeasurementCreateRequest.ts model/locationInner.ts model/measurementListResponse.ts +model/measurementRecord.ts +model/measurementRecordItem.ts model/measurementResolution.ts model/measurementResponse.ts model/models.ts model/outdoorMeasurementCreateRequest.ts +model/recordResponse.ts model/stationCreateRequest.ts model/stationCreateResponse.ts model/stationListResponse.ts diff --git a/src/app/api/api/default.service.ts b/src/app/api/api/default.service.ts index b035bc9..4d839fd 100644 --- a/src/app/api/api/default.service.ts +++ b/src/app/api/api/default.service.ts @@ -27,6 +27,8 @@ import { MeasurementResolution } from '../model/measurementResolution'; // @ts-ignore import { OutdoorMeasurementCreateRequest } from '../model/outdoorMeasurementCreateRequest'; // @ts-ignore +import { RecordResponse } from '../model/recordResponse'; +// @ts-ignore import { StationCreateRequest } from '../model/stationCreateRequest'; // @ts-ignore import { StationCreateResponse } from '../model/stationCreateResponse'; @@ -422,6 +424,81 @@ export class DefaultService extends BaseService { ); } + /** + * Get Records + * @endpoint get /measurements/records + * @param stationIds + * @param fromTimestamp + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public getRecordsMeasurementsRecordsGet(stationIds?: Array, fromTimestamp?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getRecordsMeasurementsRecordsGet(stationIds?: Array, fromTimestamp?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getRecordsMeasurementsRecordsGet(stationIds?: Array, fromTimestamp?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getRecordsMeasurementsRecordsGet(stationIds?: Array, fromTimestamp?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarQueryParameters = new OpenApiHttpParams(this.encoder); + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'station_ids', + stationIds, + QueryParamStyle.Form, + true, + ); + + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'from_timestamp', + fromTimestamp, + QueryParamStyle.Form, + true, + ); + + + let localVarHeaders = this.defaultHeaders; + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/measurements/records`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + params: localVarQueryParameters.toHttpParams(), + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + /** * List Stations * @endpoint get /stations/list diff --git a/src/app/api/model/measurementRecord.ts b/src/app/api/model/measurementRecord.ts new file mode 100644 index 0000000..56485e8 --- /dev/null +++ b/src/app/api/model/measurementRecord.ts @@ -0,0 +1,17 @@ +/** + * FastAPI + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { MeasurementRecordItem } from './measurementRecordItem'; + + +export interface MeasurementRecord { + min_value: MeasurementRecordItem; + max_value: MeasurementRecordItem; +} + diff --git a/src/app/api/model/measurementRecordItem.ts b/src/app/api/model/measurementRecordItem.ts new file mode 100644 index 0000000..6fcc9de --- /dev/null +++ b/src/app/api/model/measurementRecordItem.ts @@ -0,0 +1,18 @@ +/** + * FastAPI + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { StationListResponse } from './stationListResponse'; + + +export interface MeasurementRecordItem { + recordValue: number; + timestamp: string; + station: StationListResponse; +} + diff --git a/src/app/api/model/models.ts b/src/app/api/model/models.ts index a5c9206..1a4aad4 100644 --- a/src/app/api/model/models.ts +++ b/src/app/api/model/models.ts @@ -2,9 +2,12 @@ export * from './hTTPValidationError'; export * from './indoorMeasurementCreateRequest'; export * from './locationInner'; export * from './measurementListResponse'; +export * from './measurementRecord'; +export * from './measurementRecordItem'; export * from './measurementResolution'; export * from './measurementResponse'; export * from './outdoorMeasurementCreateRequest'; +export * from './recordResponse'; export * from './stationCreateRequest'; export * from './stationCreateResponse'; export * from './stationListResponse'; diff --git a/src/app/api/model/recordResponse.ts b/src/app/api/model/recordResponse.ts new file mode 100644 index 0000000..7c6c29c --- /dev/null +++ b/src/app/api/model/recordResponse.ts @@ -0,0 +1,21 @@ +/** + * FastAPI + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { MeasurementRecord } from './measurementRecord'; + + +export interface RecordResponse { + inner_temperature_record: MeasurementRecord; + outer_temperature_record: MeasurementRecord; + inner_humidity_record: MeasurementRecord; + outer_humidity_record: MeasurementRecord; + pressure_record: MeasurementRecord; + night_temperature_record: MeasurementRecord; +} + diff --git a/src/app/app.html b/src/app/app.html index f2f6c60..7f6f156 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -28,6 +28,7 @@ + diff --git a/src/app/app.ts b/src/app/app.ts index 19a876f..efbdcca 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -8,10 +8,11 @@ import {StationList} from './station-list/station-list'; import {WettterOverview} from './components/wettter-overview/wettter-overview'; import {TemperatureComponent, TimeResolution} from './components/diagrams/temperature/temperature'; import {Diagrams} from './components/diagrams/diagrams'; +import {Records} from './components/records/records'; @Component({ selector: 'app-root', - imports: [RouterOutlet, MatToolbar, MatIcon, MatIconButton, RouterLink, MatButton, MatDrawerContainer, MatDrawer, MatDrawerContent, StationList, WettterOverview, TemperatureComponent, Diagrams], + imports: [RouterOutlet, MatToolbar, MatIcon, MatIconButton, RouterLink, MatButton, MatDrawerContainer, MatDrawer, MatDrawerContent, StationList, WettterOverview, Diagrams, Records], templateUrl: './app.html', styleUrl: './app.css' }) diff --git a/src/app/components/records/record-card/record-card.css b/src/app/components/records/record-card/record-card.css new file mode 100644 index 0000000..8a1aada --- /dev/null +++ b/src/app/components/records/record-card/record-card.css @@ -0,0 +1,72 @@ +mat-card { + background: #1e1e1e !important; + color: #fff; + border-radius: 14px !important; + padding: 0 !important; + box-shadow: + 0 4px 10px rgba(0, 0, 0, .35), + inset 0 1px 0 rgba(255,255,255,.04) !important; + transition: all .2s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: + 0 8px 18px rgba(0,0,0,.45), + inset 0 1px 0 rgba(255,255,255,.05) !important; + } +} + +mat-card-content { + padding: 18px !important; + display: flex; + flex-direction: column; + gap: 4px; + + .record-icon { + font-size: 28px; + width: 28px; + height: 28px; + margin-bottom: 4px; + } + + .record-value { + font-size: 1.8rem; + font-weight: 500; + line-height: 1.1; + margin: 4px 0; + } + + .record-date { + font-size: 0.78rem; + opacity: 0.6; + } + + .record-station { + display: flex; + align-items: center; + gap: 6px; + margin-top: 6px; + font-size: 0.82rem; + + .dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; + + &.orange { background: #eda100; } + &.blue { background: #2a78d6; } + &.purple { background: #4a3aa7; } + } + } +} + +.record-label { + opacity: 0.6; +} + +.max-record { color: #e34948; } +.min-record { color: #2a78d6; } +.green-record { color: #25a532; } +.night-record { color: #7300ed; } +.white-record { color: #ffffff; } diff --git a/src/app/components/records/record-card/record-card.html b/src/app/components/records/record-card/record-card.html new file mode 100644 index 0000000..1723d34 --- /dev/null +++ b/src/app/components/records/record-card/record-card.html @@ -0,0 +1,22 @@ + + + + @if(measurementType() === 'temperature') { + @if(recordType() === 'max') { + thermometer + } @else { + snowflake + } + } + {{recordLabel}} + + + +

{{ recordValue }}{{recordUnit}}

+

{{ recordTimeStamp | date: 'dd.MM.yyyy HH:mm' }}

+
+ + {{ record().max_value.station.name }} +
+
+
diff --git a/src/app/components/records/record-card/record-card.spec.ts b/src/app/components/records/record-card/record-card.spec.ts new file mode 100644 index 0000000..e9d3ac9 --- /dev/null +++ b/src/app/components/records/record-card/record-card.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RecordCard } from './record-card'; + +describe('RecordCard', () => { + let component: RecordCard; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RecordCard], + }).compileComponents(); + + fixture = TestBed.createComponent(RecordCard); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/records/record-card/record-card.ts b/src/app/components/records/record-card/record-card.ts new file mode 100644 index 0000000..87f8446 --- /dev/null +++ b/src/app/components/records/record-card/record-card.ts @@ -0,0 +1,113 @@ +import {ChangeDetectorRef, Component, input, Input, OnInit} from '@angular/core'; +import {MeasurementRecord, RecordResponse} from '../../../api'; +import {MatIcon} from '@angular/material/icon'; +import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; +import {DatePipe, NgClass} from '@angular/common'; + +@Component({ + selector: 'app-record-card', + imports: [ + MatIcon, + MatCard, + MatCardHeader, + MatCardTitle, + MatCardContent, + DatePipe, + NgClass + ], + templateUrl: './record-card.html', + styleUrl: './record-card.css', +}) +export class RecordCard { + recordType = input.required<'min' | 'max'>(); + record = input.required(); + measurementType = input.required<'temperature' | 'humidity' | 'pressure'>(); + variant = input.required<'red-blue' | 'green' | 'white' | 'night'>() + predifenedRecordLabel = input() + + get recordIconClass() { + if(this.variant() === 'red-blue') { + if(this.recordType() === 'min') { + return 'record-icon min-record' + } else { + return 'record-icon max-record'; + } + } else if(this.variant() === 'green') { + return 'record-icon green-record'; + } else if(this.variant() === 'white') { + return 'record-icon white-record'; + } else if(this.variant() === 'night') { + return 'record-icon night-record'; + } + return "" + + } + + get recordValueClass() { + if(this.variant() === 'red-blue') { + if(this.recordType() === 'min') { + return 'record-value min-record' + } else { + return 'record-value max-record'; + } + } else if(this.variant() === 'green') { + return 'record-value green-record'; + } else if(this.variant() === 'white') { + return 'record-value white-record'; + } else if(this.variant() === 'night') { + return 'record-value night-record'; + } + return "" + } + + get recordLabel() { + if(this.predifenedRecordLabel()) { + return this.predifenedRecordLabel(); + } + + let recordType = '' + if(this.recordType() === 'max') { + recordType = 'Höchste' + } else { + recordType = 'Niedrigste' + } + + let measurementType = '' + if(this.measurementType() === 'humidity') { + measurementType = 'Luftfeuchtigkeit' + } else if(this.measurementType() === 'pressure') { + measurementType = 'Luftdruck' + } else { + measurementType = 'Temperatur' + } + + return recordType + ' ' + measurementType + } + + get recordUnit() { + let unit = '' + if(this.measurementType() === 'humidity') { + return '%' + } else if(this.measurementType() === 'pressure') { + return 'hPa' + } else { + return '°C' + } + } + + get recordValue() { + if(this.recordType() === 'min') { + return this.record().min_value.recordValue + } else { + return this.record().max_value.recordValue + } + } + + get recordTimeStamp() { + if(this.recordType() === 'min') { + return this.record().min_value.timestamp + } else { + return this.record().max_value.timestamp + } + } +} diff --git a/src/app/components/records/records.css b/src/app/components/records/records.css new file mode 100644 index 0000000..74c7958 --- /dev/null +++ b/src/app/components/records/records.css @@ -0,0 +1,6 @@ +.records-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin: 2em; +} diff --git a/src/app/components/records/records.html b/src/app/components/records/records.html new file mode 100644 index 0000000..e10376f --- /dev/null +++ b/src/app/components/records/records.html @@ -0,0 +1,18 @@ +
+ @if(unifiedTemperatureRecord) { + + + } + @if(records) { + + + + + } +
diff --git a/src/app/components/records/records.spec.ts b/src/app/components/records/records.spec.ts new file mode 100644 index 0000000..c7ad483 --- /dev/null +++ b/src/app/components/records/records.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Records } from './records'; + +describe('Records', () => { + let component: Records; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Records], + }).compileComponents(); + + fixture = TestBed.createComponent(Records); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/records/records.ts b/src/app/components/records/records.ts new file mode 100644 index 0000000..fd6d5c2 --- /dev/null +++ b/src/app/components/records/records.ts @@ -0,0 +1,73 @@ +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit} from '@angular/core'; +import {DefaultService, MeasurementRecord, MeasurementRecordItem, RecordResponse} from '../../api'; +import {addYears, startOfMonth, startOfWeek, startOfYear} from 'date-fns'; +import {RecordCard} from './record-card/record-card'; + +export type RecordTimeInterval = 'All' | 'Year' | 'Month' | 'Week' + +@Component({ + selector: 'app-records', + imports: [ + RecordCard + ], + templateUrl: './records.html', + styleUrl: './records.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class Records implements OnInit { + timeInterval: RecordTimeInterval = 'All'; + station_ids: number[] = []; + + records: RecordResponse | null = null; + + unifiedTemperatureRecord: MeasurementRecord | null = null; + + constructor(private apiService: DefaultService, private cdr: ChangeDetectorRef) { + } + + ngOnInit() { + let from_timestamp : undefined | string = undefined + if (this.timeInterval === 'All') { + from_timestamp = undefined + } else if(this.timeInterval === 'Year') { + const currentDate = new Date(); + from_timestamp = startOfYear(currentDate).toISOString() + } else if(this.timeInterval === 'Month') { + const currentDate = new Date(); + from_timestamp = startOfMonth(currentDate).toISOString() + } else { + const currentDate = new Date(); + from_timestamp = startOfWeek(currentDate, { weekStartsOn: 1}).toISOString() + } + + this.apiService.getRecordsMeasurementsRecordsGet(undefined, from_timestamp).subscribe( + { + next: records => { + this.records = records; + this.unifiedTemperatureRecord = { + min_value: this.getLowestMeasurement(records.inner_temperature_record, records.outer_temperature_record).min_value, + max_value: this.getHighestMeasurement(records.inner_temperature_record, records.outer_temperature_record).max_value, + } + console.log(this.unifiedTemperatureRecord) + this.cdr.detectChanges() + } + } + ) + } + + getHighestMeasurement(a: MeasurementRecord, b: MeasurementRecord) { + if(a.max_value > b.max_value) { + return a + } else { + return b + } + } + + getLowestMeasurement(a: MeasurementRecord, b: MeasurementRecord) { + if(a.min_value > b.min_value) { + return a + } else { + return b + } + } +}