ScheduleEditor responsive
@ -4,7 +4,6 @@ spring.datasource.url=jdbc:h2:./Homeautomation;AUTO_SERVER=TRUE
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=password
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
#-
|
||||
spring.jpa.hibernate.ddl-auto=create
|
||||
#-
|
||||
|
||||
82
src/main/angular/src/app/api/Duration.ts
Normal file
@ -0,0 +1,82 @@
|
||||
export class Duration {
|
||||
|
||||
static readonly ZERO: Duration = new Duration('0');
|
||||
|
||||
readonly totalSeconds: number;
|
||||
|
||||
readonly days: number;
|
||||
|
||||
readonly hours: number;
|
||||
|
||||
readonly minutes: number;
|
||||
|
||||
readonly seconds: number;
|
||||
|
||||
readonly code: string;
|
||||
|
||||
readonly zero: boolean;
|
||||
|
||||
private constructor(input: string) {
|
||||
this.totalSeconds = Duration.parse(input);
|
||||
const sign = Math.sign(this.totalSeconds);
|
||||
const abs = Math.abs(this.totalSeconds);
|
||||
this.days = sign * Math.floor(abs / (24 * 60 * 60));
|
||||
this.hours = sign * Math.floor(abs / (60 * 60)) % 24;
|
||||
this.minutes = sign * Math.floor(abs / 60) % 60;
|
||||
this.seconds = sign * Math.floor(abs) % 60;
|
||||
this.zero = this.totalSeconds === 0;
|
||||
this.code = this.buildCode();
|
||||
}
|
||||
|
||||
static ofCode(code: string) {
|
||||
return new Duration(code);
|
||||
}
|
||||
|
||||
private static parse(input: string) {
|
||||
const regex = /(?<signs>[+-]*)(?<value>\d+(?:[.,]\d+)?)(?<unit>d|h|m|s|ms)/g;
|
||||
let totalSeconds = 0;
|
||||
for (const match of input.matchAll(regex)) {
|
||||
if (!match?.groups) {
|
||||
continue;
|
||||
}
|
||||
const sign = ((match.groups["signs"].replace(/\++/g, '').length % 2) === 1) ? -1 : 1;
|
||||
const value = sign * parseInt(match.groups["value"]);
|
||||
switch (match.groups["unit"]) {
|
||||
case 'd':
|
||||
totalSeconds += value * 24 * 60 * 60;
|
||||
break;
|
||||
case 'h':
|
||||
totalSeconds += value * 60 * 60;
|
||||
break;
|
||||
case 'm':
|
||||
totalSeconds += value * 60;
|
||||
break;
|
||||
case 's':
|
||||
totalSeconds += value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return totalSeconds;
|
||||
}
|
||||
|
||||
private buildCode() {
|
||||
if (this.zero) {
|
||||
return '0';
|
||||
}
|
||||
let code = "";
|
||||
if (this.days != 0) {
|
||||
code += this.days + "d";
|
||||
}
|
||||
if (this.hours != 0) {
|
||||
code += this.hours + "h";
|
||||
}
|
||||
if (this.minutes != 0) {
|
||||
code += this.minutes + "m";
|
||||
}
|
||||
if (this.seconds != 0) {
|
||||
code += this.seconds + "s";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
}
|
||||
@ -2,13 +2,18 @@ import {validateBooleanNotNull, validateDateAllowNull, validateNumberAllowNull,
|
||||
import {Timestamp} from "../../Timestamp";
|
||||
import {Property} from "../../property/Property";
|
||||
import {Bulk} from "../../bulk/Bulk";
|
||||
|
||||
function getDaySeconds(date: Date): number {
|
||||
return date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
|
||||
}
|
||||
import {formatNumber} from "@angular/common";
|
||||
|
||||
export class ScheduleEntry {
|
||||
|
||||
readonly todo: string;
|
||||
|
||||
readonly dayMinute: number;
|
||||
|
||||
readonly anyWeekday: boolean;
|
||||
|
||||
readonly executable: boolean;
|
||||
|
||||
private constructor(
|
||||
readonly id: number,
|
||||
readonly position: number,
|
||||
@ -35,7 +40,24 @@ export class ScheduleEntry {
|
||||
readonly value: number,
|
||||
readonly bulk: Bulk | null,
|
||||
) {
|
||||
// nothing
|
||||
this.dayMinute = hour * 60 + minute;
|
||||
this.anyWeekday = this.monday || this.tuesday || this.wednesday || this.thursday || this.friday || this.saturday || this.sunday;
|
||||
this.executable = this.enabled && this.anyWeekday && !!this.bulk;
|
||||
this.todo = this.getToDo();
|
||||
}
|
||||
|
||||
getToDo(): string {
|
||||
let result: string[] = [];
|
||||
if (!this.enabled) {
|
||||
result.push("inaktiv");
|
||||
}
|
||||
if (!this.bulk) {
|
||||
result.push("keine Aktion");
|
||||
}
|
||||
if (!this.anyWeekday) {
|
||||
result.push("kein Wochentag");
|
||||
}
|
||||
return result.join(", ");
|
||||
}
|
||||
|
||||
static fromJson(json: any): ScheduleEntry {
|
||||
@ -75,4 +97,8 @@ export class ScheduleEntry {
|
||||
return a.position - b.position;
|
||||
}
|
||||
|
||||
get time(): string {
|
||||
return formatNumber(this.hour, 'de-DE', '2.0-0') + ':' + formatNumber(this.minute, 'de-DE', '2.0-0');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
12
src/main/angular/src/app/api/schedule/entry/Zenith.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export class Zenith {
|
||||
|
||||
constructor(
|
||||
readonly title: string,
|
||||
readonly value: number,
|
||||
readonly sunrise: boolean,
|
||||
readonly sunset: boolean,
|
||||
) {
|
||||
// -
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {LOCALE_ID, NgModule} from '@angular/core';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
@ -20,6 +20,14 @@ import {BulkEditorComponent} from './pages/bulk/editor/bulk-editor.component';
|
||||
import {LeftPadDirective} from './pipes/left-pad.directive';
|
||||
import {EntryValueComponent} from './shared/entry-value/entry-value.component';
|
||||
|
||||
import {registerLocaleData} from '@angular/common';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import {BoolComponent} from './shared/bool/bool.component';
|
||||
import {DurationComponent} from './shared/duration/duration.component';
|
||||
|
||||
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
@ -37,6 +45,8 @@ import {EntryValueComponent} from './shared/entry-value/entry-value.component';
|
||||
BulkEditorComponent,
|
||||
LeftPadDirective,
|
||||
EntryValueComponent,
|
||||
BoolComponent,
|
||||
DurationComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@ -45,7 +55,9 @@ import {EntryValueComponent} from './shared/entry-value/entry-value.component';
|
||||
FormsModule,
|
||||
FontAwesomeModule,
|
||||
],
|
||||
providers: [],
|
||||
providers: [
|
||||
{provide: LOCALE_ID, useValue: 'de-DE'}
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {
|
||||
|
||||
@ -1,151 +1,113 @@
|
||||
<ng-container *ngIf="schedule">
|
||||
<div id="title">
|
||||
<app-text [initial]="schedule.title" (valueChange)="scheduleService.set(schedule, 'title', $event)"></app-text>
|
||||
</div>
|
||||
<div id="entries">
|
||||
<div class="entry" *ngFor="let entry of schedule.entries; trackBy: ScheduleEntry.trackBy">
|
||||
|
||||
<ng-template #boolean let-entry="entry" let-value="value" let-key="key">
|
||||
<td class="boolean" (click)="set(entry, key, !value)" [class.true]="value" [class.false]="!value">
|
||||
<fa-icon *ngIf="value" [icon]="faCheckCircle"></fa-icon>
|
||||
<fa-icon *ngIf="!value" [icon]="faCircle"></fa-icon>
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
<p>
|
||||
<button (click)="create()">+ Hinzufügen</button>
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<tr class="header">
|
||||
<ng-container *ngTemplateOutlet="boolean;context:{schedule: schedule, value: schedule.enabled, key:'enabled'}"></ng-container>
|
||||
<td colspan="24">
|
||||
<app-text [initial]="schedule.title" (valueChange)="set(null, 'title', $event)"></app-text>
|
||||
</td>
|
||||
</tr>
|
||||
<tr [class.disabled]="!schedule.enabled">
|
||||
<th> </th>
|
||||
<th>Mo</th>
|
||||
<th>Di</th>
|
||||
<th>Mi</th>
|
||||
<th>Do</th>
|
||||
<th>Fr</th>
|
||||
<th>Sa</th>
|
||||
<th>So</th>
|
||||
<th>Typ</th>
|
||||
<th>Sonnenstand</th>
|
||||
<th colspan="3">Uhrzeit</th>
|
||||
<th>Unschärfe</th>
|
||||
<th colspan="6">Nächste Ausführung</th>
|
||||
<th colspan="2">Eingeschaft setzen</th>
|
||||
<th>Massenausführung</th>
|
||||
<th class="noBorderFirst"> </th>
|
||||
<th class="noBorderMiddle"> </th>
|
||||
<th class="noBorderLast"> </th>
|
||||
</tr>
|
||||
<tr *ngFor="let entry of schedule.entries; trackBy: ScheduleEntry.trackBy" [class.disabled]="entry.nextClearTimestamp === null">
|
||||
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.enabled, key:'enabled'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.monday, key:'monday'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.tuesday, key:'tuesday'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.wednesday, key:'wednesday'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.thursday, key:'thursday'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.friday, key:'friday'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.saturday, key:'saturday'}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="boolean;context:{entry: entry, value: entry.sunday, key:'sunday'}"></ng-container>
|
||||
|
||||
<td>
|
||||
<select [ngModel]="entry.type" (ngModelChange)="set(entry, 'type', $event)">
|
||||
<option value="TIME">Uhrzeit</option>
|
||||
<option value="SUNRISE">Sonnenaufgang</option>
|
||||
<option value="SUNSET">Sonnenuntergang</option>
|
||||
<div class="section">
|
||||
<div class="enabled">
|
||||
<app-bool [value]="entry.enabled" (onChange)="entryService.set(entry, 'enabled', $event)"></app-bool>
|
||||
</div>
|
||||
<div class="title">
|
||||
<select [ngModel]="entry.bulk?.id" (ngModelChange)="entryService.set(entry, 'bulk', $event)">
|
||||
<option [ngValue]="null">-</option>
|
||||
<option [ngValue]="bulk.id" *ngFor="let bulk of bulks.sort(Bulk.compareName)">{{ bulk.name }}</option>
|
||||
</select>
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
|
||||
<td>
|
||||
<select [ngModel]="entry.zenith" (ngModelChange)="set(entry, 'zenith', $event)">
|
||||
<option *ngFor="let event of schedule.astros; let index = index" [value]="event.zenith">
|
||||
[{{event.zenith | number:'0.1-1' | leftPad:5}}°, {{(entry.type === 'SUNRISE' ? event.sunrise : event.sunset) | date:'HH:mm'}}] {{entry.type === 'SUNRISE' ? event.sunriseName : event.sunsetName}}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
</td>
|
||||
</ng-container>
|
||||
<td *ngIf="entry.type !== 'SUNRISE' && entry.type !== 'SUNSET'" class="empty"></td>
|
||||
|
||||
<ng-container *ngIf="entry.type === 'TIME'">
|
||||
<td class="first">
|
||||
<select [ngModel]="entry.hour" (ngModelChange)="set(entry, 'hour', $event)">
|
||||
<option *ngFor="let _ of [].constructor(24); let value = index" [ngValue]="value">{{value}}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="middle">:</td>
|
||||
<td class="last">
|
||||
<select [ngModel]="entry.minute" (ngModelChange)="set(entry, 'minute', $event)">
|
||||
<option *ngFor="let _ of [].constructor(12); let value = index" [ngValue]="value * 5">{{value * 5 | number:'2.0'}}</option>
|
||||
</select>
|
||||
</td>
|
||||
</ng-container>
|
||||
<td *ngIf="entry.type !== 'TIME'" colspan="3" class="empty"></td>
|
||||
|
||||
<td>
|
||||
<select [ngModel]="entry.fuzzySeconds" (ngModelChange)="set(entry, 'fuzzySeconds', $event)">
|
||||
<option [ngValue]="0">Keine</option>
|
||||
<option [ngValue]="60">1 Minute</option>
|
||||
<option [ngValue]="300">5 Minuten</option>
|
||||
<option [ngValue]="600">10 Minuten</option>
|
||||
<option [ngValue]="1800">30 Minuten</option>
|
||||
<option [ngValue]="3600">1 Stunde</option>
|
||||
<option [ngValue]="7200">2 Stunden</option>
|
||||
<option [ngValue]="10800">3 Stunden</option>
|
||||
<option [ngValue]="21600">6 Stunden</option>
|
||||
<option [ngValue]="43200">12 Stunden</option>
|
||||
<option [ngValue]="86400">1 Tag</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<ng-container *ngIf="entry.nextClearTimestamp">
|
||||
<td class="number first" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.dayName}}</td>
|
||||
<td class="number middle" [class.empty]="entry.fuzzySeconds > 0">: </td>
|
||||
<td class="number last" [class.empty]="entry.fuzzySeconds > 0">{{entry.nextClearTimestamp.timeString}}</td>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!entry.nextClearTimestamp">
|
||||
<td class="empty first"></td>
|
||||
<td class="empty middle"></td>
|
||||
<td class="empty last"></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="entry.nextFuzzyTimestamp && entry.fuzzySeconds > 0">
|
||||
<td class="number first">{{entry.nextFuzzyTimestamp.dayName}}</td>
|
||||
<td class="number middle">: </td>
|
||||
<td class="number last">{{entry.nextFuzzyTimestamp.timeString}}</td>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!entry.nextFuzzyTimestamp || entry.fuzzySeconds <= 0">
|
||||
<td class="empty first"></td>
|
||||
<td class="empty middle"></td>
|
||||
<td class="empty last"></td>
|
||||
</ng-container>
|
||||
|
||||
<td>
|
||||
<app-search [searchService]="propertyService" [initial]="entry.property?.id" (valueChange)="set(entry, 'property', $event)"></app-search>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<app-entry-value [entry]="entry" [allowChange]="true" (onSet)="set(entry, $event.key, $event.value)"></app-entry-value>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<app-search [searchService]="bulkService" [initial]="entry.bulk?.id" (valueChange)="set(entry, 'bulk', $event)"></app-search>
|
||||
</td>
|
||||
|
||||
<td class="delete noBorderFirst" (click)="delete(entry)">
|
||||
<fa-icon [icon]="faTimes"></fa-icon>
|
||||
</td>
|
||||
|
||||
<td class="noBorderMiddle" (click)="set(entry, 'position', entry.position - 1)">
|
||||
<fa-icon *ngIf="entry.position > 0" [icon]="faUp"></fa-icon>
|
||||
</td>
|
||||
|
||||
<td class="noBorderLast" (click)="set(entry, 'position', entry.position + 1)">
|
||||
<fa-icon *ngIf="entry.position < schedule.entries.length - 1" [icon]="faDown"></fa-icon>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
<div class="section">
|
||||
<div class="weekdays">
|
||||
<div>
|
||||
<app-bool label="Mo" [value]="entry.monday" (onChange)="entryService.set(entry, 'monday', $event)"></app-bool>
|
||||
</div>
|
||||
<div>
|
||||
<app-bool label="Di" [value]="entry.tuesday" (onChange)="entryService.set(entry, 'tuesday', $event)"></app-bool>
|
||||
</div>
|
||||
<div>
|
||||
<app-bool label="Mi" [value]="entry.wednesday" (onChange)="entryService.set(entry, 'wednesday', $event)"></app-bool>
|
||||
</div>
|
||||
<div>
|
||||
<app-bool label="Do" [value]="entry.thursday" (onChange)="entryService.set(entry, 'thursday', $event)"></app-bool>
|
||||
</div>
|
||||
<div>
|
||||
<app-bool label="Fr" [value]="entry.friday" (onChange)="entryService.set(entry, 'friday', $event)"></app-bool>
|
||||
</div>
|
||||
<div>
|
||||
<app-bool label="Sa" [value]="entry.saturday" (onChange)="entryService.set(entry, 'saturday', $event)"></app-bool>
|
||||
</div>
|
||||
<div>
|
||||
<app-bool label="So" [value]="entry.sunday" (onChange)="entryService.set(entry, 'sunday', $event)"></app-bool>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modes">
|
||||
<div class="_inner_">
|
||||
<div>
|
||||
<app-bool icon="faClock" [value]="entry.type === 'TIME'" (onChange)="entryService.set(entry, 'type', 'TIME')"></app-bool>
|
||||
</div>
|
||||
<div>
|
||||
<app-bool icon="faArrowAltCircleUp" [value]="entry.type === 'SUNRISE'" (onChange)="entryService.set(entry, 'type', 'SUNRISE')"></app-bool>
|
||||
</div>
|
||||
<div>
|
||||
<app-bool icon="faArrowAltCircleDown" [value]="entry.type === 'SUNSET'" (onChange)="entryService.set(entry, 'type', 'SUNSET')"></app-bool>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" *ngIf="entry.type === 'TIME'">
|
||||
<div class="time">
|
||||
<button class="buttonPlus" (click)="dayMinuteAdd(entry, +60)">+</button>
|
||||
<button class="buttonMinus" (click)="dayMinuteAdd(entry, -60)">-</button>
|
||||
<input type="time" [ngModel]="entry.time" (ngModelChange)="timeFromString(entry, $event)">
|
||||
<button class="buttonPlus" (click)="dayMinuteAdd(entry, +1)">+</button>
|
||||
<button class="buttonMinus" (click)="dayMinuteAdd(entry, -1)">-</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" *ngIf="entry.type === 'SUNRISE' || entry.type === 'SUNSET'">
|
||||
<div class="sun">
|
||||
<div *ngFor="let zenith of getZenithEntries(entry.type); trackBy: trackByZenith">
|
||||
<app-bool [label]="zenith.title" [value]="entry.zenith === zenith.value" (onChange)="entryService.set(entry, 'zenith', zenith.value)"></app-bool>
|
||||
</div>
|
||||
<div>
|
||||
<input type="number" min="45" max="120" [ngModel]="entry.zenith" (ngModelChange)="entryService.set(entry, 'zenith', $event || 0)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="flexHalf">
|
||||
<div class="flexIcon">
|
||||
<img src="assets/dice.svg" alt="+/-" title="Zufallsabweichung +/-">
|
||||
</div>
|
||||
<div class="flexInput">
|
||||
<app-duration [duration]="Duration.ofCode(entry.fuzzySeconds + 's')" [bgcolor]="entry.fuzzySeconds ? '#88c0ff' : 'white'" [min]="Duration.ofCode('')" (onChange)="entryService.set(entry, 'fuzzySeconds', $event.totalSeconds)"></app-duration>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flexHalf">
|
||||
<div class="flexIcon">
|
||||
<img src="assets/skip.svg" alt="Überspringen">
|
||||
</div>
|
||||
<div class="flexInput flexInputLast">
|
||||
<input type="number" min="0" [class.skipBack]="entry.skip" [ngModel]="entry.skip" (ngModelChange)="entryService.set(entry, 'skip', $event || 0)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="nextFuzzyTimestamp" [class.skipFont]="entry.skip" *ngIf="entry.executable">
|
||||
{{ relativeDate(entry.nextFuzzyTimestamp?.date) }}
|
||||
<span [class.fuzzyFont]="entry.fuzzySeconds" *ngIf="entry.fuzzySeconds">
|
||||
(eig: {{ entry.nextClearTimestamp.date | date:'HH:mm' }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="inactive" *ngIf="entry.todo">
|
||||
{{ entry.todo }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@ -1,19 +1,134 @@
|
||||
select {
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
font-family: monospace;
|
||||
#title {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: lightblue;
|
||||
#entries {
|
||||
margin-top: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
|
||||
img {
|
||||
display: inline;
|
||||
height: 1em;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tr.header {
|
||||
.entry {
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
height: 8.2em;
|
||||
border-radius: 0.2em;
|
||||
background-color: #ececec;
|
||||
|
||||
th:not(:first-child), td:not(:first-child) {
|
||||
border: none;
|
||||
@media (min-width: 1001px) {
|
||||
float: left;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: 0.25em;
|
||||
|
||||
.enabled {
|
||||
float: left;
|
||||
width: 1.4em;
|
||||
border-radius: 0.2em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.title {
|
||||
float: left;
|
||||
padding: 0.1em;
|
||||
width: calc(100% - 1.65em);
|
||||
}
|
||||
|
||||
.weekdays {
|
||||
float: left;
|
||||
width: 75%;
|
||||
border-radius: 0.2em;
|
||||
|
||||
div {
|
||||
float: left;
|
||||
width: 14.2857%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.modes {
|
||||
float: left;
|
||||
width: 25%;
|
||||
|
||||
._inner_ {
|
||||
margin-left: 0.25em;
|
||||
border-radius: 0.2em;
|
||||
|
||||
div {
|
||||
float: left;
|
||||
width: 33.3333%;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
width: 50%;
|
||||
|
||||
button {
|
||||
width: 16.25%;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 35%;
|
||||
}
|
||||
}
|
||||
|
||||
.sun {
|
||||
border-radius: 0.2em;
|
||||
|
||||
div {
|
||||
float: left;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.flexHalf {
|
||||
float: left;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flexIcon {
|
||||
width: 2.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flexInput {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.nextFuzzyTimestamp {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.skipBack {
|
||||
background-color: #ffc059;
|
||||
}
|
||||
|
||||
.skipFont {
|
||||
color: #ff9a00;
|
||||
}
|
||||
|
||||
.fuzzyFont {
|
||||
color: #489dff;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
color: gray;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {Component, Inject, LOCALE_ID, OnInit} from '@angular/core';
|
||||
import {ScheduleService} from "../../../api/schedule/schedule.service";
|
||||
import {Schedule} from "../../../api/schedule/Schedule";
|
||||
import {ScheduleEntry} from "../../../api/schedule/entry/ScheduleEntry";
|
||||
import {ScheduleEntryService} from "../../../api/schedule/entry/schedule-entry.service";
|
||||
import {faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCircle, faTimesCircle} from '@fortawesome/free-regular-svg-icons';
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {PropertyService} from "../../../api/property/property.service";
|
||||
import {BulkService} from "../../../api/bulk/BulkService";
|
||||
import {Update} from "../../../api/Update";
|
||||
import {NO_OP} from "../../../api/api.service";
|
||||
import {DatePipe} from "@angular/common";
|
||||
|
||||
import {Duration} from "../../../api/Duration";
|
||||
import {Bulk} from "../../../api/bulk/Bulk";
|
||||
import {BulkService} from "../../../api/bulk/BulkService";
|
||||
import {Zenith} from "../../../api/schedule/entry/Zenith";
|
||||
|
||||
const DAY_MINUTES: number = 24 * 60;
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule-editor',
|
||||
@ -17,36 +22,47 @@ import {NO_OP} from "../../../api/api.service";
|
||||
})
|
||||
export class ScheduleEditorComponent implements OnInit {
|
||||
|
||||
readonly ScheduleEntry = ScheduleEntry;
|
||||
protected readonly datePipe: DatePipe = new DatePipe(this.locale);
|
||||
|
||||
readonly faCheckCircle = faCheckCircle;
|
||||
protected readonly ScheduleEntry = ScheduleEntry;
|
||||
|
||||
readonly faCircle = faCircle;
|
||||
protected readonly Schedule = Schedule;
|
||||
|
||||
readonly faTimes = faTimesCircle;
|
||||
protected readonly Duration = Duration;
|
||||
|
||||
readonly faUp = faArrowAltCircleUp;
|
||||
protected readonly Bulk = Bulk;
|
||||
|
||||
readonly faDown = faArrowAltCircleDown;
|
||||
protected readonly expanded: number[] = [];
|
||||
|
||||
readonly Schedule = Schedule;
|
||||
protected now: Date = new Date(Date.now());
|
||||
|
||||
schedule!: Schedule;
|
||||
protected schedule!: Schedule;
|
||||
|
||||
protected bulks: Bulk[] = [];
|
||||
|
||||
protected readonly ZENITH_ENTRIES: Zenith[] = [
|
||||
new Zenith("Astr.", 107, true, true),
|
||||
new Zenith("Naut.", 102, true, true),
|
||||
new Zenith("Bürg.", 96, true, true),
|
||||
new Zenith("Aufg.", 90.8, true, false),
|
||||
new Zenith("Unterg.", 90.8, false, true),
|
||||
];
|
||||
|
||||
constructor(
|
||||
readonly router: Router,
|
||||
readonly activatedRoute: ActivatedRoute,
|
||||
readonly scheduleService: ScheduleService,
|
||||
readonly scheduleEntryService: ScheduleEntryService,
|
||||
readonly propertyService: PropertyService,
|
||||
readonly entryService: ScheduleEntryService,
|
||||
readonly bulkService: BulkService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) {
|
||||
// nothing
|
||||
// -
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.scheduleService.subscribe(update => this.update(update));
|
||||
this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params['id'], schedule => this.schedule = schedule));
|
||||
this.bulkService.findAll(list => this.bulks = list);
|
||||
this.activatedRoute.params.subscribe(params => this.scheduleService.getById(params['id'], schedule => this.setSchedule(schedule)));
|
||||
}
|
||||
|
||||
private update(update: Update<Schedule>): void {
|
||||
@ -57,25 +73,104 @@ export class ScheduleEditorComponent implements OnInit {
|
||||
this.router.navigate(['/ScheduleList']);
|
||||
return;
|
||||
}
|
||||
this.schedule = update.payload;
|
||||
this.setSchedule(update.payload);
|
||||
}
|
||||
|
||||
private setSchedule(schedule: Schedule) {
|
||||
this.schedule = schedule;
|
||||
this.expanded.length = 0;
|
||||
this.expanded.push(...schedule.entries.map(e => e.id));
|
||||
}
|
||||
|
||||
create(): void {
|
||||
this.scheduleEntryService.create(this.schedule, NO_OP);
|
||||
this.entryService.create(this.schedule, NO_OP);
|
||||
}
|
||||
|
||||
delete(entry: ScheduleEntry): void {
|
||||
if (confirm("Eintrag \"" + entry.nextClearTimestamp?.timeString + " +/-" + entry.fuzzySeconds + "\" wirklich löschen?")) {
|
||||
this.scheduleEntryService.delete(entry, NO_OP);
|
||||
this.entryService.delete(entry, NO_OP);
|
||||
}
|
||||
}
|
||||
|
||||
set(entry: ScheduleEntry | null, key: string, value: any): void {
|
||||
if (entry) {
|
||||
this.scheduleEntryService.set(entry, key, value, NO_OP);
|
||||
this.entryService.set(entry, key, value, NO_OP);
|
||||
} else {
|
||||
this.scheduleService.set(this.schedule, key, value, NO_OP);
|
||||
}
|
||||
}
|
||||
|
||||
relativeDate(date: Date | undefined) {
|
||||
if (date === undefined || date === null) {
|
||||
return "";
|
||||
}
|
||||
const relativeName = this.relativeCalendarDaysName(date);
|
||||
return relativeName + " " + this.datePipe.transform(date, 'HH:mm');
|
||||
}
|
||||
|
||||
relativeCalendarDaysName(date: Date): string {
|
||||
const prefix = this.relativeCalendarDaysPrefix(date);
|
||||
const weekday = date.toLocaleDateString(this.locale, {weekday: 'long'});
|
||||
return prefix + ", " + weekday;
|
||||
}
|
||||
|
||||
private relativeCalendarDaysPrefix(date: Date): string {
|
||||
const days = this.calendarDays(date);
|
||||
if (days < -2) {
|
||||
return "Vor " + -days + " Tagen";
|
||||
} else if (days === -2) {
|
||||
return "Vorgestern";
|
||||
} else if (days === -1) {
|
||||
return "Gestern";
|
||||
} else if (days === 1) {
|
||||
return "Morgen";
|
||||
} else if (days === 2) {
|
||||
return "Übermorgen";
|
||||
} else if (days > 2) {
|
||||
return "In " + days + " Tagen";
|
||||
}
|
||||
return "Heute";
|
||||
}
|
||||
|
||||
private calendarDays(date: Date) {
|
||||
const DAY_MS = 1000 * 60 * 60 * 24;
|
||||
const aMidnight = new Date(this.now.getFullYear(), this.now.getMonth(), this.now.getDate());
|
||||
const bMidnight = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
const milliseconds = bMidnight.getTime() - aMidnight.getTime();
|
||||
return Math.floor(milliseconds / DAY_MS);
|
||||
}
|
||||
|
||||
getZenithEntries(type: string): Zenith[] {
|
||||
if (type === 'SUNRISE') {
|
||||
return this.ZENITH_ENTRIES.filter(zenith => zenith.sunrise);
|
||||
}
|
||||
return this.ZENITH_ENTRIES.reverse().filter(zenith => zenith.sunset);
|
||||
}
|
||||
|
||||
trackByZenith(index: number, zenith: Zenith) {
|
||||
return zenith.value;
|
||||
}
|
||||
|
||||
timeFromString(entry: ScheduleEntry, time: string) {
|
||||
const parts = time.split(':');
|
||||
const hour = parseInt(parts[0]);
|
||||
const minute = parseInt(parts[1]);
|
||||
let second = 0;
|
||||
if (parts.length === 3) {
|
||||
second = parseInt(parts[2]);
|
||||
}
|
||||
const daySecond = (hour * 24 + minute) * 60 + second;
|
||||
console.log(hour, minute, second, daySecond);
|
||||
this.entryService.set(entry, 'daySecond', daySecond);
|
||||
}
|
||||
|
||||
dayMinuteAdd(entry: ScheduleEntry, minutes: number) {
|
||||
let newMinutes = entry.dayMinute + minutes;
|
||||
while (newMinutes < 0 || newMinutes >= DAY_MINUTES) {
|
||||
newMinutes = (newMinutes + DAY_MINUTES) % DAY_MINUTES;
|
||||
}
|
||||
this.entryService.set(entry, 'daySecond', newMinutes * 60);
|
||||
}
|
||||
|
||||
protected readonly console = console;
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
.schedules {
|
||||
font-size: 4vw;
|
||||
padding: 0.25em;
|
||||
|
||||
.scheduleBox {
|
||||
@ -132,8 +131,4 @@
|
||||
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
18
src/main/angular/src/app/shared/bool/bool.component.html
Normal file
@ -0,0 +1,18 @@
|
||||
<div type="checkbox" [style.background-color]="color()" (click)="onChange.emit(!value)">
|
||||
<ng-container *ngIf="label">
|
||||
{{ label }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!label">
|
||||
<ng-container *ngIf="icon">
|
||||
<fa-icon *ngIf="icon === 'faCircle'" [icon]="faCircle"></fa-icon>
|
||||
<fa-icon *ngIf="icon === 'faCheckCircle'" [icon]="faCheckCircle"></fa-icon>
|
||||
<fa-icon *ngIf="icon === 'faClock'" [icon]="faClock"></fa-icon>
|
||||
<fa-icon *ngIf="icon === 'faArrowAltCircleUp'" [icon]="faArrowAltCircleUp"></fa-icon>
|
||||
<fa-icon *ngIf="icon === 'faArrowAltCircleDown'" [icon]="faArrowAltCircleDown"></fa-icon>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!icon">
|
||||
<fa-icon *ngIf="value" [icon]="faCheckCircle"></fa-icon>
|
||||
<fa-icon *ngIf="!value" [icon]="faCircle"></fa-icon>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
8
src/main/angular/src/app/shared/bool/bool.component.less
Normal file
@ -0,0 +1,8 @@
|
||||
div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 0.1em;
|
||||
padding-bottom: 0.1em;
|
||||
text-align: center;
|
||||
background-color: gray;
|
||||
}
|
||||
58
src/main/angular/src/app/shared/bool/bool.component.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCircle, faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
@Component({
|
||||
selector: 'app-bool',
|
||||
templateUrl: './bool.component.html',
|
||||
styleUrls: ['./bool.component.less']
|
||||
})
|
||||
export class BoolComponent implements OnInit {
|
||||
|
||||
protected readonly faCheckCircle = faCheckCircle;
|
||||
|
||||
protected readonly faCircle = faCircle;
|
||||
|
||||
protected readonly faClock = faClock;
|
||||
|
||||
protected readonly faArrowAltCircleUp = faArrowAltCircleUp;
|
||||
|
||||
protected readonly faArrowAltCircleDown = faArrowAltCircleDown;
|
||||
|
||||
@Input()
|
||||
icon: string | undefined = undefined;
|
||||
|
||||
@Input()
|
||||
colorActive: string = '#8FBC8FFF';
|
||||
|
||||
@Input()
|
||||
colorInactive: string = '#8c8c8c';
|
||||
|
||||
@Input()
|
||||
label: string | undefined = undefined;
|
||||
|
||||
@Input()
|
||||
value: boolean | undefined = undefined;
|
||||
|
||||
@Output()
|
||||
onChange: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
||||
}
|
||||
|
||||
color(): string {
|
||||
if (this.value === true) {
|
||||
return this.colorActive;
|
||||
} else if (this.value === false) {
|
||||
return this.colorInactive;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
<input type="text" [(ngModel)]="code" (focus)="focus = true" (blur)="apply()" (keydown.enter)="apply()" (keydown.escape)="cancel()" [style.background-color]="bgcolor">
|
||||
@ -0,0 +1,3 @@
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {Duration} from "../../api/Duration";
|
||||
|
||||
@Component({
|
||||
selector: 'app-duration',
|
||||
templateUrl: './duration.component.html',
|
||||
styleUrls: ['./duration.component.less']
|
||||
})
|
||||
export class DurationComponent implements OnInit {
|
||||
|
||||
private _duration: Duration = Duration.ZERO;
|
||||
|
||||
@Input()
|
||||
set duration(duration: Duration) {
|
||||
this._duration = duration;
|
||||
if (!this.focus) {
|
||||
this.code = this.duration.code;
|
||||
}
|
||||
}
|
||||
|
||||
get duration(): Duration {
|
||||
return this._duration;
|
||||
}
|
||||
|
||||
@Input()
|
||||
bgcolor: string = '';
|
||||
|
||||
@Input()
|
||||
min: Duration | undefined = undefined;
|
||||
|
||||
@Input()
|
||||
max: Duration | undefined = undefined;
|
||||
|
||||
@Output()
|
||||
readonly onChange: EventEmitter<Duration> = new EventEmitter();
|
||||
|
||||
protected code: string = '';
|
||||
|
||||
protected focus: boolean = false;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
apply() {
|
||||
let duration = Duration.ofCode(this.code);
|
||||
if (this.min !== undefined && duration.totalSeconds < this.min.totalSeconds) {
|
||||
duration = this.min;
|
||||
}
|
||||
if (this.max !== undefined && duration.totalSeconds > this.max.totalSeconds) {
|
||||
duration = this.max;
|
||||
}
|
||||
this.code = duration.code;
|
||||
this.onChange.emit(duration);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.code = this.duration.code;
|
||||
}
|
||||
|
||||
}
|
||||
3
src/main/angular/src/assets/checked.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M243.8 339.8C232.9 350.7 215.1 350.7 204.2 339.8L140.2 275.8C129.3 264.9 129.3 247.1 140.2 236.2C151.1 225.3 168.9 225.3 179.8 236.2L224 280.4L332.2 172.2C343.1 161.3 360.9 161.3 371.8 172.2C382.7 183.1 382.7 200.9 371.8 211.8L243.8 339.8zM512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 553 B |
3
src/main/angular/src/assets/delete.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M175 175C184.4 165.7 199.6 165.7 208.1 175L255.1 222.1L303 175C312.4 165.7 327.6 165.7 336.1 175C346.3 184.4 346.3 199.6 336.1 208.1L289.9 255.1L336.1 303C346.3 312.4 346.3 327.6 336.1 336.1C327.6 346.3 312.4 346.3 303 336.1L255.1 289.9L208.1 336.1C199.6 346.3 184.4 346.3 175 336.1C165.7 327.6 165.7 312.4 175 303L222.1 255.1L175 208.1C165.7 199.6 165.7 184.4 175 175V175zM512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 687 B |
9
src/main/angular/src/assets/dice.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="translate(-220.000000, -8079.000000)">
|
||||
<g transform="translate(56.000000, 160.000000)">
|
||||
<path
|
||||
d="M174,7927.1047 C172.896,7927.1047 172,7927.9997 172,7929.1047 C172,7930.2097 172.896,7931.1047 174,7931.1047 C175.104,7931.1047 176,7930.2097 176,7929.1047 C176,7927.9997 175.104,7927.1047 174,7927.1047 L174,7927.1047 Z M182,7921.9997 C182,7921.4477 181.552,7920.9997 181,7920.9997 L167,7920.9997 C166.448,7920.9997 166,7921.4477 166,7921.9997 L166,7935.9997 C166,7936.5527 166.448,7936.9997 167,7936.9997 L181,7936.9997 C181.552,7936.9997 182,7936.5527 182,7935.9997 L182,7921.9997 Z M184,7920.9997 L184,7936.9997 C184,7938.1047 183.105,7938.9997 182,7938.9997 L166,7938.9997 C164.896,7938.9997 164,7938.1047 164,7936.9997 L164,7920.9997 C164,7919.8957 164.896,7918.9997 166,7918.9997 L182,7918.9997 C183.105,7918.9997 184,7919.8957 184,7920.9997 L184,7920.9997 Z M170,7927.1047 C171.104,7927.1047 172,7926.2097 172,7925.1047 C172,7923.9997 171.104,7923.1047 170,7923.1047 C168.896,7923.1047 168,7923.9997 168,7925.1047 C168,7926.2097 168.896,7927.1047 170,7927.1047 L170,7927.1047 Z M170,7931.1047 C168.896,7931.1047 168,7931.9997 168,7933.1047 C168,7934.2097 168.896,7935.1047 170,7935.1047 C171.104,7935.1047 172,7934.2097 172,7933.1047 C172,7931.9997 171.104,7931.1047 170,7931.1047 L170,7931.1047 Z M178,7923.1047 C176.896,7923.1047 176,7923.9997 176,7925.1047 C176,7926.2097 176.896,7927.1047 178,7927.1047 C179.104,7927.1047 180,7926.2097 180,7925.1047 C180,7923.9997 179.104,7923.1047 178,7923.1047 L178,7923.1047 Z M180,7933.1047 C180,7934.2097 179.104,7935.1047 178,7935.1047 C176.896,7935.1047 176,7934.2097 176,7933.1047 C176,7931.9997 176.896,7931.1047 178,7931.1047 C179.104,7931.1047 180,7931.9997 180,7933.1047 L180,7933.1047 Z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
4
src/main/angular/src/assets/down.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 9L12 15L6 9" stroke="#33363F" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 292 B |
4
src/main/angular/src/assets/right.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 6L15 12L9 18" stroke="#33363F" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 292 B |
12
src/main/angular/src/assets/sun.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<path d="M12 2V4" stroke-linecap="round"/>
|
||||
<path d="M12 20V22" stroke-linecap="round"/>
|
||||
<path d="M4 12L2 12" stroke-linecap="round"/>
|
||||
<path d="M22 12L20 12" stroke-linecap="round"/>
|
||||
<path d="M19.7778 4.22266L17.5558 6.25424" stroke-linecap="round"/>
|
||||
<path d="M4.22217 4.22266L6.44418 6.25424" stroke-linecap="round"/>
|
||||
<path d="M6.44434 17.5557L4.22211 19.7779" stroke-linecap="round"/>
|
||||
<path d="M19.7778 19.7773L17.5558 17.5551" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 684 B |
4
src/main/angular/src/assets/time.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 7V12L10.5 14.5M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 349 B |
3
src/main/angular/src/assets/unchecked.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<path d="M512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 314 B |
@ -1,6 +1,30 @@
|
||||
// font
|
||||
body, input, select, button {
|
||||
font-size: 5vw;
|
||||
font-family: arial, sans-serif;
|
||||
@media (min-width: 1001px) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// forms
|
||||
input, select, button {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0.1em;
|
||||
border-radius: 0.1em;
|
||||
box-sizing: border-box;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
select {
|
||||
margin-top: -0.1em;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: arial, sans-serif;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -9,9 +33,9 @@ a {
|
||||
}
|
||||
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@ -127,3 +151,11 @@ table.vertical {
|
||||
clear: both;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.buttonPlus {
|
||||
background-color: #8fbc8f;
|
||||
}
|
||||
|
||||
.buttonMinus {
|
||||
background-color: #ef8787;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import de.ph87.homeautomation.bulk.BulkDto;
|
||||
import de.ph87.homeautomation.bulk.entry.BulkEntryController;
|
||||
import de.ph87.homeautomation.bulk.entry.BulkEntryCreateDto;
|
||||
import de.ph87.homeautomation.channel.Channel;
|
||||
import de.ph87.homeautomation.device.DeviceController;
|
||||
import de.ph87.homeautomation.property.Property;
|
||||
import de.ph87.homeautomation.property.PropertyRepository;
|
||||
import de.ph87.homeautomation.property.PropertyType;
|
||||
@ -39,23 +40,32 @@ public class DemoDataService {
|
||||
|
||||
private final ScheduleEntryController scheduleEntryController;
|
||||
|
||||
private final DeviceController deviceController;
|
||||
|
||||
public void insertDemoData() {
|
||||
if (!config.isInsertDemoData()) {
|
||||
return;
|
||||
}
|
||||
final Property propertyDirect = createProperty("propertyDirect", "direct", PropertyType.BOOLEAN, null, null);
|
||||
final Property propertyBulkBoolean = createProperty("propertyBulkBoolean", null, PropertyType.BOOLEAN, null, null);
|
||||
final Property propertyBulkShutter = createProperty("propertyBulkShutter", null, PropertyType.SHUTTER, null, null);
|
||||
final Property propertyBulkBrightness = createProperty("propertyBulkBrightness", null, PropertyType.BRIGHTNESS_PERCENT, null, null);
|
||||
final Property propertyBulkColorTemperature = createProperty("propertyBulkColorTemperature", null, PropertyType.COLOR_TEMPERATURE, null, null);
|
||||
final BulkDto bulk = bulkController.create(new BulkCreateDto("bulk", true));
|
||||
bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkBoolean.getId(), 1, 0));
|
||||
bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkShutter.getId(), 35, 0));
|
||||
bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkBrightness.getId(), 40, 0));
|
||||
bulkEntryController.create(new BulkEntryCreateDto(bulk.getId(), propertyBulkColorTemperature.getId(), 55, 0));
|
||||
final long scheduleId = createSchedule(true, "schedule");
|
||||
|
||||
final long schedule = createSchedule(true, "Schedule");
|
||||
|
||||
deviceController.create("DeviceSwitch");
|
||||
deviceController.create("DeviceSwitch");
|
||||
deviceController.create("DeviceSwitch");
|
||||
deviceController.create("DeviceShutter");
|
||||
deviceController.create("DeviceShutter");
|
||||
deviceController.create("DeviceShutter");
|
||||
|
||||
final Property propWeihnachtsbeleuchtung = createProperty("Weihnachtsbeleuchtung", null, PropertyType.BOOLEAN, null, null);
|
||||
final BulkDto bulkWeihnachtsbeleuchtungAn = bulkController.create(new BulkCreateDto("Weihnachtsbeleuchtung An", true));
|
||||
bulkEntryController.create(new BulkEntryCreateDto(bulkWeihnachtsbeleuchtungAn.getId(), propWeihnachtsbeleuchtung.getId(), 35, 0));
|
||||
final ZonedDateTime now = ZonedDateTime.now().plusSeconds(3);
|
||||
createTime(scheduleId, true, now.getHour(), now.getMinute(), now.getSecond(), 0, propertyDirect, 1, bulk);
|
||||
createTime(schedule, true, now.getHour(), now.getMinute(), now.getSecond(), 0, 1, bulkWeihnachtsbeleuchtungAn);
|
||||
|
||||
final Property propRollladenSchlafzimmer = createProperty("Schlafzimmer", null, PropertyType.SHUTTER, null, null);
|
||||
final BulkDto bulkRollladenSchlafzimmerAufstehen = bulkController.create(new BulkCreateDto("Rollladen Schlafzimmer Aufstehen", true));
|
||||
bulkEntryController.create(new BulkEntryCreateDto(bulkRollladenSchlafzimmerAufstehen.getId(), propRollladenSchlafzimmer.getId(), 1, 0));
|
||||
createSunrise(schedule, true, Zenith.CIVIL, 0, 1, bulkRollladenSchlafzimmerAufstehen);
|
||||
}
|
||||
|
||||
private Property createProperty(final String title, final String slug, final PropertyType type, final Channel readChannel, final Channel writeChannel) {
|
||||
@ -73,22 +83,23 @@ public class DemoDataService {
|
||||
return id;
|
||||
}
|
||||
|
||||
private void createTime(final long scheduleId, final boolean enabled, final int hour, final int minute, final int second, final int fuzzySeconds, final Property property, final double value, final BulkDto bulk) {
|
||||
newScheduleEntry(scheduleId, enabled, ScheduleEntryType.TIME, null, hour, minute, second, fuzzySeconds, property, value, bulk);
|
||||
private void createTime(final long scheduleId, final boolean enabled, final int hour, final int minute, final int second, final int fuzzySeconds, final double value, final BulkDto bulk) {
|
||||
newScheduleEntry(scheduleId, enabled, ScheduleEntryType.TIME, null, hour, minute, second, fuzzySeconds, value, bulk);
|
||||
}
|
||||
|
||||
private void newScheduleEntry(final long scheduleId, final boolean enabled, final ScheduleEntryType type, final Zenith zenith, final int hour, final int minute, final int second, final int fuzzySeconds, final Property property, final double value, final BulkDto bulk) {
|
||||
private void createSunrise(final long scheduleId, final boolean enabled, final Zenith zenith, final int fuzzySeconds, final double value, final BulkDto bulk) {
|
||||
newScheduleEntry(scheduleId, enabled, ScheduleEntryType.SUNRISE, zenith, 0, 0, 0, fuzzySeconds, value, bulk);
|
||||
}
|
||||
|
||||
private void newScheduleEntry(final long scheduleId, final boolean enabled, final ScheduleEntryType type, final Zenith zenith, final int hour, final int minute, final int second, final int fuzzySeconds, final double value, final BulkDto bulk) {
|
||||
final long id = scheduleEntryController.create(scheduleId).getId();
|
||||
scheduleEntryController.setEnabled(id, enabled);
|
||||
scheduleEntryController.setType(id, type.name());
|
||||
if (zenith != null) {
|
||||
scheduleEntryController.setZenith(id, zenith.degrees() + "");
|
||||
}
|
||||
scheduleEntryController.setHour(id, hour);
|
||||
scheduleEntryController.setMinute(id, minute);
|
||||
scheduleEntryController.setSecond(id, second);
|
||||
scheduleEntryController.daySecond(id, (hour * 60 + minute) * 60 + second);
|
||||
scheduleEntryController.setFuzzySeconds(id, fuzzySeconds);
|
||||
scheduleEntryController.setProperty(id, property == null ? null : property.getId());
|
||||
scheduleEntryController.setValue(id, value);
|
||||
scheduleEntryController.setBulk(id, bulk == null ? null : bulk.getId());
|
||||
scheduleEntryController.setSkip(id, 1);
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package de.ph87.homeautomation.schedule.entry;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("schedule/entry")
|
||||
@RequiredArgsConstructor
|
||||
@ -75,19 +77,9 @@ public class ScheduleEntryController {
|
||||
return scheduleEntryWriter.setZenith(id, Double.parseDouble(value));
|
||||
}
|
||||
|
||||
@PostMapping("set/{id}/hour")
|
||||
public ScheduleEntryDto setHour(@PathVariable final long id, @RequestBody final int value) {
|
||||
return scheduleEntryWriter.setHour(id, value);
|
||||
}
|
||||
|
||||
@PostMapping("set/{id}/minute")
|
||||
public ScheduleEntryDto setMinute(@PathVariable final long id, @RequestBody final int value) {
|
||||
return scheduleEntryWriter.setMinute(id, value);
|
||||
}
|
||||
|
||||
@PostMapping("set/{id}/second")
|
||||
public ScheduleEntryDto setSecond(@PathVariable final long id, @RequestBody final int value) {
|
||||
return scheduleEntryWriter.setSecond(id, value);
|
||||
@PostMapping("set/{id}/daySecond")
|
||||
public ScheduleEntryDto daySecond(@PathVariable final long id, @RequestBody final int daySecond) {
|
||||
return scheduleEntryWriter.daySecond(id, daySecond);
|
||||
}
|
||||
|
||||
@PostMapping("set/{id}/fuzzySeconds")
|
||||
|
||||
@ -8,8 +8,10 @@ import de.ph87.homeautomation.schedule.ScheduleWriter;
|
||||
import de.ph87.homeautomation.web.BadRequestException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
@ -24,6 +26,12 @@ import static java.lang.Math.min;
|
||||
@RequiredArgsConstructor
|
||||
public class ScheduleEntryWriter {
|
||||
|
||||
private static final int MINUTE_SECONDS = 60;
|
||||
|
||||
private static final int HOUR_SECONDS = 60 * MINUTE_SECONDS;
|
||||
|
||||
private static final int DAY_SECONDS = 24 * HOUR_SECONDS;
|
||||
|
||||
private final ScheduleEntryReader scheduleEntryReader;
|
||||
|
||||
private final ScheduleReader scheduleReader;
|
||||
@ -114,16 +122,19 @@ public class ScheduleEntryWriter {
|
||||
return modifyValue(id, ScheduleEntry::setZenith, value);
|
||||
}
|
||||
|
||||
public ScheduleEntryDto setHour(final long id, final int value) {
|
||||
return modifyValue(id, ScheduleEntry::setHour, value);
|
||||
public ScheduleEntryDto daySecond(final long id, final int daySecond) {
|
||||
if (daySecond < 0 || daySecond > DAY_SECONDS) {
|
||||
log.error("daySecond must be in [0, {}] but is: {}", DAY_SECONDS, daySecond);
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
public ScheduleEntryDto setMinute(final long id, final int value) {
|
||||
return modifyValue(id, ScheduleEntry::setMinute, value);
|
||||
}
|
||||
|
||||
public ScheduleEntryDto setSecond(final long id, final int value) {
|
||||
return modifyValue(id, ScheduleEntry::setSecond, value);
|
||||
final int hour = daySecond / HOUR_SECONDS;
|
||||
final int minute = (daySecond / MINUTE_SECONDS) % 60;
|
||||
final int second = daySecond % 60;
|
||||
return modify(id, entry -> {
|
||||
entry.setHour(hour);
|
||||
entry.setMinute(minute);
|
||||
entry.setSecond(second);
|
||||
});
|
||||
}
|
||||
|
||||
public ScheduleEntryDto setFuzzySeconds(final long id, final int value) {
|
||||
|
||||