mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-03-12 01:32:12 +00:00
feat: timetable module - schedule and calendar
This commit is contained in:
324
src/app/modules/schedule/page/calendar-view.component.ts
Normal file
324
src/app/modules/schedule/page/calendar-view.component.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {IonDatetime, Platform} from '@ionic/angular';
|
||||
import {SCUuid} from '@openstapps/core';
|
||||
import {last} from 'lodash-es';
|
||||
import moment, {Moment} from 'moment';
|
||||
import {DateFormatPipe} from 'ngx-moment';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {SharedAxisChoreographer} from '../../../animation/animation-choreographer';
|
||||
import {
|
||||
materialFade,
|
||||
materialManualFade,
|
||||
materialSharedAxisX,
|
||||
} from '../../../animation/material-motion';
|
||||
import {ScheduleProvider} from '../schedule.provider';
|
||||
import {ScheduleEvent, ScheduleResponsiveBreakpoint} from './schema/schema';
|
||||
|
||||
/**
|
||||
* Component that displays the schedule
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-calendar-view',
|
||||
templateUrl: 'calendar-view.html',
|
||||
styleUrls: ['calendar-view.scss'],
|
||||
animations: [materialFade, materialSharedAxisX, materialManualFade],
|
||||
})
|
||||
export class CalendarViewComponent implements OnDestroy, OnInit, OnChanges {
|
||||
/**
|
||||
* UUID subscription
|
||||
*/
|
||||
private _uuidSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* The day that is routed to
|
||||
*/
|
||||
protected routeDate: Moment;
|
||||
|
||||
/**
|
||||
* @see {blockDateTimeChange}
|
||||
*/
|
||||
anticipateDatetimeChangeBlocked = false;
|
||||
|
||||
/**
|
||||
* @see {blockDateTimeChange}
|
||||
*/
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
readonly anticipateDatetimeChangeTimeoutMs: 100;
|
||||
|
||||
/**
|
||||
* Animation state for cards
|
||||
*/
|
||||
cardsAnimationState: 'in' | 'out' = 'out';
|
||||
|
||||
/**
|
||||
* The cursor
|
||||
*/
|
||||
@ViewChild('cursor', {read: HTMLElement}) cursor?: HTMLElement;
|
||||
|
||||
/**
|
||||
* Choreographer
|
||||
*/
|
||||
dateLabelsChoreographer: SharedAxisChoreographer<Moment[]>;
|
||||
|
||||
/**
|
||||
* The date range to initially display
|
||||
*/
|
||||
displayDates: Moment[][] = [];
|
||||
|
||||
/**
|
||||
* Hours for grid
|
||||
*/
|
||||
readonly hours: number[];
|
||||
|
||||
/**
|
||||
* Height of the slides based on the displayed hours
|
||||
*/
|
||||
readonly hoursAmount: number;
|
||||
|
||||
/**
|
||||
* Range of hours to display
|
||||
*/
|
||||
@Input() readonly hoursRange = {
|
||||
from: 5,
|
||||
to: 22,
|
||||
};
|
||||
|
||||
/**
|
||||
* Layout of the schedule
|
||||
*/
|
||||
@Input() layout: ScheduleResponsiveBreakpoint;
|
||||
|
||||
/**
|
||||
* Get the date format for the date field
|
||||
*/
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
localDateFormat = moment.localeData().longDateFormat('L');
|
||||
|
||||
/**
|
||||
* Route fragment
|
||||
*/
|
||||
routeFragment = 'schedule/calendar';
|
||||
|
||||
/**
|
||||
* Vertical scale of the schedule (distance between hour lines)
|
||||
*/
|
||||
scale = 60;
|
||||
|
||||
/**
|
||||
* date -> (uid -> event)
|
||||
*/
|
||||
testSchedule: Record<string, Record<SCUuid, ScheduleEvent>> = {};
|
||||
|
||||
/**
|
||||
* UUIDs
|
||||
*/
|
||||
uuids: SCUuid[];
|
||||
|
||||
constructor(
|
||||
protected readonly scheduleProvider: ScheduleProvider,
|
||||
protected readonly activatedRoute: ActivatedRoute,
|
||||
protected readonly datePipe: DateFormatPipe,
|
||||
protected readonly platform: Platform,
|
||||
) {
|
||||
// This could be done directly on the properties too instead of
|
||||
// here in the constructor, but because of TSLint member ordering,
|
||||
// some properties wouldn't be initialized, and if you disable
|
||||
// member ordering, auto-fixing the file can still cause reordering
|
||||
// of properties.
|
||||
this.hoursAmount = this.hoursRange.to - this.hoursRange.from + 1;
|
||||
this.hours = [...Array.from({length: this.hoursAmount}).keys()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Because of some stupid Ionic implementation, there is no
|
||||
* way to wait for the datetime picker to be dismissed without
|
||||
* listening for (ionChange). Unfortunately that also includes
|
||||
* changes caused by a page change, so whenever we do that,
|
||||
* we have to block the event for a few milliseconds.
|
||||
*/
|
||||
blockDateTimeChange() {
|
||||
this.anticipateDatetimeChangeBlocked = true;
|
||||
setTimeout(() => {
|
||||
this.anticipateDatetimeChangeBlocked = false;
|
||||
}, this.anticipateDatetimeChangeTimeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine displayed dates according to display size
|
||||
*/
|
||||
determineDisplayDates() {
|
||||
// let's boldly assume that we at least display one day
|
||||
|
||||
const out = [moment(this.routeDate).startOf(this.layout.startOf)];
|
||||
for (let i = 1; i < this.layout.days; i++) {
|
||||
out.push(out[0].clone().add(i, 'day'));
|
||||
}
|
||||
|
||||
this.displayDates = [
|
||||
out.map(it => it.clone().subtract(this.layout.days, 'days')),
|
||||
out,
|
||||
out.map(it => it.clone().add(this.layout.days, 'days')),
|
||||
];
|
||||
|
||||
this.dateLabelsChoreographer?.changeViewForState(this.getDateLabels(), 0);
|
||||
// void this.mainSlides.slideTo(this.mode === 'schedule' ? 0 : 1, 0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date labels
|
||||
*/
|
||||
getDateLabels(): Moment[] {
|
||||
return (this.displayDates[1] ?? this.displayDates[0]).map(it => it.clone());
|
||||
}
|
||||
|
||||
/**
|
||||
* Jump to a date
|
||||
*/
|
||||
jumpToDate(alt: IonDatetime, offset = 0, date?: Moment) {
|
||||
if (this.anticipateDatetimeChangeBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newDate = (date ?? moment(alt.value)).subtract(offset, 'days');
|
||||
const direction = this.routeDate.isBefore(newDate)
|
||||
? 1
|
||||
: this.routeDate.isAfter(newDate)
|
||||
? -1
|
||||
: 0;
|
||||
|
||||
this.blockDateTimeChange();
|
||||
this.routeDate = newDate;
|
||||
this.determineDisplayDates();
|
||||
|
||||
this.dateLabelsChoreographer.changeViewForState(
|
||||
this.getDateLabels(),
|
||||
direction,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load events
|
||||
*/
|
||||
async loadEvents(): Promise<void> {
|
||||
this.cardsAnimationState = 'out';
|
||||
const dateSeries = await this.scheduleProvider.getDateSeries(this.uuids);
|
||||
|
||||
this.testSchedule = {};
|
||||
|
||||
for (const series of dateSeries) {
|
||||
for (const date of series.dates) {
|
||||
const parsedDate = moment(date).startOf('day').unix();
|
||||
|
||||
// fall back to default
|
||||
(this.testSchedule[parsedDate] ?? (this.testSchedule[parsedDate] = {}))[
|
||||
series.uid
|
||||
] = {
|
||||
dateSeries: series,
|
||||
time: {
|
||||
start: moment(date).hours(),
|
||||
duration: series.duration,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.cursor?.scrollIntoView();
|
||||
this.cardsAnimationState = 'in';
|
||||
}
|
||||
|
||||
/**
|
||||
* On Changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const layout = changes.layout?.currentValue as
|
||||
| ScheduleResponsiveBreakpoint
|
||||
| undefined;
|
||||
if (layout) {
|
||||
this.determineDisplayDates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OnDestroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this._uuidSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
ngOnInit() {
|
||||
this._uuidSubscription = this.scheduleProvider.uuids$.subscribe(
|
||||
async result => {
|
||||
this.uuids = result;
|
||||
await this.loadEvents();
|
||||
},
|
||||
);
|
||||
|
||||
let dayString: string | number | null =
|
||||
this.activatedRoute.snapshot.paramMap.get('date');
|
||||
if (dayString == undefined || dayString === 'now') {
|
||||
const urlFragment: string = last(window.location.href.split('/')) ?? '';
|
||||
|
||||
dayString = /^\d{4}-\d{2}-\d{2}$/.test(urlFragment)
|
||||
? urlFragment
|
||||
: moment.now();
|
||||
}
|
||||
this.routeDate = moment(dayString).startOf('day');
|
||||
this.dateLabelsChoreographer = new SharedAxisChoreographer(
|
||||
this.getDateLabels(),
|
||||
);
|
||||
|
||||
this.determineDisplayDates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change page
|
||||
*/
|
||||
async onPageChange(direction: number) {
|
||||
this.blockDateTimeChange();
|
||||
const amount = direction * this.displayDates[0].length;
|
||||
|
||||
this.routeDate.add(amount, 'days');
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'',
|
||||
`#/${this.routeFragment}/${this.routeDate.format('YYYY-MM-DD')}`,
|
||||
);
|
||||
|
||||
for (const slide of this.displayDates) {
|
||||
for (const date of slide) {
|
||||
date.add(amount, 'days');
|
||||
}
|
||||
}
|
||||
|
||||
this.dateLabelsChoreographer.changeViewForState(
|
||||
this.getDateLabels(),
|
||||
direction > 0 ? 1 : direction < 0 ? -1 : 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
86
src/app/modules/schedule/page/calendar-view.html
Normal file
86
src/app/modules/schedule/page/calendar-view.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<div>
|
||||
<ion-button fill="clear" class="left-button" (click)="mainSlides.prevPage()">
|
||||
<ion-icon slot="icon-only" name="chevron-back-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" class="right-button" (click)="mainSlides.nextPage()">
|
||||
<ion-icon slot="icon-only" name="chevron-forward-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-item>
|
||||
<ion-grid
|
||||
class="day-labels"
|
||||
[@materialSharedAxisX]="dateLabelsChoreographer.animationState"
|
||||
(@materialSharedAxisX.done)="dateLabelsChoreographer.animationDone()"
|
||||
>
|
||||
<ion-row>
|
||||
<ion-col
|
||||
*ngFor="
|
||||
let item of dateLabelsChoreographer.currentValue;
|
||||
let idx = index
|
||||
"
|
||||
>
|
||||
<ion-button expand="block" fill="clear" (click)="datetime.open()">
|
||||
<ion-label>
|
||||
{{
|
||||
item
|
||||
| amDateFormat: ((item | dateIsThis: 'week') ? 'dddd' : 'll')
|
||||
}}
|
||||
</ion-label>
|
||||
</ion-button>
|
||||
|
||||
<!-- This poor datetime element is a phantom element to provide us with a date picker -->
|
||||
<ion-datetime
|
||||
#datetime
|
||||
[displayFormat]="localDateFormat"
|
||||
[value]="item.toISOString()"
|
||||
(ionChange)="jumpToDate(datetime, idx)"
|
||||
>
|
||||
</ion-datetime>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-item>
|
||||
</div>
|
||||
<ion-content>
|
||||
<stapps-infinite-slides
|
||||
#mainSlides
|
||||
(pageChangeCallback)="onPageChange($event.direction)"
|
||||
>
|
||||
<ion-slide class="slide" *ngFor="let slide of displayDates">
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let item of slide">
|
||||
<div
|
||||
class="vertical-line"
|
||||
[style.height.px]="hoursAmount * scale"
|
||||
></div>
|
||||
<stapps-schedule-cursor
|
||||
*ngIf="item | dateIsThis: 'date'"
|
||||
[hoursRange]="hoursRange"
|
||||
[scale]="scale"
|
||||
#cursor
|
||||
>
|
||||
</stapps-schedule-cursor>
|
||||
<div [@materialManualFade]="cardsAnimationState">
|
||||
<stapps-schedule-card
|
||||
*ngFor="let entry of testSchedule[item.unix()] | keyvalue"
|
||||
[scheduleEvent]="entry.value"
|
||||
[fromHour]="hoursRange.from"
|
||||
[scale]="scale"
|
||||
>
|
||||
</stapps-schedule-card>
|
||||
</div>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-slide>
|
||||
</stapps-infinite-slides>
|
||||
|
||||
<div
|
||||
class="hour-lines"
|
||||
*ngFor="let i of hours"
|
||||
[style.marginTop.px]="i * scale"
|
||||
>
|
||||
<ion-label>{{ i + hoursRange.from }}:00</ion-label>
|
||||
<hr class="horizontal-line" />
|
||||
</div>
|
||||
</ion-content>
|
||||
97
src/app/modules/schedule/page/calendar-view.scss
Normal file
97
src/app/modules/schedule/page/calendar-view.scss
Normal file
@@ -0,0 +1,97 @@
|
||||
div {
|
||||
position: relative;
|
||||
|
||||
.left-button, .right-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.left-button {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right-button {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.day-labels {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 8px 20px 8px 30px;
|
||||
width: 100%;
|
||||
|
||||
ion-row {
|
||||
padding-right: 20px;
|
||||
|
||||
ion-col {
|
||||
ion-button {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
font-size: large;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// phantom element
|
||||
ion-datetime {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
height: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slide {
|
||||
ion-grid {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
padding-top: 8px;
|
||||
|
||||
ion-row {
|
||||
ion-col {
|
||||
width: 100%;
|
||||
|
||||
.vertical-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-left: 1px solid #dbdbdb;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
stapps-schedule-card {
|
||||
z-index: 4;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hour-lines {
|
||||
top: 0;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
|
||||
ion-label {
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
.horizontal-line {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
border-top: 1px solid #dbdbdb;
|
||||
}
|
||||
}
|
||||
107
src/app/modules/schedule/page/grid/infinite-slides.component.ts
Normal file
107
src/app/modules/schedule/page/grid/infinite-slides.component.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Component, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||
import {IonSlides} from '@ionic/angular';
|
||||
|
||||
/**
|
||||
* Component that can display infinite slides
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-infinite-slides',
|
||||
templateUrl: 'infinite-slides.html',
|
||||
styleUrls: ['infinite-slides.scss'],
|
||||
})
|
||||
export class InfiniteSlidesComponent {
|
||||
/**
|
||||
* If the view was initialized
|
||||
*/
|
||||
initialized = false;
|
||||
|
||||
/**
|
||||
* Callback for when the page has changed
|
||||
*
|
||||
* The caller needs to replace the component here
|
||||
*/
|
||||
@Output() pageChangeCallback: EventEmitter<{
|
||||
/**
|
||||
* The current page
|
||||
*/
|
||||
currentPage: number;
|
||||
|
||||
/**
|
||||
* The direction that was scrolled
|
||||
*/
|
||||
direction: number;
|
||||
}> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* The virtual page we are currently on
|
||||
*/
|
||||
page = 0;
|
||||
|
||||
/**
|
||||
* Options for IonSlides
|
||||
*/
|
||||
@Input() slideOpts = {
|
||||
initialSlide: 1,
|
||||
speed: 200,
|
||||
loop: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Slider element
|
||||
*/
|
||||
@ViewChild('slides') slides: IonSlides;
|
||||
|
||||
/**
|
||||
* Slide to next page
|
||||
*/
|
||||
async nextPage() {
|
||||
await this.slides.slideNext(this.slideOpts.speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change page
|
||||
*/
|
||||
async onPageChange(direction: number) {
|
||||
if (!this.initialized) {
|
||||
// setting the initial page to 1 causes a page change to
|
||||
// be emitted initially, which intern would cause the
|
||||
// page to actually change one to far, so we listen for
|
||||
// that first page change and skip it
|
||||
this.initialized = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.page += direction;
|
||||
|
||||
this.pageChangeCallback.emit({
|
||||
currentPage: this.page,
|
||||
direction: direction,
|
||||
});
|
||||
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
this.slides.slideTo(1, 0, false).then();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide to previous page
|
||||
*/
|
||||
async prevPage() {
|
||||
await this.slides.slidePrev(this.slideOpts.speed);
|
||||
}
|
||||
}
|
||||
9
src/app/modules/schedule/page/grid/infinite-slides.html
Normal file
9
src/app/modules/schedule/page/grid/infinite-slides.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<ion-slides
|
||||
#slides
|
||||
pager="false"
|
||||
[options]="slideOpts"
|
||||
(ionSlideNextEnd)="onPageChange(1)"
|
||||
(ionSlidePrevEnd)="onPageChange(-1)"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</ion-slides>
|
||||
4
src/app/modules/schedule/page/grid/infinite-slides.scss
Normal file
4
src/app/modules/schedule/page/grid/infinite-slides.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
ion-slides {
|
||||
width: 100%;
|
||||
height: 1100px; // BIG TODO: This is completely bypasses the scale parameter
|
||||
}
|
||||
100
src/app/modules/schedule/page/grid/schedule-card.component.ts
Normal file
100
src/app/modules/schedule/page/grid/schedule-card.component.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import moment from 'moment';
|
||||
import {ScheduleProvider} from '../../schedule.provider';
|
||||
import {ScheduleEvent} from '../schema/schema';
|
||||
|
||||
/**
|
||||
* Component that can display a schedule event
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-schedule-card',
|
||||
templateUrl: 'schedule-card.html',
|
||||
styleUrls: ['../../../data/list/data-list-item.scss', 'schedule-card.scss'],
|
||||
})
|
||||
export class ScheduleCardComponent implements OnInit {
|
||||
/**
|
||||
* The hour from which on the schedule is displayed
|
||||
*/
|
||||
@Input() fromHour = 0;
|
||||
|
||||
/**
|
||||
* Card Y start position
|
||||
*/
|
||||
fromY = 0;
|
||||
|
||||
/**
|
||||
* Card Y end position
|
||||
*/
|
||||
height = 0;
|
||||
|
||||
/**
|
||||
* Show the card without a top offset
|
||||
*/
|
||||
@Input() noOffset = false;
|
||||
|
||||
/**
|
||||
* The scale of the schedule
|
||||
*/
|
||||
@Input() scale = 1;
|
||||
|
||||
/**
|
||||
* The event
|
||||
*/
|
||||
@Input() scheduleEvent: ScheduleEvent;
|
||||
|
||||
/**
|
||||
* The title of the event
|
||||
*/
|
||||
title: string;
|
||||
|
||||
constructor(private readonly scheduleProvider: ScheduleProvider) {}
|
||||
|
||||
/**
|
||||
* Get the note text
|
||||
*/
|
||||
getNote(): string | undefined {
|
||||
return 'categories' in this.scheduleEvent.dateSeries.event
|
||||
? this.scheduleEvent.dateSeries.event.categories?.join(', ')
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializer
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.fromY = this.noOffset ? 0 : this.scheduleEvent.time.start;
|
||||
this.height = moment.duration(this.scheduleEvent.time.duration).asHours();
|
||||
|
||||
this.title = this.scheduleEvent.dateSeries.event.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the event
|
||||
*/
|
||||
removeEvent(): false {
|
||||
if (confirm('Remove event?')) {
|
||||
this.scheduleProvider.uuids$.next(
|
||||
this.scheduleProvider.uuids$.value.filter(
|
||||
it => it !== this.scheduleEvent.dateSeries.uid,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// to prevent event propagation
|
||||
return false;
|
||||
}
|
||||
}
|
||||
30
src/app/modules/schedule/page/grid/schedule-card.html
Normal file
30
src/app/modules/schedule/page/grid/schedule-card.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<ion-card
|
||||
[style.height.px]="height * scale"
|
||||
[style.marginTop.px]="(fromY - fromHour) * scale - 5"
|
||||
[routerLink]="['/data-detail', scheduleEvent.dateSeries.event.uid]"
|
||||
>
|
||||
<ion-card-header mode="md">
|
||||
<ion-card-title>
|
||||
{{
|
||||
this.scheduleEvent.dateSeries.event.name
|
||||
| nullishCoalesce: this.scheduleEvent.dateSeries.name
|
||||
}}
|
||||
</ion-card-title>
|
||||
<ion-card-subtitle>
|
||||
<ion-icon name="calendar"></ion-icon>
|
||||
<span class="repetitions">
|
||||
{{ scheduleEvent.dateSeries.frequency }}
|
||||
until
|
||||
{{
|
||||
scheduleEvent.dateSeries.dates | last | amDateFormat: 'DD. MMM YYYY'
|
||||
}}
|
||||
</span>
|
||||
</ion-card-subtitle>
|
||||
</ion-card-header>
|
||||
<ion-card-content>
|
||||
<ion-note>
|
||||
{{ getNote() }}
|
||||
</ion-note>
|
||||
</ion-card-content>
|
||||
<div></div>
|
||||
</ion-card>
|
||||
37
src/app/modules/schedule/page/grid/schedule-card.scss
Normal file
37
src/app/modules/schedule/page/grid/schedule-card.scss
Normal file
@@ -0,0 +1,37 @@
|
||||
ion-card {
|
||||
z-index: 2;
|
||||
|
||||
ion-grid {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
ion-row {
|
||||
ion-col {
|
||||
height: 5px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ion-card-header {
|
||||
height: available;
|
||||
width: 100%;
|
||||
|
||||
ion-card-title {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 255) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import moment from 'moment';
|
||||
import {HoursRange} from '../schema/schema';
|
||||
|
||||
/**
|
||||
* Component that displays the schedule
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-schedule-cursor',
|
||||
templateUrl: 'schedule-cursor.html',
|
||||
styleUrls: ['schedule-cursor.scss'],
|
||||
})
|
||||
export class ScheduleCursorComponent implements OnInit {
|
||||
/**
|
||||
* Cursor update
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore unused
|
||||
private cursorInterval: NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* Range of hours to display
|
||||
*/
|
||||
@Input() readonly hoursRange: HoursRange;
|
||||
|
||||
/**
|
||||
* Cursor
|
||||
*/
|
||||
now = ScheduleCursorComponent.getCursorTime();
|
||||
|
||||
/**
|
||||
* Vertical scale of the schedule (distance between hour lines)
|
||||
*/
|
||||
@Input() readonly scale: number;
|
||||
|
||||
/**
|
||||
* Get a floating point time 0..24
|
||||
*/
|
||||
static getCursorTime(): number {
|
||||
const mnt = moment(moment.now());
|
||||
|
||||
const hh = mnt.hours();
|
||||
const mm = mnt.minutes();
|
||||
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
return hh + mm / 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.cursorInterval = setInterval(async () => {
|
||||
this.now = ScheduleCursorComponent.getCursorTime();
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
}, 1000 * 60 /*1 Minute*/);
|
||||
}
|
||||
}
|
||||
6
src/app/modules/schedule/page/grid/schedule-cursor.html
Normal file
6
src/app/modules/schedule/page/grid/schedule-cursor.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<div [style.marginTop.px]="(now - hoursRange.from) * scale">
|
||||
<div>
|
||||
<hr />
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
38
src/app/modules/schedule/page/grid/schedule-cursor.scss
Normal file
38
src/app/modules/schedule/page/grid/schedule-cursor.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
div {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
z-index: 0;
|
||||
|
||||
div {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
|
||||
hr {
|
||||
width: calc(100% - 8px);
|
||||
position: absolute;
|
||||
margin-left: 4px;
|
||||
margin-right: 16px;
|
||||
margin-top: 8px;
|
||||
height: 2px;
|
||||
border-top: 2px solid var(--ion-color-primary);
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
div {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -4px;
|
||||
border-radius: 50% 0 50% 50%;
|
||||
transform: rotateZ(45deg);
|
||||
background-color: var(--ion-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {SCSearchFilter, SCThingType} from '@openstapps/core';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Component({
|
||||
selector: 'modal-event-creator',
|
||||
templateUrl: 'modal-event-creator.html',
|
||||
styleUrls: ['modal-event-creator.scss'],
|
||||
})
|
||||
export class ModalEventCreatorComponent {
|
||||
/**
|
||||
* Action when close is pressed
|
||||
*/
|
||||
@Input() dismissAction: () => void;
|
||||
|
||||
/**
|
||||
* Forced filter
|
||||
*/
|
||||
filter: SCSearchFilter = {
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.AcademicEvent,
|
||||
},
|
||||
type: 'value',
|
||||
};
|
||||
}
|
||||
15
src/app/modules/schedule/page/modal/modal-event-creator.html
Normal file
15
src/app/modules/schedule/page/modal/modal-event-creator.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<ion-card-header>
|
||||
<ion-card-title>{{
|
||||
'schedule.addEventModal.addEvent' | translate
|
||||
}}</ion-card-title>
|
||||
<ion-button fill="clear" (click)="dismissAction()">
|
||||
<ion-label>{{ 'schedule.addEventModal.close' | translate }}</ion-label>
|
||||
</ion-button>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content>
|
||||
<stapps-search-page
|
||||
[forcedFilter]="filter"
|
||||
[itemRouting]="false"
|
||||
></stapps-search-page>
|
||||
</ion-card-content>
|
||||
17
src/app/modules/schedule/page/modal/modal-event-creator.scss
Normal file
17
src/app/modules/schedule/page/modal/modal-event-creator.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
ion-card-header {
|
||||
ion-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
height: 100%;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
stapps-data-list {
|
||||
height: available;
|
||||
}
|
||||
}
|
||||
243
src/app/modules/schedule/page/schedule-page.component.ts
Normal file
243
src/app/modules/schedule/page/schedule-page.component.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
HostListener,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {AnimationController, ModalController} from '@ionic/angular';
|
||||
import {last} from 'lodash-es';
|
||||
import {SharedAxisChoreographer} from '../../../animation/animation-choreographer';
|
||||
import {materialSharedAxisX} from '../../../animation/material-motion';
|
||||
import {ModalEventCreatorComponent} from './modal/modal-event-creator.component';
|
||||
import {ScheduleResponsiveBreakpoint} from './schema/schema';
|
||||
import {animate, style, transition, trigger} from '@angular/animations';
|
||||
|
||||
/**
|
||||
* This needs to be sorted by break point low -> high
|
||||
*
|
||||
* Last entry must have `until: Infinity`
|
||||
*/
|
||||
const responsiveConfig: ScheduleResponsiveBreakpoint[] = [
|
||||
{
|
||||
until: 768,
|
||||
days: 1,
|
||||
startOf: 'day',
|
||||
},
|
||||
{
|
||||
until: 1700,
|
||||
days: 3,
|
||||
startOf: 'day',
|
||||
},
|
||||
{
|
||||
until: Number.POSITIVE_INFINITY,
|
||||
days: 7,
|
||||
startOf: 'week',
|
||||
},
|
||||
];
|
||||
|
||||
const fabAnimations = trigger('fabAnimation', [
|
||||
transition(':leave', [
|
||||
style({opacity: 1, transform: 'translate(0, 0) scale(1)'}),
|
||||
animate(
|
||||
'100ms ease-in',
|
||||
style({opacity: 0, transform: 'translate(-5vw, -5vh) scale(200%)'}),
|
||||
),
|
||||
]),
|
||||
transition(':enter', [
|
||||
style({opacity: 0, transform: 'translate(-5vw, -5vh) scale(200%)'}),
|
||||
animate(
|
||||
'200ms ease-out',
|
||||
style({opacity: 1, transform: 'translate(0, 0) scale(1)'}),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Component that displays the schedule
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-schedule-page',
|
||||
templateUrl: 'schedule-page.html',
|
||||
styleUrls: ['schedule-page.scss'],
|
||||
animations: [materialSharedAxisX, fabAnimations],
|
||||
})
|
||||
export class SchedulePageComponent implements OnInit, AfterViewInit {
|
||||
/**
|
||||
* Current width of the window
|
||||
*/
|
||||
private currentWindowWidth: number = window.innerWidth;
|
||||
|
||||
/**
|
||||
* Actual Segment Tab
|
||||
*/
|
||||
actualSegmentValue?: string | null;
|
||||
|
||||
fabVisible = true;
|
||||
|
||||
/**
|
||||
* Layout
|
||||
*/
|
||||
layout: ScheduleResponsiveBreakpoint = SchedulePageComponent.getDaysToDisplay(
|
||||
this.currentWindowWidth,
|
||||
);
|
||||
|
||||
/**
|
||||
* Vertical scale of the schedule (distance between hour lines)
|
||||
*/
|
||||
scale = 60;
|
||||
|
||||
@ViewChild('segment') segmentView!: HTMLIonSegmentElement;
|
||||
|
||||
/**
|
||||
* Choreographer for the tab switching
|
||||
*/
|
||||
tabChoreographer: SharedAxisChoreographer<string | null | undefined>;
|
||||
|
||||
/**
|
||||
* Weekly config for schedule
|
||||
*/
|
||||
weeklyConfig: ScheduleResponsiveBreakpoint = {
|
||||
until: Number.POSITIVE_INFINITY,
|
||||
days: 7,
|
||||
startOf: 'week',
|
||||
};
|
||||
|
||||
/**
|
||||
* Amount of days that should be shown according to current display width
|
||||
*/
|
||||
static getDaysToDisplay(width: number): ScheduleResponsiveBreakpoint {
|
||||
// the search could be optimized, but probably would have little
|
||||
// actual effect with five entries.
|
||||
// we can be sure we get an hit when the last value.until is infinity
|
||||
// (unless someone has a display that reaches across the universe)
|
||||
return (
|
||||
responsiveConfig.find(value => width < value.until) ??
|
||||
responsiveConfig[responsiveConfig.length - 1]
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly modalController: ModalController,
|
||||
private readonly activatedRoute: ActivatedRoute,
|
||||
private readonly animationController: AnimationController,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.tabChoreographer = new SharedAxisChoreographer(
|
||||
this.activatedRoute.snapshot.paramMap.get('mode'),
|
||||
['calendar', 'recurring', 'single'],
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.segmentView.value = this.tabChoreographer.currentValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize callback
|
||||
*
|
||||
* Note: this may not fire when the browser transfers from full screen to windowed
|
||||
* (Firefox & Chrome tested)
|
||||
*/
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(_: UIEvent) {
|
||||
const current = SchedulePageComponent.getDaysToDisplay(
|
||||
this.currentWindowWidth,
|
||||
);
|
||||
const next = SchedulePageComponent.getDaysToDisplay(window.innerWidth);
|
||||
this.currentWindowWidth = window.innerWidth;
|
||||
|
||||
if (current.days === next.days) {
|
||||
this.layout = next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the segment changes
|
||||
*/
|
||||
onSegmentChange() {
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'',
|
||||
`/#/schedule/${this.segmentView.value}/${last(
|
||||
window.location.href.split('/'),
|
||||
)}`,
|
||||
);
|
||||
this.tabChoreographer.changeViewForState(this.segmentView.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event modal sheet
|
||||
*/
|
||||
async showCreateEventModal() {
|
||||
this.fabVisible = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any,unicorn/consistent-function-scoping
|
||||
const enterAnimation = (baseElement: any) => {
|
||||
const backdropAnimation = this.animationController
|
||||
.create()
|
||||
.addElement(baseElement.querySelector('.modal-wrapper'))
|
||||
.fromTo('opacity', '0', 'var(--backdrop-opacity)');
|
||||
|
||||
const wrapperAnimation = this.animationController
|
||||
.create()
|
||||
.addElement(baseElement.querySelector('.modal-wrapper'))
|
||||
.keyframes([
|
||||
{
|
||||
opacity: '0',
|
||||
transform: 'translate(30vw, 30vh) scale(0.5)',
|
||||
},
|
||||
{
|
||||
opacity: '1',
|
||||
transform: 'translate(0, 0) scale(1)',
|
||||
},
|
||||
]);
|
||||
|
||||
return this.animationController
|
||||
.create()
|
||||
.addElement(baseElement)
|
||||
.easing('ease-out')
|
||||
.duration(150)
|
||||
.addAnimation([backdropAnimation, wrapperAnimation]);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any,unicorn/consistent-function-scoping
|
||||
const leaveAnimation = (baseElement: any) => {
|
||||
return enterAnimation(baseElement).direction('reverse');
|
||||
};
|
||||
|
||||
const modal = await this.modalController.create({
|
||||
component: ModalEventCreatorComponent,
|
||||
swipeToClose: true,
|
||||
cssClass: 'add-modal',
|
||||
componentProps: {
|
||||
dismissAction: () => {
|
||||
modal.dismiss();
|
||||
},
|
||||
},
|
||||
enterAnimation,
|
||||
leaveAnimation,
|
||||
});
|
||||
|
||||
await modal.present();
|
||||
await modal.onWillDismiss();
|
||||
|
||||
this.fabVisible = true;
|
||||
}
|
||||
}
|
||||
57
src/app/modules/schedule/page/schedule-page.html
Normal file
57
src/app/modules/schedule/page/schedule-page.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
~ Copyright (C) 2021 StApps
|
||||
~ This program is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU General Public License as published by the Free
|
||||
~ Software Foundation, version 3.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
~ more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with
|
||||
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<ion-header>
|
||||
<ion-segment #segment value="calendar" (ionChange)="onSegmentChange()">
|
||||
<ion-segment-button value="calendar">
|
||||
<ion-label>{{ 'schedule.calendar' | translate }}</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="recurring">
|
||||
<ion-label>{{ 'schedule.recurring' | translate }}</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="single">
|
||||
<ion-label>{{ 'schedule.single' | translate }}</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
</ion-header>
|
||||
|
||||
<div
|
||||
[ngSwitch]="tabChoreographer.currentValue"
|
||||
[@materialSharedAxisX]="tabChoreographer.animationState"
|
||||
(@materialSharedAxisX.done)="tabChoreographer.animationDone()"
|
||||
>
|
||||
<stapps-calendar-view
|
||||
*ngSwitchCase="'calendar'"
|
||||
[layout]="layout"
|
||||
></stapps-calendar-view>
|
||||
<!-- Schedule view needs full week -->
|
||||
<stapps-schedule-view
|
||||
*ngSwitchCase="'recurring'"
|
||||
[layout]="weeklyConfig"
|
||||
></stapps-schedule-view>
|
||||
<stapps-single-events *ngSwitchCase="'single'"></stapps-single-events>
|
||||
</div>
|
||||
|
||||
<ion-fab
|
||||
*ngIf="fabVisible"
|
||||
@fabAnimation
|
||||
vertical="bottom"
|
||||
horizontal="end"
|
||||
slot="fixed"
|
||||
>
|
||||
<ion-fab-button (click)="showCreateEventModal()">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
26
src/app/modules/schedule/page/schedule-page.scss
Normal file
26
src/app/modules/schedule/page/schedule-page.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
ion-header {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.add-modal {
|
||||
align-items: flex-end !important;
|
||||
justify-content: flex-end !important;
|
||||
|
||||
.modal-wrapper {
|
||||
transform-origin: bottom right !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {SCDateSeries, SCUuid} from '@openstapps/core';
|
||||
import {flatMap, groupBy, omit, sortBy} from 'lodash-es';
|
||||
import moment from 'moment';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {materialFade} from '../../../animation/material-motion';
|
||||
import {ScheduleProvider} from '../schedule.provider';
|
||||
import {ScheduleEvent} from './schema/schema';
|
||||
|
||||
/**
|
||||
* A single event
|
||||
*/
|
||||
export interface ScheduleSingleEvent {
|
||||
/**
|
||||
* Day the event is on
|
||||
*/
|
||||
day: string;
|
||||
|
||||
/**
|
||||
* Event the date is referring to
|
||||
*/
|
||||
event: ScheduleEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that displays single events one after each other
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-single-events',
|
||||
templateUrl: 'schedule-single-events.html',
|
||||
styleUrls: ['schedule-single-events.scss'],
|
||||
animations: [materialFade],
|
||||
})
|
||||
export class ScheduleSingleEventsComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* UUID subscription
|
||||
*/
|
||||
private _uuidSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* The events to display
|
||||
*/
|
||||
private uuids: SCUuid[];
|
||||
|
||||
/**
|
||||
* Events that are displayed
|
||||
*/
|
||||
events: Promise<ScheduleSingleEvent[][]>;
|
||||
|
||||
/**
|
||||
* Scale of the view
|
||||
*/
|
||||
@Input() scale = 60;
|
||||
|
||||
/**
|
||||
* Sorts dates to a list of days with events on each
|
||||
*/
|
||||
static groupDateSeriesToDays(
|
||||
dateSeries: SCDateSeries[],
|
||||
): ScheduleSingleEvent[][] {
|
||||
return sortBy(
|
||||
groupBy(
|
||||
flatMap(dateSeries, event =>
|
||||
event.dates.map(date => ({
|
||||
dateUnix: moment(date).unix(),
|
||||
day: moment(date).startOf('day').toISOString(),
|
||||
event: {
|
||||
dateSeries: event,
|
||||
time: {
|
||||
start:
|
||||
moment(date).hour() +
|
||||
moment(date)
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
.minute() /
|
||||
60,
|
||||
duration: event.duration,
|
||||
},
|
||||
},
|
||||
})),
|
||||
)
|
||||
.sort((a, b) => a.dateUnix - b.dateUnix)
|
||||
.map(event => omit(event, 'dateUnix')),
|
||||
'day',
|
||||
),
|
||||
'day',
|
||||
);
|
||||
}
|
||||
|
||||
constructor(protected readonly scheduleProvider: ScheduleProvider) {}
|
||||
|
||||
/**
|
||||
* Fetch date series items
|
||||
*/
|
||||
async fetchDateSeries(): Promise<ScheduleSingleEvent[][]> {
|
||||
// TODO: only single events
|
||||
const dateSeries = await this.scheduleProvider.getDateSeries(
|
||||
this.uuids,
|
||||
undefined /*TODO*/,
|
||||
moment(moment.now()).startOf('week').toISOString(),
|
||||
);
|
||||
|
||||
// TODO: replace with filter
|
||||
return ScheduleSingleEventsComponent.groupDateSeriesToDays(
|
||||
dateSeries.filter(it => it.frequency === 'single'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnDestroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this._uuidSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
ngOnInit() {
|
||||
this._uuidSubscription = this.scheduleProvider.uuids$.subscribe(
|
||||
async result => {
|
||||
this.uuids = result;
|
||||
this.events = this.fetchDateSeries();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
21
src/app/modules/schedule/page/schedule-single-events.html
Normal file
21
src/app/modules/schedule/page/schedule-single-events.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<ion-content @materialFade>
|
||||
<ion-list lines="none">
|
||||
<ion-item-group *ngFor="let day of events | async" @materialFade>
|
||||
<ion-label class="day-label" color="medium">
|
||||
{{ day[0].day | amDateFormat: 'LL' }}
|
||||
</ion-label>
|
||||
<ion-item *ngFor="let event of day" lines="none">
|
||||
<ion-avatar class="hour-label">
|
||||
{{ event.event.dateSeries.dates[0] | amDateFormat: 'HH:mm' }}
|
||||
</ion-avatar>
|
||||
<stapps-schedule-card
|
||||
class="event-card"
|
||||
[scheduleEvent]="event.event"
|
||||
[noOffset]="true"
|
||||
[scale]="scale"
|
||||
>
|
||||
</stapps-schedule-card>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
17
src/app/modules/schedule/page/schedule-single-events.scss
Normal file
17
src/app/modules/schedule/page/schedule-single-events.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
ion-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hour-label {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.day-label {
|
||||
padding: 16px;
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.event-card {
|
||||
width: 100%
|
||||
}
|
||||
64
src/app/modules/schedule/page/schedule-single-events.spec.ts
Normal file
64
src/app/modules/schedule/page/schedule-single-events.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {SCDateSeries} from '@openstapps/core';
|
||||
import {ScheduleSingleEventsComponent} from './schedule-single-events.component';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('ScheduleSingleEvents', () => {
|
||||
it('should group date series to days', () => {
|
||||
const events: Partial<SCDateSeries>[] = [
|
||||
{
|
||||
dates: ['2021-12-24T10:00Z', '2021-12-24T12:00Z'],
|
||||
duration: 'A',
|
||||
},
|
||||
{
|
||||
dates: ['2021-12-20T10:00Z'],
|
||||
duration: 'B',
|
||||
},
|
||||
{
|
||||
dates: ['2021-12-24T10:15Z'],
|
||||
duration: 'C',
|
||||
},
|
||||
];
|
||||
|
||||
const grouped = ScheduleSingleEventsComponent.groupDateSeriesToDays(
|
||||
events as SCDateSeries[],
|
||||
);
|
||||
const seriesToDate = (series: Partial<SCDateSeries>, index: number) => {
|
||||
const time = moment(series.dates?.[index]);
|
||||
|
||||
return {
|
||||
day: time.clone().startOf('day').toISOString(),
|
||||
event: {
|
||||
dateSeries: series as SCDateSeries,
|
||||
time: {
|
||||
start: time.hour() + time.minute() / 60,
|
||||
duration: series.duration as string,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
expect(grouped).toEqual([
|
||||
[seriesToDate(events[1], 0)],
|
||||
[
|
||||
seriesToDate(events[0], 0),
|
||||
seriesToDate(events[2], 0),
|
||||
seriesToDate(events[0], 1),
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
108
src/app/modules/schedule/page/schedule-view.component.ts
Normal file
108
src/app/modules/schedule/page/schedule-view.component.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2021 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component} from '@angular/core';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {Platform} from '@ionic/angular';
|
||||
import moment from 'moment';
|
||||
import {DateFormatPipe} from 'ngx-moment';
|
||||
import {
|
||||
materialFade,
|
||||
materialManualFade,
|
||||
materialSharedAxisX,
|
||||
} from '../../../animation/material-motion';
|
||||
import {ScheduleProvider} from '../schedule.provider';
|
||||
import {CalendarViewComponent} from './calendar-view.component';
|
||||
|
||||
/**
|
||||
* Component that displays the schedule
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-schedule-view',
|
||||
// this is intentional for extending
|
||||
templateUrl: 'calendar-view.html',
|
||||
styleUrls: ['calendar-view.scss'],
|
||||
animations: [materialFade, materialSharedAxisX, materialManualFade],
|
||||
})
|
||||
export class ScheduleViewComponent extends CalendarViewComponent {
|
||||
/**
|
||||
* Route Fragment
|
||||
*/
|
||||
// @Override
|
||||
routeFragment = 'schedule/recurring';
|
||||
|
||||
constructor(
|
||||
scheduleProvider: ScheduleProvider,
|
||||
activatedRoute: ActivatedRoute,
|
||||
datePipe: DateFormatPipe,
|
||||
platform: Platform,
|
||||
) {
|
||||
super(scheduleProvider, activatedRoute, datePipe, platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine displayed dates according to display size
|
||||
*/
|
||||
// @Override
|
||||
determineDisplayDates() {
|
||||
// let's boldly assume that we at least display one day
|
||||
|
||||
const out = [moment(moment.now()).startOf(this.layout.startOf)];
|
||||
for (let i = 1; i < this.layout.days; i++) {
|
||||
out.push(out[0].clone().add(i, 'day'));
|
||||
}
|
||||
|
||||
this.displayDates = [out];
|
||||
|
||||
// void this.mainSlides.slideTo(this.mode === 'schedule' ? 0 : 1, 0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load events
|
||||
*/
|
||||
// @Override
|
||||
async loadEvents(): Promise<void> {
|
||||
this.cardsAnimationState = 'out';
|
||||
const dateSeries = await this.scheduleProvider.getDateSeries(
|
||||
this.uuids,
|
||||
undefined,
|
||||
moment(moment.now()).startOf('week').toISOString(),
|
||||
);
|
||||
|
||||
this.testSchedule = {};
|
||||
|
||||
for (const series of dateSeries) {
|
||||
if (series.dates.length > 0) {
|
||||
const date = moment(moment.now())
|
||||
.startOf('week')
|
||||
.day(moment(series.dates[0]).day())
|
||||
.unix();
|
||||
|
||||
// fall back to default
|
||||
(this.testSchedule[date] ?? (this.testSchedule[date] = {}))[
|
||||
series.uid
|
||||
] = {
|
||||
dateSeries: series,
|
||||
time: {
|
||||
start: moment(series.dates[0]).hours(),
|
||||
duration: series.duration,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.cursor?.scrollIntoView();
|
||||
this.cardsAnimationState = 'in';
|
||||
}
|
||||
}
|
||||
21
src/app/modules/schedule/page/schedule.service.ts
Normal file
21
src/app/modules/schedule/page/schedule.service.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
/**
|
||||
* MenuService provides bidirectional communication of context menu options and search queries
|
||||
*/
|
||||
@Injectable()
|
||||
export class ScheduleService {}
|
||||
69
src/app/modules/schedule/page/schema/schema.ts
Normal file
69
src/app/modules/schedule/page/schema/schema.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {SCDateSeries, SCISO8601Duration} from '@openstapps/core';
|
||||
import {unitOfTime} from 'moment';
|
||||
|
||||
interface DateRange {
|
||||
duration: SCISO8601Duration;
|
||||
start: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal interface to provide information about a custom event
|
||||
*/
|
||||
export interface ScheduleEvent {
|
||||
/**
|
||||
* UUIDs of things related to the event
|
||||
*/
|
||||
dateSeries: SCDateSeries;
|
||||
|
||||
/**
|
||||
* How long the event goes
|
||||
*/
|
||||
time: DateRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Range of hours
|
||||
*/
|
||||
export interface HoursRange {
|
||||
/**
|
||||
* Start hour
|
||||
*/
|
||||
from: number;
|
||||
|
||||
/**
|
||||
* End hour
|
||||
*/
|
||||
to: number;
|
||||
}
|
||||
|
||||
export interface ScheduleResponsiveBreakpoint {
|
||||
/**
|
||||
* Number of days to display
|
||||
*/
|
||||
days: number;
|
||||
|
||||
/**
|
||||
* When the first day should start
|
||||
*/
|
||||
startOf: unitOfTime.StartOf;
|
||||
|
||||
/**
|
||||
* Width until next breakpoint is hit
|
||||
*/
|
||||
until: number;
|
||||
}
|
||||
71
src/app/modules/schedule/schedule.module.ts
Normal file
71
src/app/modules/schedule/schedule.module.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {ScheduleCardComponent} from './page/grid/schedule-card.component';
|
||||
|
||||
import {DateFormatPipe, MomentModule} from 'ngx-moment';
|
||||
import {UtilModule} from '../../util/util.module';
|
||||
import {DataModule} from '../data/data.module';
|
||||
import {DataProvider} from '../data/data.provider';
|
||||
import {CalendarViewComponent} from './page/calendar-view.component';
|
||||
import {InfiniteSlidesComponent} from './page/grid/infinite-slides.component';
|
||||
import {ScheduleCursorComponent} from './page/grid/schedule-cursor.component';
|
||||
import {ModalEventCreatorComponent} from './page/modal/modal-event-creator.component';
|
||||
import {SchedulePageComponent} from './page/schedule-page.component';
|
||||
import {ScheduleSingleEventsComponent} from './page/schedule-single-events.component';
|
||||
import {ScheduleViewComponent} from './page/schedule-view.component';
|
||||
import {ScheduleProvider} from './schedule.provider';
|
||||
|
||||
const settingsRoutes: Routes = [
|
||||
{path: 'schedule', redirectTo: 'schedule/calendar/now'},
|
||||
{path: 'schedule/calendar', redirectTo: 'schedule/calendar/now'},
|
||||
{path: 'schedule/recurring', redirectTo: 'schedule/recurring/now'},
|
||||
{path: 'schedule/single', redirectTo: 'schedule/single/now'},
|
||||
// calendar | recurring | single
|
||||
{path: 'schedule/:mode/:date', component: SchedulePageComponent},
|
||||
];
|
||||
|
||||
/**
|
||||
* Schedule Module
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CalendarViewComponent,
|
||||
InfiniteSlidesComponent,
|
||||
ModalEventCreatorComponent,
|
||||
ScheduleCardComponent,
|
||||
ScheduleCursorComponent,
|
||||
SchedulePageComponent,
|
||||
ScheduleSingleEventsComponent,
|
||||
ScheduleViewComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
UtilModule,
|
||||
IonicModule.forRoot(),
|
||||
TranslateModule.forChild(),
|
||||
RouterModule.forChild(settingsRoutes),
|
||||
DataModule,
|
||||
MomentModule,
|
||||
],
|
||||
providers: [ScheduleProvider, DataProvider, DateFormatPipe],
|
||||
})
|
||||
export class ScheduleModule {}
|
||||
195
src/app/modules/schedule/schedule.provider.ts
Normal file
195
src/app/modules/schedule/schedule.provider.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/* eslint-disable unicorn/no-null */
|
||||
/*
|
||||
* Copyright (C) 2018-2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Injectable, OnDestroy} from '@angular/core';
|
||||
import {
|
||||
Bounds,
|
||||
SCDateSeries,
|
||||
SCISO8601Date,
|
||||
SCSearchFilter,
|
||||
SCThingType,
|
||||
SCUuid,
|
||||
} from '@openstapps/core';
|
||||
import {BehaviorSubject, Subscription} from 'rxjs';
|
||||
import {DataProvider} from '../data/data.provider';
|
||||
|
||||
/**
|
||||
* Provider for app settings
|
||||
*/
|
||||
@Injectable()
|
||||
export class ScheduleProvider implements OnDestroy {
|
||||
// tslint:disable:prefer-function-over-method
|
||||
|
||||
/**
|
||||
* Storage key for event UUIDs
|
||||
*/
|
||||
private static uuidStorageKey = 'schedule::event_uuids';
|
||||
|
||||
private _uuids$?: BehaviorSubject<SCUuid[]>;
|
||||
|
||||
private _uuidSubscription?: Subscription;
|
||||
|
||||
constructor(private readonly dataProvider: DataProvider) {
|
||||
window.addEventListener('storage', this.storageEventListener.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Push one or more values to local storage
|
||||
*/
|
||||
private static get<T>(key: string): T[] {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item == undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return JSON.parse(item) as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Push one or more values to local storage
|
||||
*/
|
||||
private static set<T>(key: string, item: T[]) {
|
||||
const newValue = JSON.stringify(item);
|
||||
// prevent feedback loop from storageEvent -> _uuids$.next() -> set -> storageEvent
|
||||
if (newValue !== localStorage.getItem(key)) {
|
||||
localStorage.setItem(key, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
public get uuids$(): BehaviorSubject<SCUuid[]> {
|
||||
if (!this._uuids$) {
|
||||
this._uuids$ = new BehaviorSubject(
|
||||
ScheduleProvider.get<SCUuid>(ScheduleProvider.uuidStorageKey),
|
||||
);
|
||||
this._uuidSubscription = this._uuids$.subscribe(result => {
|
||||
ScheduleProvider.set(ScheduleProvider.uuidStorageKey, result);
|
||||
});
|
||||
}
|
||||
|
||||
return this._uuids$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to updates in local storage
|
||||
*/
|
||||
private storageEventListener(event: StorageEvent) {
|
||||
if (
|
||||
event.newValue &&
|
||||
event.storageArea === localStorage &&
|
||||
event.key === ScheduleProvider.uuidStorageKey
|
||||
) {
|
||||
this._uuids$?.next(JSON.parse(event.newValue));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Date Series
|
||||
*/
|
||||
async getDateSeries(
|
||||
uuids: SCUuid[],
|
||||
frequencies?: Array<'single' | 'weekly' | 'biweekly'>,
|
||||
from?: SCISO8601Date | 'now',
|
||||
to?: SCISO8601Date | 'now',
|
||||
): Promise<SCDateSeries[]> {
|
||||
if (uuids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filters: SCSearchFilter[] = [
|
||||
{
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.DateSeries,
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
filters: uuids.map(uid => ({
|
||||
arguments: {
|
||||
field: 'uid',
|
||||
value: uid,
|
||||
},
|
||||
type: 'value',
|
||||
})),
|
||||
operation: 'or',
|
||||
},
|
||||
type: 'boolean',
|
||||
},
|
||||
];
|
||||
|
||||
if (frequencies) {
|
||||
filters.push({
|
||||
arguments: {
|
||||
filters: frequencies.map(frequency => ({
|
||||
arguments: {
|
||||
field: 'frequency',
|
||||
value: frequency,
|
||||
},
|
||||
type: 'value',
|
||||
})),
|
||||
operation: 'or',
|
||||
},
|
||||
type: 'boolean',
|
||||
});
|
||||
}
|
||||
|
||||
if (from || to) {
|
||||
const bounds: Bounds<string> = {};
|
||||
if (from) {
|
||||
bounds.lowerBound = {
|
||||
limit: from,
|
||||
mode: 'inclusive',
|
||||
};
|
||||
}
|
||||
if (to) {
|
||||
bounds.upperBound = {
|
||||
limit: to,
|
||||
mode: 'inclusive',
|
||||
};
|
||||
}
|
||||
filters.push({
|
||||
arguments: {
|
||||
field: 'dates',
|
||||
bounds: bounds,
|
||||
},
|
||||
type: 'date range',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
await this.dataProvider.search({
|
||||
filter: {
|
||||
arguments: {
|
||||
filters: filters,
|
||||
operation: 'and',
|
||||
},
|
||||
type: 'boolean',
|
||||
},
|
||||
})
|
||||
).data as SCDateSeries[];
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this._uuidSubscription?.unsubscribe();
|
||||
window.removeEventListener('storage', this.storageEventListener.bind(this));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user