location-detail

This commit is contained in:
Patrick Haßel 2025-10-29 12:02:09 +01:00
parent 2ad140589c
commit 3509d8ab41
8 changed files with 240 additions and 44 deletions

View File

@ -1,11 +1,123 @@
@if (location) {
<app-text [initial]="location.name" (onChange)="locationService.name(location, $event, update)"></app-text>
<app-number [initial]="location.latitude" (onChange)="locationService.latitude(location, $event, update)" unit="°"></app-number>
<app-number [initial]="location.longitude" (onChange)="locationService.longitude(location, $event, update)" unit="°"></app-number>
<app-series-select [initial]="location.energyPurchase" (onChange)="locationService.energyPurchase(location, $event, update)" [list]="filterEnergy()"></app-series-select>
<app-series-select [initial]="location.energyDeliver" (onChange)="locationService.energyDeliver(location, $event, update)" [list]="filterEnergy()"></app-series-select>
<app-series-select [initial]="location.energyProduce" (onChange)="locationService.energyProduce(location, $event, update)" [list]="filterEnergy()"></app-series-select>
<app-series-select [initial]="location.powerPurchase" (onChange)="locationService.powerPurchase(location, $event, update)" [list]="filterPower()"></app-series-select>
<app-series-select [initial]="location.powerDeliver" (onChange)="locationService.powerDeliver(location, $event, update)" [list]="filterPower()"></app-series-select>
<app-series-select [initial]="location.powerProduce" (onChange)="locationService.powerProduce(location, $event, update)" [list]="filterPower()"></app-series-select>
<div class="Section">
<div class="SectionHeading">
<div class="SectionHeadingText">
Ort
</div>
</div>
<div class="SectionBody">
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Name:
</div>
</div>
<div class="SectionBody">
<app-text [initial]="location.name" (onChange)="locationService.name(location, $event, updateLocation)"></app-text>
</div>
</div>
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Breitegrad:
</div>
</div>
<div class="SectionBody">
<app-number [initial]="location.latitude" (onChange)="locationService.latitude(location, $event, updateLocation)" unit="°"></app-number>
</div>
</div>
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Längengrad:
</div>
</div>
<div class="SectionBody">
<app-number [initial]="location.longitude" (onChange)="locationService.longitude(location, $event, updateLocation)" unit="°"></app-number>
</div>
</div>
</div>
</div>
<div class="Section">
<div class="SectionHeading">
<div class="SectionHeadingText">
Energie
</div>
</div>
<div class="SectionBody">
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Bezug:
</div>
</div>
<div class="SectionBody">
<app-series-select [now]="now" [initial]="location.energyPurchase" (onChange)="locationService.energyPurchase(location, $event, updateLocation)" [series]="filterEnergy()"></app-series-select>
</div>
</div>
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Einspeisung:
</div>
</div>
<div class="SectionBody">
<app-series-select [now]="now" [initial]="location.energyDeliver" (onChange)="locationService.energyDeliver(location, $event, updateLocation)" [series]="filterEnergy()"></app-series-select>
</div>
</div>
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Erzeugung:
</div>
</div>
<div class="SectionBody">
<app-series-select [now]="now" [initial]="location.energyProduce" (onChange)="locationService.energyProduce(location, $event, updateLocation)" [series]="filterEnergy()"></app-series-select>
</div>
</div>
</div>
</div>
<div class="Section">
<div class="SectionHeading">
<div class="SectionHeadingText">
Leistung
</div>
</div>
<div class="SectionBody">
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Bezug:
</div>
</div>
<div class="SectionBody">
<app-series-select [now]="now" [initial]="location.powerPurchase" (onChange)="locationService.powerPurchase(location, $event, updateLocation)" [series]="filterPower()"></app-series-select>
</div>
</div>
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Einspeisung:
</div>
</div>
<div class="SectionBody">
<app-series-select [now]="now" [initial]="location.powerDeliver" (onChange)="locationService.powerDeliver(location, $event, updateLocation)" [series]="filterPower()"></app-series-select>
</div>
</div>
<div class="Section2">
<div class="SectionHeading">
<div class="SectionHeadingText">
Erzeugung:
</div>
</div>
<div class="SectionBody">
<app-series-select [now]="now" [initial]="location.powerProduce" (onChange)="locationService.powerProduce(location, $event, updateLocation)" [series]="filterPower()"></app-series-select>
</div>
</div>
</div>
</div>
}

View File

@ -1,12 +1,14 @@
import {Component, OnInit} from '@angular/core';
import {Component, OnDestroy, OnInit} from '@angular/core';
import {LocationService} from '../location-service';
import {ActivatedRoute} from '@angular/router';
import {Location} from '../Location';
import {Text} from '../../shared/text/text';
import {Number} from '../../shared/number/number';
import {SeriesSelect, SeriesType} from '../../series/select/series-select';
import {SeriesSelect} from '../../series/select/series-select';
import {Series} from '../../series/Series';
import {SeriesService} from '../../series/series-service';
import {SeriesType} from '../../series/SeriesType';
import {Subscription, timer} from 'rxjs';
@Component({
selector: 'app-location-detail',
@ -18,11 +20,15 @@ import {SeriesService} from '../../series/series-service';
templateUrl: './location-detail.html',
styleUrl: './location-detail.less',
})
export class LocationDetail implements OnInit {
export class LocationDetail implements OnInit, OnDestroy {
private readonly subs: Subscription [] = [];
private series: Series[] = [];
protected location: Location | null = null;
private series: Series[] = [];
protected now: Date = new Date();
constructor(
readonly locationService: LocationService,
@ -37,14 +43,29 @@ export class LocationDetail implements OnInit {
this.locationService.getById(params['id'], location => this.location = location);
});
this.seriesService.findAll(list => this.series = list);
this.subs.push(this.seriesService.subscribe(this.updateSeries));
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
}
protected readonly update = (location: Location): void => {
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
}
protected readonly updateLocation = (location: Location): void => {
if (this.location?.id === location.id) {
this.location = location;
}
};
protected readonly updateSeries = (fresh: Series): void => {
const index = this.series.findIndex(series => series.id === fresh.id);
if (index >= 0) {
this.series.splice(index, 1, fresh);
} else {
this.series.push(fresh);
}
};
protected readonly filterEnergy = (): Series[] => {
return this.series.filter(series => series.type === SeriesType.DELTA && series.unit === 'kWh');
};

View File

@ -1,24 +1,44 @@
import {validateEnum, validateNumber, validateString} from "../common";
import {SeriesType} from "./select/series-select";
import {or, validateDate, validateEnum, validateNumber, validateString} from "../common";
import {SeriesType} from './SeriesType';
import {formatNumber} from '@angular/common';
export class Series {
readonly valueString: string;
constructor(
readonly id: number,
readonly name: string,
readonly decimals: number,
readonly seconds: number,
readonly value: number | null,
readonly last: Date | null,
readonly unit: string,
readonly type: SeriesType,
) {
//
this.valueString = (value === null ? '-' : formatNumber(value, "de-DE", `0.${decimals}-${decimals}`)) + ' ' + unit;
}
static fromJson(json: any): Series {
return new Series(
validateNumber(json.id),
validateString(json.name),
validateNumber(json.decimals),
validateNumber(json.seconds),
or(json.value, validateNumber, null),
or(json.last, validateDate, null),
validateString(json.unit),
validateEnum(json.type, SeriesType),
);
}
isOld(now: Date): boolean {
if (this.last === null) {
return true;
}
const ageSeconds = (now.getTime() - this.last.getTime()) / 1000;
return ageSeconds > this.seconds * 2.1;
}
}

View File

@ -0,0 +1,5 @@
export enum SeriesType {
BOOL = 'BOOL',
DELTA = 'DELTA',
VARYING = 'VARYING',
}

View File

@ -4,15 +4,17 @@
(mouseenter)="showPen = true"
(mouseleave)="showPen = false"
>
<select
#input
[(ngModel)]="model"
(blur)="apply()"
(keydown.enter)="apply()"
>
<select [(ngModel)]="model" (ngModelChange)="changed($event)">
<option [ngValue]="null">-</option>
@for (series of list; track series.id) {
<option [ngValue]="series.id">{{ series.name }}</option>
@for (series of series; track series.id) {
<option [ngValue]="series.id">
{{ series.name }}:
@if (series.isOld(now)) {
--- {{ series.unit }}
} @else {
{{ series.valueString }}
}
</option>
}
</select>
@if (showPen) {

View File

@ -1,4 +1,4 @@
import {Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {FaIconComponent} from '@fortawesome/angular-fontawesome';
import {FormsModule} from '@angular/forms';
import {NgClass} from '@angular/common';
@ -6,12 +6,6 @@ import {faPen} from '@fortawesome/free-solid-svg-icons';
import {Series} from '../Series';
import {or} from '../../common';
export enum SeriesType {
BOOL = 'BOOL',
DELTA = 'DELTA',
VARYING = 'VARYING',
}
@Component({
selector: 'app-series-select',
imports: [
@ -24,16 +18,18 @@ export enum SeriesType {
})
export class SeriesSelect {
@ViewChild('input')
protected readonly input!: ElementRef;
protected readonly faPen = faPen;
private _initial: Series | null = null;
@Input()
protected allowEmpty: boolean = true;
now!: Date;
@Input()
list: Series[] = [];
series!: Series[];
@Input()
allowEmpty: boolean = true;
@Output()
readonly onChange = new EventEmitter<number | null>();
@ -42,6 +38,8 @@ export class SeriesSelect {
protected model: number | null = null;
protected readonly Series = Series;
@Input()
set initial(value: Series | null) {
this._initial = value;
@ -56,12 +54,6 @@ export class SeriesSelect {
return this._initial;
}
protected apply() {
if (this.model !== this.initial) {
this.onChange.emit(this.model);
}
}
protected classes(): {} {
return {
"unchanged": this.model === this.initial,
@ -70,5 +62,12 @@ export class SeriesSelect {
};
}
protected readonly faPen = faPen;
protected changed(id: number | null) {
if (this.allowEmpty || id !== null) {
this.onChange.emit(id);
} else {
this.reset();
}
}
}

View File

@ -3,7 +3,7 @@ body {
height: 100%;
margin: 0;
font-family: sans-serif;
font-size: 5vw;
font-size: 4vw;
}
div {
@ -23,3 +23,35 @@ div {
.NoUserSelect {
user-select: none;
}
.Section {
border: 1px solid gray;
margin: 1em 0.5em 0.5em;
padding: 0.5em;
overflow: visible;
> .SectionHeading {
display: flex;
margin-top: -1.25em;
> .SectionHeadingText {
font-weight: bold;
background-color: white;
}
}
}
.Section2 {
overflow: visible;
> .SectionHeading {
> .SectionHeadingText {
font-style: italic;
}
}
}
.Section2:not(:last-child) {
border-bottom: 1px solid gray;
padding-bottom: 0.5em;
margin-bottom: 0.5em;
}

View File

@ -4,6 +4,7 @@ import de.ph87.data.common.CrudAction;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -20,6 +21,8 @@ public class SeriesService {
private final SeriesRepository seriesRepository;
private final ApplicationEventPublisher applicationEventPublisher;
@NonNull
@Transactional
public SeriesDto create() {
@ -60,6 +63,7 @@ public class SeriesService {
private SeriesDto publish(@NonNull final Series series, @NonNull final CrudAction action) {
final SeriesDto dto = new SeriesDto(series);
log.info("{} {}: {}", Series.class.getSimpleName(), action, dto);
applicationEventPublisher.publishEvent(dto);
return dto;
}
@ -68,6 +72,7 @@ public class SeriesService {
public Series update(final long seriesId, @NonNull final ZonedDateTime date, final double value) {
final Series series = getById(seriesId);
series.update(date, value);
applicationEventPublisher.publishEvent(new SeriesDto(series));
return series;
}