location-detail
This commit is contained in:
parent
2ad140589c
commit
3509d8ab41
@ -1,11 +1,123 @@
|
|||||||
@if (location) {
|
@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>
|
<div class="Section">
|
||||||
<app-number [initial]="location.longitude" (onChange)="locationService.longitude(location, $event, update)" unit="°"></app-number>
|
<div class="SectionHeading">
|
||||||
<app-series-select [initial]="location.energyPurchase" (onChange)="locationService.energyPurchase(location, $event, update)" [list]="filterEnergy()"></app-series-select>
|
<div class="SectionHeadingText">
|
||||||
<app-series-select [initial]="location.energyDeliver" (onChange)="locationService.energyDeliver(location, $event, update)" [list]="filterEnergy()"></app-series-select>
|
Ort
|
||||||
<app-series-select [initial]="location.energyProduce" (onChange)="locationService.energyProduce(location, $event, update)" [list]="filterEnergy()"></app-series-select>
|
</div>
|
||||||
<app-series-select [initial]="location.powerPurchase" (onChange)="locationService.powerPurchase(location, $event, update)" [list]="filterPower()"></app-series-select>
|
</div>
|
||||||
<app-series-select [initial]="location.powerDeliver" (onChange)="locationService.powerDeliver(location, $event, update)" [list]="filterPower()"></app-series-select>
|
<div class="SectionBody">
|
||||||
<app-series-select [initial]="location.powerProduce" (onChange)="locationService.powerProduce(location, $event, update)" [list]="filterPower()"></app-series-select>
|
<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 {LocationService} from '../location-service';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {Location} from '../Location';
|
import {Location} from '../Location';
|
||||||
import {Text} from '../../shared/text/text';
|
import {Text} from '../../shared/text/text';
|
||||||
import {Number} from '../../shared/number/number';
|
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 {Series} from '../../series/Series';
|
||||||
import {SeriesService} from '../../series/series-service';
|
import {SeriesService} from '../../series/series-service';
|
||||||
|
import {SeriesType} from '../../series/SeriesType';
|
||||||
|
import {Subscription, timer} from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-location-detail',
|
selector: 'app-location-detail',
|
||||||
@ -18,11 +20,15 @@ import {SeriesService} from '../../series/series-service';
|
|||||||
templateUrl: './location-detail.html',
|
templateUrl: './location-detail.html',
|
||||||
styleUrl: './location-detail.less',
|
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;
|
protected location: Location | null = null;
|
||||||
|
|
||||||
private series: Series[] = [];
|
protected now: Date = new Date();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly locationService: LocationService,
|
readonly locationService: LocationService,
|
||||||
@ -37,14 +43,29 @@ export class LocationDetail implements OnInit {
|
|||||||
this.locationService.getById(params['id'], location => this.location = location);
|
this.locationService.getById(params['id'], location => this.location = location);
|
||||||
});
|
});
|
||||||
this.seriesService.findAll(list => this.series = list);
|
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) {
|
if (this.location?.id === location.id) {
|
||||||
this.location = location;
|
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[] => {
|
protected readonly filterEnergy = (): Series[] => {
|
||||||
return this.series.filter(series => series.type === SeriesType.DELTA && series.unit === 'kWh');
|
return this.series.filter(series => series.type === SeriesType.DELTA && series.unit === 'kWh');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,24 +1,44 @@
|
|||||||
import {validateEnum, validateNumber, validateString} from "../common";
|
import {or, validateDate, validateEnum, validateNumber, validateString} from "../common";
|
||||||
import {SeriesType} from "./select/series-select";
|
|
||||||
|
import {SeriesType} from './SeriesType';
|
||||||
|
import {formatNumber} from '@angular/common';
|
||||||
|
|
||||||
export class Series {
|
export class Series {
|
||||||
|
|
||||||
|
readonly valueString: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly id: number,
|
readonly id: number,
|
||||||
readonly name: string,
|
readonly name: string,
|
||||||
|
readonly decimals: number,
|
||||||
|
readonly seconds: number,
|
||||||
|
readonly value: number | null,
|
||||||
|
readonly last: Date | null,
|
||||||
readonly unit: string,
|
readonly unit: string,
|
||||||
readonly type: SeriesType,
|
readonly type: SeriesType,
|
||||||
) {
|
) {
|
||||||
//
|
this.valueString = (value === null ? '-' : formatNumber(value, "de-DE", `0.${decimals}-${decimals}`)) + ' ' + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json: any): Series {
|
static fromJson(json: any): Series {
|
||||||
return new Series(
|
return new Series(
|
||||||
validateNumber(json.id),
|
validateNumber(json.id),
|
||||||
validateString(json.name),
|
validateString(json.name),
|
||||||
|
validateNumber(json.decimals),
|
||||||
|
validateNumber(json.seconds),
|
||||||
|
or(json.value, validateNumber, null),
|
||||||
|
or(json.last, validateDate, null),
|
||||||
validateString(json.unit),
|
validateString(json.unit),
|
||||||
validateEnum(json.type, SeriesType),
|
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"
|
(mouseenter)="showPen = true"
|
||||||
(mouseleave)="showPen = false"
|
(mouseleave)="showPen = false"
|
||||||
>
|
>
|
||||||
<select
|
<select [(ngModel)]="model" (ngModelChange)="changed($event)">
|
||||||
#input
|
|
||||||
[(ngModel)]="model"
|
|
||||||
(blur)="apply()"
|
|
||||||
(keydown.enter)="apply()"
|
|
||||||
>
|
|
||||||
<option [ngValue]="null">-</option>
|
<option [ngValue]="null">-</option>
|
||||||
@for (series of list; track series.id) {
|
@for (series of series; track series.id) {
|
||||||
<option [ngValue]="series.id">{{ series.name }}</option>
|
<option [ngValue]="series.id">
|
||||||
|
{{ series.name }}:
|
||||||
|
@if (series.isOld(now)) {
|
||||||
|
--- {{ series.unit }}
|
||||||
|
} @else {
|
||||||
|
{{ series.valueString }}
|
||||||
|
}
|
||||||
|
</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@if (showPen) {
|
@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 {FaIconComponent} from '@fortawesome/angular-fontawesome';
|
||||||
import {FormsModule} from '@angular/forms';
|
import {FormsModule} from '@angular/forms';
|
||||||
import {NgClass} from '@angular/common';
|
import {NgClass} from '@angular/common';
|
||||||
@ -6,12 +6,6 @@ import {faPen} from '@fortawesome/free-solid-svg-icons';
|
|||||||
import {Series} from '../Series';
|
import {Series} from '../Series';
|
||||||
import {or} from '../../common';
|
import {or} from '../../common';
|
||||||
|
|
||||||
export enum SeriesType {
|
|
||||||
BOOL = 'BOOL',
|
|
||||||
DELTA = 'DELTA',
|
|
||||||
VARYING = 'VARYING',
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-series-select',
|
selector: 'app-series-select',
|
||||||
imports: [
|
imports: [
|
||||||
@ -24,16 +18,18 @@ export enum SeriesType {
|
|||||||
})
|
})
|
||||||
export class SeriesSelect {
|
export class SeriesSelect {
|
||||||
|
|
||||||
@ViewChild('input')
|
protected readonly faPen = faPen;
|
||||||
protected readonly input!: ElementRef;
|
|
||||||
|
|
||||||
private _initial: Series | null = null;
|
private _initial: Series | null = null;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
protected allowEmpty: boolean = true;
|
now!: Date;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
list: Series[] = [];
|
series!: Series[];
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
allowEmpty: boolean = true;
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
readonly onChange = new EventEmitter<number | null>();
|
readonly onChange = new EventEmitter<number | null>();
|
||||||
@ -42,6 +38,8 @@ export class SeriesSelect {
|
|||||||
|
|
||||||
protected model: number | null = null;
|
protected model: number | null = null;
|
||||||
|
|
||||||
|
protected readonly Series = Series;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set initial(value: Series | null) {
|
set initial(value: Series | null) {
|
||||||
this._initial = value;
|
this._initial = value;
|
||||||
@ -56,12 +54,6 @@ export class SeriesSelect {
|
|||||||
return this._initial;
|
return this._initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected apply() {
|
|
||||||
if (this.model !== this.initial) {
|
|
||||||
this.onChange.emit(this.model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected classes(): {} {
|
protected classes(): {} {
|
||||||
return {
|
return {
|
||||||
"unchanged": this.model === this.initial,
|
"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%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 5vw;
|
font-size: 4vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@ -23,3 +23,35 @@ div {
|
|||||||
.NoUserSelect {
|
.NoUserSelect {
|
||||||
user-select: none;
|
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.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@ -20,6 +21,8 @@ public class SeriesService {
|
|||||||
|
|
||||||
private final SeriesRepository seriesRepository;
|
private final SeriesRepository seriesRepository;
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Transactional
|
@Transactional
|
||||||
public SeriesDto create() {
|
public SeriesDto create() {
|
||||||
@ -60,6 +63,7 @@ public class SeriesService {
|
|||||||
private SeriesDto publish(@NonNull final Series series, @NonNull final CrudAction action) {
|
private SeriesDto publish(@NonNull final Series series, @NonNull final CrudAction action) {
|
||||||
final SeriesDto dto = new SeriesDto(series);
|
final SeriesDto dto = new SeriesDto(series);
|
||||||
log.info("{} {}: {}", Series.class.getSimpleName(), action, dto);
|
log.info("{} {}: {}", Series.class.getSimpleName(), action, dto);
|
||||||
|
applicationEventPublisher.publishEvent(dto);
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +72,7 @@ public class SeriesService {
|
|||||||
public Series update(final long seriesId, @NonNull final ZonedDateTime date, final double value) {
|
public Series update(final long seriesId, @NonNull final ZonedDateTime date, final double value) {
|
||||||
final Series series = getById(seriesId);
|
final Series series = getById(seriesId);
|
||||||
series.update(date, value);
|
series.update(date, value);
|
||||||
|
applicationEventPublisher.publishEvent(new SeriesDto(series));
|
||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user