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 implements OnDestroy { private readonly _searchbarFocus$ = new Subject(); 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((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) => { 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) => $.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), }; } }