Files
ISA-Frontend/apps/isa-app/src/page/task-calendar/task-calendar.store.ts
Lorenz Hilpert 8ae990bcde Merged PR 1815: Angular Update V18
Related work items: #4830, #4834
2024-10-22 09:23:23 +00:00

409 lines
14 KiB
TypeScript

import { Injectable, OnDestroy } from '@angular/core';
import { DomainTaskCalendarService } from '@domain/task-calendar';
import { ComponentStore } from '@ngrx/component-store';
import { tapResponse } from '@ngrx/operators';
import { DisplayInfoDTO, InputDTO, QuerySettingsDTO } from '@swagger/eis';
import { CalendarIndicator } from '@ui/calendar';
import { DateAdapter } from '@ui/common';
import { Filter, FilterOption, SelectFilter, UiFilter, UiFilterMappingService } from '@ui/filter';
import { UiMessageModalComponent, UiModalRef, UiModalResult, UiModalService } from '@ui/modal';
import { clone } from 'lodash';
import { Observable, Subject, zip } from 'rxjs';
import { debounceTime, map, switchMap, tap, withLatestFrom, first, takeWhile, filter } from 'rxjs/operators';
import { InfoModalComponent } from './modals/info/info-modal.component';
import { PreInfoModalComponent } from './modals/preinfo/preinfo-modal.component';
import { TaskModalComponent } from './modals/task/task-modal.component';
export interface TaskCalendarState {
mode: 'week' | 'month';
selectedDate: Date;
displayedDate: Date;
displayInfos: DisplayInfoDTO[];
initialFilter: UiFilter;
filter: UiFilter;
message: string;
searchResults: DisplayInfoDTO[];
searchTarget: string;
fetchingFilter: boolean;
fetchingItems: boolean;
fetchingSearch: boolean;
hits: number;
}
@Injectable()
export class TaskCalendarStore extends ComponentStore<TaskCalendarState> implements OnDestroy {
private readonly _searchbarFocus$ = new Subject<void>();
get filter() {
return this.get((s) => s.filter);
}
readonly searchbarFocus$ = this._searchbarFocus$.asObservable();
readonly selectSelectedDate = this.select((s) => s.selectedDate);
readonly selectDisplayedDate = this.select((s) => s.displayedDate);
readonly selectSelectedYear = this.select(this.selectSelectedDate, (selectedDate) => this.dateAdapter.getYear(selectedDate));
readonly selectSelectedMonth = this.select(this.selectSelectedDate, (selectedDate) => this.dateAdapter.getMonth(selectedDate));
readonly selectedCalendarWeek = this.select(this.selectSelectedDate, (selectedDate) => this.dateAdapter.getCalendarWeek(selectedDate));
readonly selectDisplayInfos = this.select((s) => s.displayInfos);
readonly searchResults$ = this.select((s) => s.searchResults);
readonly hits$ = this.select((s) => s.hits);
readonly searchTarget$ = this.select((s) => s.searchTarget);
readonly selectCalendarIndicators = this.select(this.selectDisplayInfos, (displayItems) =>
displayItems.reduce<CalendarIndicator[]>((agg, item) => {
const calendarIndicator = this.mapDisplayInfoToCalendarIndicator(item);
if (
!agg.some(
(s) =>
this.dateAdapter.equals({ first: s.date, second: new Date(calendarIndicator.date), precision: 'day' }) &&
s.color === calendarIndicator.color,
) &&
!item.successor // do not show color indicator for items with successor
) {
return [...agg, calendarIndicator];
}
return agg;
}, []),
);
readonly selectInitialFilter = this.select((s) => s.initialFilter);
readonly selectFilter = this.select((s) => s.filter);
readonly selectFetchingFilter = this.select((s) => s.fetchingFilter);
readonly selectFetchingItems = this.select((s) => s.fetchingItems);
readonly selectFetchingSearch = this.select((s) => s.fetchingSearch);
readonly selectMessage = this.select((s) => s.message);
readonly selectStartStop = this.select((s) => {
const { mode, displayedDate } = s;
if (mode === 'week') {
const fdow = this.dateAdapter.getFirstDateOfWeek(displayedDate);
const fdowWithOffset = this.dateAdapter.addCalendarDays(fdow, -7);
const ldow = this.dateAdapter.getLastDateOfWeek(displayedDate);
return {
start: fdowWithOffset,
stop: ldow,
};
} else {
const month = this.dateAdapter.getDatesAndCalendarWeeksForMonth(displayedDate);
if (month.length > 0) {
const firstWeekOfMonth = month[0];
const lastWeekOfMonth = month[month.length - 1];
return {
start: firstWeekOfMonth.dates[0],
stop: lastWeekOfMonth.dates[lastWeekOfMonth.dates.length - 1],
};
}
}
});
hits = this.get((s) => s.hits);
constructor(
public domainTaskCalendarService: DomainTaskCalendarService,
private dateAdapter: DateAdapter,
private uiModal: UiModalService,
private uiFilterMappingService: UiFilterMappingService,
) {
super({
mode: 'month',
selectedDate: dateAdapter.today(),
displayedDate: dateAdapter.today(),
displayInfos: [],
initialFilter: undefined,
filter: undefined,
message: undefined,
fetchingFilter: false,
fetchingItems: false,
fetchingSearch: false,
searchResults: [],
hits: undefined,
searchTarget: '',
});
}
readonly setSelectedDate = this.updater((s, { date }: { date: Date }) => ({
...s,
selectedDate: date,
}));
readonly setDisplayedDate = this.updater((s, { date }: { date: Date }) => ({
...s,
displayedDate: date,
}));
readonly setFilter = this.updater((s, { filters }: { filters: UiFilter }) => ({
...s,
filter: filters,
}));
readonly setMode = this.updater((s, { mode }: { mode: 'week' | 'month' }) => ({
...s,
mode,
}));
readonly setMessage = this.updater((s, { message }: { message: string }) => ({
...s,
message,
}));
/**
* Der Key der Datums-Gruppe, zu der initial navigiert werden soll
*/
readonly setSearchTarget = this.updater((s, { searchTarget }: { searchTarget: string }) => ({
...s,
searchTarget,
}));
search = this.effect((options$: Observable<{ clear?: boolean }>) => {
const currentBranch$ = this.domainTaskCalendarService.currentBranchId$.pipe(filter((f) => !!f));
const selectFilter$ = this.selectFilter.pipe(filter((f) => !!f));
return options$.pipe(
tap(() => this.patchState({ fetchingSearch: true, searchTarget: '' })),
debounceTime(150),
switchMap((options) =>
zip(currentBranch$, selectFilter$).pipe(
switchMap(([branchId, filter]) => {
const querytoken = {
...filter?.getQueryToken(),
};
// Im Zeitraum von 6 Wochen in der Vergangenheit und 2 Wochen in der Zukunft abfragen
const start = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -42);
const stop = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), 14);
return this.domainTaskCalendarService
.getInfos({
...querytoken,
filter: {
timespan: `"${start.toISOString()}"-"${stop?.toISOString()}"`,
...querytoken?.filter,
branch_id: String(branchId),
},
})
.pipe(
tapResponse(
(response) => {
if (!response.error) {
response = this.preparePreInfos(response);
const results = this.get((s) => s.searchResults);
const searchResults = results.length > 0 && !options?.clear ? [...results, ...response.result] : [...response.result];
const sorted = searchResults.sort((a, b) =>
this.dateAdapter.findClosestDate(
new Date(a.taskDate || a.publicationDate),
new Date(b.taskDate || b.publicationDate),
),
);
const searchTarget =
sorted?.length > 0
? this.domainTaskCalendarService.getDateGroupKey(sorted[0].taskDate || sorted[0].publicationDate)
: '';
this.patchState({
searchResults,
searchTarget,
fetchingSearch: false,
message: undefined,
hits: response.hits,
});
} else {
this.uiModal.open({ content: UiMessageModalComponent, data: response });
this.patchState({ searchResults: [], fetchingSearch: false, message: 'Keine Suchergebnisse' });
}
},
(error) => {
console.error(error);
this.patchState({ searchResults: [], fetchingSearch: false, message: 'Keine Suchergebnisse' });
},
),
);
}),
),
),
);
});
resetSearch() {
this.patchState({ searchResults: [], hits: undefined });
}
focusSearchbar() {
this._searchbarFocus$.next();
}
ngOnDestroy(): void {
this._searchbarFocus$.complete();
}
loadItems = this.effect(($: Observable<void>) => {
const currentBranch$ = this.domainTaskCalendarService.currentBranchId$.pipe(filter((f) => !!f));
const selectStartStop$ = this.selectStartStop.pipe(filter((f) => !!f));
const selectInitialFilter$ = this.selectInitialFilter.pipe(filter((f) => !!f));
return $.pipe(
tap(() => this.patchState({ fetchingItems: true })),
debounceTime(250),
switchMap((_) =>
zip(currentBranch$, selectStartStop$, selectInitialFilter$).pipe(
switchMap(([branchId, date, filter]) => {
const querytoken = { ...filter?.getQueryToken() };
return this.domainTaskCalendarService
.getInfos({
...querytoken,
filter: {
...querytoken?.filter,
branch_id: String(branchId),
timespan: `"${date?.start?.toISOString()}"-"${date.stop?.toISOString()}"`,
},
})
.pipe(
tapResponse(
(response) => {
if (!response.error) {
response = this.preparePreInfos(response);
this.patchState({ displayInfos: response.result, fetchingItems: false, message: undefined });
} else {
this.uiModal.open({
content: UiMessageModalComponent,
data: response,
});
this.patchState({ displayInfos: [], fetchingItems: false, message: 'Keine Suchergebnisse' });
}
},
(error) => {
console.error(error);
this.patchState({ displayInfos: [], fetchingItems: false, message: 'Keine Suchergebnisse' });
},
),
);
}),
),
),
);
});
preparePreInfos(response) {
const preInfos = response.result.filter((info) => this.domainTaskCalendarService.getInfoType(info) === 'PreInfo');
preInfos.forEach((info) => {
const preInfoTask = clone(info);
delete preInfoTask.publicationDate;
response.result.push(preInfoTask);
});
return response;
}
readonly loadFilter = this.effect(($: Observable<void>) =>
$.pipe(
tap(() => this.patchState({ fetchingFilter: true })),
switchMap((_) => this.domainTaskCalendarService.getSettings()),
tapResponse(
(response: QuerySettingsDTO) => {
this.patchState({ filter: UiFilter.create(response), initialFilter: UiFilter.create(response), fetchingFilter: false });
},
(error) => {
console.error(error);
this.patchState({ filter: undefined, initialFilter: undefined, fetchingFilter: false });
},
),
),
);
open(displayInfoDTO: DisplayInfoDTO) {
const type = this.domainTaskCalendarService.getInfoType(displayInfoDTO);
let taskModalRef: UiModalRef<{ successorId: number }, DisplayInfoDTO>;
switch (type) {
case 'Info':
taskModalRef = this.uiModal.open({
content: InfoModalComponent,
data: displayInfoDTO,
});
break;
case 'PreInfo':
taskModalRef = this.uiModal.open({
content: PreInfoModalComponent,
data: displayInfoDTO,
});
break;
case 'Task':
taskModalRef = this.uiModal.open({
content: TaskModalComponent,
data: displayInfoDTO,
});
break;
}
taskModalRef?.afterClosed$.subscribe({
next: async (result: UiModalResult<{ successorId: number }>) => {
if (result.data?.successorId) {
const info = await this.domainTaskCalendarService
.getInfoById({ infoId: result.data.successorId })
.pipe(map((res) => res.result))
.toPromise();
if (info) {
this.open(info);
}
}
},
});
let hasChanged = false;
taskModalRef?.afterChanged$.subscribe({
next: (changed) => (hasChanged = changed),
complete: async () => {
if (hasChanged) {
this.loadItems();
const searchResults = await this.searchResults$.pipe(first()).toPromise();
if (searchResults?.length > 0) {
this.search({ clear: true });
}
}
},
});
}
mapInputArrayToFilterArray(source: InputDTO[]): SelectFilter[] {
const mappedArr = source.map((input) => {
return this.uiFilterMappingService.fromInputDto(input as any);
});
return mappedArr;
}
mapFilterArrayToStringDictionary(source: Filter[]): { [key: string]: string } {
const dict: { [key: string]: string } = {};
for (const filter of source) {
const options: FilterOption[] = filter.options;
const selected = options.filter((o) => o.selected);
if (selected.length > 0) {
dict[filter.key] = selected.map((o) => o.id).join(';');
}
}
return dict;
}
mapDisplayInfoToCalendarIndicator(source: DisplayInfoDTO): CalendarIndicator {
return {
date: this.domainTaskCalendarService.getDisplayInfoDate(source),
color: this.domainTaskCalendarService.getProcessingStatusColorCode(source),
};
}
}