location-detail
This commit is contained in:
parent
2ad140589c
commit
3509d8ab41
@ -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>
|
||||
|
||||
}
|
||||
|
||||
@ -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');
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
5
src/main/angular/src/app/series/SeriesType.ts
Normal file
5
src/main/angular/src/app/series/SeriesType.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum SeriesType {
|
||||
BOOL = 'BOOL',
|
||||
DELTA = 'DELTA',
|
||||
VARYING = 'VARYING',
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user