Files
openstapps/frontend/app/src/app/modules/calendar/schedule.provider.ts
T

266 lines
6.8 KiB
TypeScript

/*
* Copyright (C) 2023 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/>.
*/
/* eslint-disable unicorn/no-null */
import {DestroyRef, inject, Injectable, OnDestroy} from '@angular/core';
import {
Bounds,
SCDateSeries,
SCISO8601Date,
SCISO8601Duration,
SCSearchFilter,
SCThingType,
SCUuid,
} from '@openstapps/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {DataProvider} from '../data/data.provider';
import {map} from 'rxjs/operators';
import {DateFormatPipe, DurationPipe} from 'ngx-moment';
import {pick} from '@openstapps/collection-utils';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
/**
*
*/
export function toDateSeriesRelevantData(dateSeries: SCDateSeries): DateSeriesRelevantData {
return pick(dateSeries, dateSeriesRelevantKeys);
}
export type DateSeriesRelevantKeys = 'uid' | 'dates' | 'exceptions' | 'repeatFrequency' | 'duration';
export const dateSeriesRelevantKeys: Array<DateSeriesRelevantKeys> = [
'uid',
'dates',
'exceptions',
'repeatFrequency',
'duration',
];
export const formatRelevantKeys: {
[key in DateSeriesRelevantKeys]: (
value: SCDateSeries[key],
dateFormatter: DateFormatPipe,
durationFormatter: DurationPipe,
) => string;
} = {
uid: value => value,
dates: (value, dateFormatter) => `[${value.map(it => dateFormatter.transform(it)).join(', ')}]`,
exceptions: (value, dateFormatter) => `[${value?.map(it => dateFormatter.transform(it)).join(', ') ?? ''}]`,
repeatFrequency: (value, _, durationFormatter) => durationFormatter.transform(value),
duration: (value, _, durationFormatter) => durationFormatter.transform(value),
};
export type DateSeriesRelevantData = Pick<SCDateSeries, DateSeriesRelevantKeys>;
/**
* Provider for app settings
*/
@Injectable()
export class ScheduleProvider implements OnDestroy {
private readonly dataProvider = inject(DataProvider);
// tslint:disable:prefer-function-over-method
private static partialEventsStorageKey = 'schedule::partial_events';
private _partialEvents$?: BehaviorSubject<DateSeriesRelevantData[]>;
destroy$ = inject(DestroyRef);
constructor() {
window.addEventListener('storage', this.storageListener);
}
/**
* 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);
}
}
public async restore(uuids: SCUuid[]): Promise<SCDateSeries[] | undefined> {
if (uuids.length === 0) {
return undefined;
}
const dateSeries = (await this.getDateSeries(uuids)).dates;
this._partialEvents$?.next(dateSeries.map(toDateSeriesRelevantData));
return dateSeries;
}
/**
* TODO
*/
public get uuids$(): Observable<SCUuid[]> {
return this.partialEvents$.pipe(map(events => events.map(it => it.uid)));
}
public get partialEvents$(): BehaviorSubject<DateSeriesRelevantData[]> {
if (!this._partialEvents$) {
const data = ScheduleProvider.get<DateSeriesRelevantData>(ScheduleProvider.partialEventsStorageKey);
this._partialEvents$ = new BehaviorSubject(data ?? []);
this._partialEvents$.pipe(takeUntilDestroyed(this.destroy$)).subscribe(result => {
ScheduleProvider.set(ScheduleProvider.partialEventsStorageKey, result);
});
}
return this._partialEvents$;
}
/**
* What to do when local storage updates
*/
private storageEventHandler(event: StorageEvent) {
if (
event.newValue &&
event.storageArea === localStorage &&
event.key === ScheduleProvider.partialEventsStorageKey
) {
this._partialEvents$?.next(JSON.parse(event.newValue));
}
}
/**
* Listen to updates in local storage
*/
private storageListener = this.storageEventHandler.bind(this);
/**
* Load Date Series
*/
async getDateSeries(
uuids: SCUuid[],
frequencies?: Array<SCISO8601Duration>,
from?: SCISO8601Date | 'now',
to?: SCISO8601Date | 'now',
): Promise<{
dates: SCDateSeries[];
min: SCISO8601Date;
max: SCISO8601Date;
}> {
if (uuids.length === 0) {
return {
dates: [],
min: '',
max: '',
};
}
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: 'repeatFrequency',
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',
});
}
const result = await this.dataProvider.search({
filter: {
arguments: {
filters: filters,
operation: 'and',
},
type: 'boolean',
},
size: 50,
});
return {
dates: result.data as SCDateSeries[],
// TODO: https://gitlab.com/openstapps/backend/-/issues/100
min: new Date(2021, 11, 1).toISOString(),
max: new Date(2022, 1, 24).toISOString(),
};
}
/**
* TODO
*/
ngOnDestroy(): void {
window.removeEventListener('storage', this.storageListener);
}
}