diff --git a/.npmrc b/.npmrc index 17ec3e118..8f87ca382 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1 @@ -@isa:registry=https://pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/registry/ -@cmf:registry=https://pkgs.dev.azure.com/hugendubel/_packaging/hugendubel%40Local/npm/registry/ -always-auth=true +@paragondata:registry=https://npm.pkg.github.com \ No newline at end of file diff --git a/apps/domain/task-calendar/src/lib/task-calendar.service.ts b/apps/domain/task-calendar/src/lib/task-calendar.service.ts index 4a3886537..a06e5cb70 100644 --- a/apps/domain/task-calendar/src/lib/task-calendar.service.ts +++ b/apps/domain/task-calendar/src/lib/task-calendar.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { EISPublicService, FileDTO, ProcessingStatus, QueryTokenDTO } from '@swagger/eis'; +import { DisplayInfoDTO, EISPublicService, FileDTO, ProcessingStatus, QueryTokenDTO } from '@swagger/eis'; import { DateAdapter } from '@ui/common'; import { memorize } from '@utils/common'; import { map, shareReplay, switchMap } from 'rxjs/operators'; @@ -279,4 +279,80 @@ export class DomainTaskCalendarService { ) ); } + + moveRemovedToEnd(a: DisplayInfoDTO, b: DisplayInfoDTO) { + const statusA = this.getProcessingStatusList(a)?.includes('Removed'); + const statusB = this.getProcessingStatusList(b)?.includes('Removed'); + + if (statusA && statusB) { + return 0; + } else if (statusA && !statusB) { + return 1; + } else if (!statusA && statusB) { + return -1; + } + + return 0; + } + + /** + * Returns an Array of DisplayInfoDTO, sorted by ProcessingStatus + * Ignores Overdue if Task is already Completed + * Compared DisploayInfoDTO is of Type Task and Info Or PreInfo then sort by Type + * @param items DisplayInfoDTO Array to sort + * @param order Processing Status Order + * @returns DisplayInfoDTO Array ordered by Processing Status anf Type + */ + sort(items: DisplayInfoDTO[], order: ProcessingStatusList) { + let result = [...items]; + const reversedOrder = [...order].reverse(); + + for (const status of reversedOrder) { + result = result?.sort((a, b) => { + const statusA = this.getProcessingStatusList(a); + const statusB = this.getProcessingStatusList(b); + + // Ignore Overdue when it is already Completed + if (status === 'Overdue' && statusA.includes('Completed')) { + return 0; + } + + const aHasStatus = statusA.includes(status); + const bHasStatus = statusB.includes(status); + + if (aHasStatus && bHasStatus) { + // If it has the same ProcessingStatus then Sort by Type + const aType = this.getInfoType(a); + const bType = this.getInfoType(b); + if (aType !== bType) { + if (aType === 'Info' || aType === 'PreInfo') { + return -1; + } else { + return 1; + } + } + + if (statusB.includes('Completed')) { + return -1; + } + + return 0; + } else if (aHasStatus && !bHasStatus) { + return -1; + } else if (!aHasStatus && bHasStatus) { + return 1; + } + + return 0; + }); + } + + return result; + } + + getDateGroupKey(d: string) { + // Get Date as string key to ignore time for grouping + const date = new Date(d); + return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; + } } diff --git a/apps/isa-app/src/assets/icons.svg b/apps/isa-app/src/assets/icons.svg index 4a71a3013..73b9010ff 100644 --- a/apps/isa-app/src/assets/icons.svg +++ b/apps/isa-app/src/assets/icons.svg @@ -212,4 +212,7 @@ + + + diff --git a/apps/page/goods-out/src/lib/goods-out-search/goods-out-search-filter/goods-out-search-filter.component.ts b/apps/page/goods-out/src/lib/goods-out-search/goods-out-search-filter/goods-out-search-filter.component.ts index e5c1f3bc2..76a3cde0b 100644 --- a/apps/page/goods-out/src/lib/goods-out-search/goods-out-search-filter/goods-out-search-filter.component.ts +++ b/apps/page/goods-out/src/lib/goods-out-search/goods-out-search-filter/goods-out-search-filter.component.ts @@ -46,8 +46,7 @@ export class GoodsOutSearchFilterComponent implements OnInit, OnDestroy { private _goodsOutSearchStore: GoodsOutSearchStore, private _breadcrumb: BreadcrumbService, private _cdr: ChangeDetectorRef, - private _router: Router, - private readonly _config: Config + private _router: Router ) {} ngOnInit() { diff --git a/apps/page/task-calendar/src/lib/components/task-info/task-info.component.ts b/apps/page/task-calendar/src/lib/components/task-info/task-info.component.ts index b14686b6c..d4a1cb2c5 100644 --- a/apps/page/task-calendar/src/lib/components/task-info/task-info.component.ts +++ b/apps/page/task-calendar/src/lib/components/task-info/task-info.component.ts @@ -1,6 +1,16 @@ import { Clipboard } from '@angular/cdk/clipboard'; import { DatePipe } from '@angular/common'; -import { Component, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges, Optional, HostBinding } from '@angular/core'; +import { + Component, + ChangeDetectionStrategy, + Input, + OnChanges, + SimpleChanges, + Optional, + HostBinding, + EventEmitter, + Output, +} from '@angular/core'; import { DomainTaskCalendarService } from '@domain/task-calendar'; import { FileDTO } from '@swagger/checkout'; import { DisplayInfoDTO } from '@swagger/eis'; @@ -27,6 +37,9 @@ export class TaskInfoComponent implements OnChanges { @Input() info: DisplayInfoDTO; + @Output() + changed = new EventEmitter(); + @Input() showTaskDate: boolean; @@ -210,5 +223,6 @@ export class TaskInfoComponent implements OnChanges { await this.domainTaskCalendarService.addComment({ infoId: this.info.id, text: note }).toPromise(); sessionStorage.removeItem(`INFO_NOTE_${this.info?.id}`); this.noteAdded$.next(); + this.changed.emit(true); } } diff --git a/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.html b/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.html index 729082a7d..86568692f 100644 --- a/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.html +++ b/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.html @@ -1,33 +1,45 @@ -
- - {{ item?.taskDate || item?.publicationDate | date }} - - - - {{ item?.timeFrom | date: 'HH:mm' }} - {{ item?.timeTo | date: 'HH:mm' }} - - Ganztägig +
+ +
+ + {{ item?.taskDate || item?.publicationDate | date }} + + + + {{ item?.timeFrom | date: 'HH:mm' }} - {{ item?.timeTo | date: 'HH:mm' }} + + Ganztägig + +
+ + -
- -
-
- - - -
-
- -
-
- {{ item?.title }} - - - + +
+
+ {{ item?.title }} + + + +
+
{{ item?.category | replace: '##':' / ' }}
+
{{ item?.text | stripHtmlTags | substr: 70 }}
-
{{ item?.category | replace: '##':' / ' }}
-
{{ item?.text | stripHtmlTags | substr: 70 }}
diff --git a/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.scss b/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.scss index 64abff6d2..9fd2ed304 100644 --- a/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.scss +++ b/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.scss @@ -1,13 +1,20 @@ -:host { - @apply flex flex-row px-4 py-3; +.task-list-item-wrapper { + @apply flex flex-row pr-4 py-3 bg-white border-solid cursor-pointer; + border-radius: 5px 0px 5px 5px; + box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5); .date { - @apply font-bold mr-2 self-start flex-shrink-0 flex-grow-0; + @apply font-bold mr-2 self-start flex-shrink-0 flex-grow-0 ml-4; width: 112px !important; } .indicator { - @apply w-px-2 bg-gray-100 self-stretch mx-4; + @apply bg-gray-100 justify-self-stretch; + width: 6px; + margin-top: -13px; + margin-bottom: -13px; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; } .icon { @@ -41,3 +48,7 @@ } } } + +.show { + @apply visible; +} diff --git a/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.ts b/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.ts index f20804181..6a0d3429a 100644 --- a/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.ts +++ b/apps/page/task-calendar/src/lib/components/task-list/task-list-item.component.ts @@ -2,7 +2,7 @@ import { Component, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges, Ho import { DomainTaskCalendarService } from '@domain/task-calendar'; import { DisplayInfoDTO } from '@swagger/eis'; import { combineLatest, ReplaySubject } from 'rxjs'; -import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; +import { map, shareReplay } from 'rxjs/operators'; @Component({ selector: 'page-task-list-item', diff --git a/apps/page/task-calendar/src/lib/components/task-list/task-list.component.html b/apps/page/task-calendar/src/lib/components/task-list/task-list.component.html index 25988258d..a0830d5e2 100644 --- a/apps/page/task-calendar/src/lib/components/task-list/task-list.component.html +++ b/apps/page/task-calendar/src/lib/components/task-list/task-list.component.html @@ -1,52 +1,55 @@ - -
-
-

Überfällig

- {{ overdieItems?.length }} Aufgaben -
-
- - + + +
+
+
+

Überfällig

+
+ {{ overdieItems?.length }} Aufgaben +

- -
-
+ + +
+
+
+
- -
-
-
-

{{ selected | date: 'EEEE' }}

-
{{ selected | date }}
+ +
+
+
+

{{ selected | date: 'EEEE' }}, {{ selected | date }}

+
+
+ + {{ itemsForSelectedDate?.length }} Aufgaben und Infos +
-
- - {{ itemsForSelectedDate?.length }} Aufgaben und Infos -
-
-
- -
-
-
- + + +
+
+
+ - -
-
-
-

Laufende Aufgaben

+ +
+
+
+

Laufende Aufgaben

+
+
+ {{ ongoingItems?.length }} Aufgaben +
-
- {{ ongoingItems?.length }} Aufgaben -
-
-
- -
-
-
- + + +
+
+
+ + diff --git a/apps/page/task-calendar/src/lib/components/task-list/task-list.component.scss b/apps/page/task-calendar/src/lib/components/task-list/task-list.component.scss index fea004b68..fe6a3dd71 100644 --- a/apps/page/task-calendar/src/lib/components/task-list/task-list.component.scss +++ b/apps/page/task-calendar/src/lib/components/task-list/task-list.component.scss @@ -3,13 +3,17 @@ } .task-list { - @apply pt-6; + @apply pt-4; .head-row { - @apply flex flex-row justify-between items-baseline px-4 py-3; + @apply flex flex-row justify-between items-center px-5 py-3; + height: 53px; + background-color: #f5f7fa; + border-radius: 5px 5px 0px 0px; + box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5); .head-row-title { - @apply flex flex-row self-end; + @apply flex flex-row text-lg; .muted { @apply ml-4 self-end; @@ -17,10 +21,10 @@ } .head-row-info { - @apply flex flex-col text-right items-end; + @apply flex flex-col text-right justify-center items-end; .cta-print { - @apply border-none outline-none bg-transparent text-brand text-cta-l font-bold pr-0 mb-3; + @apply border-none outline-none bg-transparent text-brand text-cta-l font-bold pr-0; } } @@ -50,3 +54,7 @@ @apply opacity-30 line-through; } } + +:host ::ng-deep ui-scroll-container .scroll-container { + @apply flex flex-col; +} diff --git a/apps/page/task-calendar/src/lib/components/task-list/task-list.component.ts b/apps/page/task-calendar/src/lib/components/task-list/task-list.component.ts index 502d3af8c..2db4b49a7 100644 --- a/apps/page/task-calendar/src/lib/components/task-list/task-list.component.ts +++ b/apps/page/task-calendar/src/lib/components/task-list/task-list.component.ts @@ -1,7 +1,7 @@ import { DatePipe } from '@angular/common'; import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core'; import { DomainPrinterService } from '@domain/printer'; -import { DomainTaskCalendarService, ProcessingStatusList } from '@domain/task-calendar'; +import { DomainTaskCalendarService } from '@domain/task-calendar'; import { PrintModalComponent, PrintModalData } from '@modal/printer'; import { DisplayInfoDTO } from '@swagger/eis'; import { DateAdapter } from '@ui/common'; @@ -43,6 +43,19 @@ export class TaskListComponent { } } + /* @internal */ + fetching$ = new BehaviorSubject(true); + + @Input() + get fetching() { + return this.fetching$.value; + } + set fetching(value) { + if (this.fetching !== value) { + this.fetching$.next(value); + } + } + isToday$ = this.selected$.pipe(map((selected) => this.dateAdapter.equals({ first: selected, second: this.today, precision: 'day' }))); overdueItems$ = combineLatest([this.items$, this.selected$]).pipe( @@ -65,7 +78,7 @@ export class TaskListComponent { : item ) ), - map((list) => list.sort(this.moveRemovedToEnd.bind(this))) + map((list) => list.sort((a, b) => this.domainTaskCalendarService.moveRemovedToEnd(a, b))) ); ongoingItems$ = combineLatest([this.items$, this.selected$]).pipe( @@ -95,8 +108,8 @@ export class TaskListComponent { : item ) ), - map((list) => this.sort(list, ['Overdue', 'InProcess', 'Approved', 'Completed', 'Removed'])), - map((list) => list.sort(this.moveRemovedToEnd.bind(this))) + map((list) => this.domainTaskCalendarService.sort(list, ['Overdue', 'InProcess', 'Approved', 'Completed', 'Removed'])), + map((list) => list.sort(this.domainTaskCalendarService.moveRemovedToEnd)) ); selectedItems$ = combineLatest([this.items$, this.selected$]).pipe( @@ -116,8 +129,8 @@ export class TaskListComponent { ) ), // Sortierung der aufgaben nach Rot => Gelb => Grau => Grün - map((list) => this.sort(list, ['Overdue', 'InProcess', 'Approved', 'Completed', 'Removed'])), - map((list) => list.sort(this.moveRemovedToEnd.bind(this))) + map((list) => this.domainTaskCalendarService.sort(list, ['Overdue', 'InProcess', 'Approved', 'Completed', 'Removed'])), + map((list) => list.sort((a, b) => this.domainTaskCalendarService.moveRemovedToEnd(a, b))) ); @Output() @@ -131,76 +144,6 @@ export class TaskListComponent { private domainPrinterService: DomainPrinterService ) {} - moveRemovedToEnd(a: DisplayInfoDTO, b: DisplayInfoDTO) { - const statusA = this.domainTaskCalendarService.getProcessingStatusList(a)?.includes('Removed'); - const statusB = this.domainTaskCalendarService.getProcessingStatusList(b)?.includes('Removed'); - - if (statusA && statusB) { - return 0; - } else if (statusA && !statusB) { - return 1; - } else if (!statusA && statusB) { - return -1; - } - - return 0; - } - - /** - * Returns an Array of DisplayInfoDTO, sorted by ProcessingStatus - * Ignores Overdue if Task is already Completed - * Compared DisploayInfoDTO is of Type Task and Info Or PreInfo then sort by Type - * @param items DisplayInfoDTO Array to sort - * @param order Processing Status Order - * @returns DisplayInfoDTO Array ordered by Processing Status anf Type - */ - sort(items: DisplayInfoDTO[], order: ProcessingStatusList) { - let result = [...items]; - const reversedOrder = [...order].reverse(); - - for (const status of reversedOrder) { - result = result?.sort((a, b) => { - const statusA = this.domainTaskCalendarService.getProcessingStatusList(a); - const statusB = this.domainTaskCalendarService.getProcessingStatusList(b); - - // Ignore Overdue when it is already Completed - if (status === 'Overdue' && statusA.includes('Completed')) { - return 0; - } - - const aHasStatus = statusA.includes(status); - const bHasStatus = statusB.includes(status); - - if (aHasStatus && bHasStatus) { - // If it has the same ProcessingStatus then Sort by Type - const aType = this.domainTaskCalendarService.getInfoType(a); - const bType = this.domainTaskCalendarService.getInfoType(b); - if (aType !== bType) { - if (aType === 'Info' || aType === 'PreInfo') { - return -1; - } else { - return 1; - } - } - - if (statusB.includes('Completed')) { - return -1; - } - - return 0; - } else if (aHasStatus && !bHasStatus) { - return -1; - } else if (!aHasStatus && bHasStatus) { - return 1; - } - - return 0; - }); - } - - return result; - } - async print() { let displayInfos = await this.selectedItems$.pipe(first()).toPromise(); displayInfos = displayInfos.filter((di) => !this.domainTaskCalendarService.getProcessingStatusList(di).includes('Completed')); diff --git a/apps/page/task-calendar/src/lib/components/task-list/task-list.module.ts b/apps/page/task-calendar/src/lib/components/task-list/task-list.module.ts index f00e6dd0f..cbc268858 100644 --- a/apps/page/task-calendar/src/lib/components/task-list/task-list.module.ts +++ b/apps/page/task-calendar/src/lib/components/task-list/task-list.module.ts @@ -7,10 +7,13 @@ import { FilterStatusPipe, FilterTypePipe } from './pipes'; import { UiCommonModule } from '@ui/common'; import { UiTshirtModule } from '@ui/tshirt'; import { UiIconModule } from '@ui/icon'; +import { RouterModule } from '@angular/router'; +import { UiSpinnerModule } from '@ui/spinner'; +import { UiScrollContainerModule } from '@ui/scroll-container'; @NgModule({ - imports: [CommonModule, UiCommonModule, UiTshirtModule, UiIconModule], - exports: [TaskListComponent], + imports: [CommonModule, UiCommonModule, UiSpinnerModule, UiTshirtModule, UiIconModule, RouterModule, UiScrollContainerModule], + exports: [TaskListComponent, TaskListItemComponent], declarations: [TaskListComponent, TaskListItemComponent, FilterStatusPipe, FilterTypePipe], }) export class TaskListModule {} diff --git a/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.component.html b/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.component.html new file mode 100644 index 000000000..dfb95ef85 --- /dev/null +++ b/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.component.html @@ -0,0 +1,16 @@ + + + + + + + diff --git a/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.component.scss b/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.component.scss new file mode 100644 index 000000000..875d646cf --- /dev/null +++ b/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.component.scss @@ -0,0 +1,29 @@ +:host { + @apply grid relative; +} + +input { + @apply py-4 px-4 outline-none h-14 font-bold text-lg; + box-shadow: 0px 6px 24px rgba(206, 212, 219, 0.8); + border-radius: 5px; +} + +button.search { + @apply grid items-center justify-center bg-brand text-white px-2 py-2 rounded-lg -ml-2 w-14 h-14 absolute; + right: 0; +} + +button.clear { + @apply grid items-center justify-center px-2 py-2 rounded-lg -ml-2 w-14 h-14 absolute; + color: #1f466c; + right: 0; +} + +button:disabled { + @apply bg-disabled-branch cursor-not-allowed; +} + +::placeholder { + @apply font-bold text-lg; + color: #596470; +} diff --git a/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.component.ts b/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.component.ts new file mode 100644 index 000000000..266a9cf17 --- /dev/null +++ b/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.component.ts @@ -0,0 +1,97 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { isEqual } from 'lodash'; +import { BehaviorSubject, combineLatest, Subject } from 'rxjs'; +import { map, shareReplay, takeUntil, first } from 'rxjs/operators'; +import { TaskCalendarStore } from '../../task-calendar.store'; + +@Component({ + selector: 'page-task-searchbar', + templateUrl: 'task-searchbar.component.html', + styleUrls: ['task-searchbar.component.scss'], +}) +export class TaskSearchbarComponent implements OnInit, OnDestroy, AfterViewInit { + private _onDestroy$ = new Subject(); + + @ViewChild('searchInput', { static: true }) + searchInput: ElementRef; + + filterActive$ = new BehaviorSubject(false); + + searchActive$ = new BehaviorSubject(false); + searchDisabled$ = new BehaviorSubject(true); + + search$ = new BehaviorSubject(''); + + filter$ = this.taskCalendarStore.selectFilter; + + get search() { + return this.search$.value; + } + set search(search: string) { + this.search$.next(search); + this.searchDisabled$.next(search?.length < 3); + } + + constructor(private taskCalendarStore: TaskCalendarStore, private _router: Router, private _activatedRoute: ActivatedRoute) {} + + ngOnInit(): void { + this._activatedRoute.queryParams.pipe(takeUntil(this._onDestroy$)).subscribe(async (queryParams) => { + const filter = await this.taskCalendarStore.selectFilter.pipe(first()).toPromise(); + const search = filter?.input?.find((_) => true)?.input.find((_) => true)?.value || queryParams?.main_qs || ''; + + this.search$.next(search); + this.searchActive$.next(!!search); + this.focusSearch(); + }); + } + + ngAfterViewInit(): void { + this.focusSearch(); + } + + ngOnDestroy() { + this._onDestroy$.next(); + this._onDestroy$.complete(); + } + + focusSearch() { + setTimeout(() => this.searchInput?.nativeElement?.focus(), 100); + } + + async clearSearch() { + const filter = await this.filter$.pipe(first()).toPromise(); + filter?.input + ?.find((_) => true) + ?.input.find((_) => true) + ?.setValue(''); + this.taskCalendarStore.setFilter({ filters: filter }); + + this.search = ''; + this.searchActive$.next(false); + this.focusSearch(); + } + + async searchTasks() { + const filter = await this.filter$.pipe(first()).toPromise(); + + filter?.input + ?.find((_) => true) + ?.input.find((_) => true) + ?.setValue(this.search); + this.taskCalendarStore.setFilter({ filters: filter }); + + if (this.search?.length >= 3) { + this.navigate('/filiale/task-calendar/search', { ...filter?.getQueryParams() }); + + // ActivatedRouteChange wird nicht aufgerufen, wenn sich die URL nicht verändert. In dem Fall erneute Suche ausführen + if (isEqual(filter.getQueryParams(), this._activatedRoute.snapshot.queryParams)) { + this.taskCalendarStore.search({ clear: true }); + } + } + } + + navigate(uri: string, queryParams?: Params) { + this._router.navigate([uri], { queryParams }); + } +} diff --git a/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.module.ts b/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.module.ts new file mode 100644 index 000000000..ca6b10927 --- /dev/null +++ b/apps/page/task-calendar/src/lib/components/task-searchbar/task-searchbar.module.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { UiFormFieldModule } from '@paragondata/ngx-ui/form-field'; +import { UiIconModule } from '@ui/icon'; + +import { TaskSearchbarComponent } from './task-searchbar.component'; + +@NgModule({ + imports: [CommonModule, UiFormFieldModule, FormsModule, UiIconModule], + exports: [TaskSearchbarComponent], + declarations: [TaskSearchbarComponent], + providers: [], +}) +export class TaskSearchbarModule {} diff --git a/apps/page/task-calendar/src/lib/containers/task-calendar-filter/task-calendar-filter.component.ts b/apps/page/task-calendar/src/lib/containers/task-calendar-filter/task-calendar-filter.component.ts index ceb5917c5..966621dd8 100644 --- a/apps/page/task-calendar/src/lib/containers/task-calendar-filter/task-calendar-filter.component.ts +++ b/apps/page/task-calendar/src/lib/containers/task-calendar-filter/task-calendar-filter.component.ts @@ -1,5 +1,9 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { ActivatedRoute, Params, Router } from '@angular/router'; import { UiFilter } from '@ui/filter'; +import { isEqual } from 'lodash'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { first, takeUntil } from 'rxjs/operators'; import { TaskCalendarStore } from '../../task-calendar.store'; @Component({ @@ -8,24 +12,66 @@ import { TaskCalendarStore } from '../../task-calendar.store'; styleUrls: ['task-calendar-filter.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TaskCalendarFilterComponent { +export class TaskCalendarFilterComponent implements OnInit, OnDestroy { + private _onDestroy$ = new Subject(); @Output() exitFilter = new EventEmitter(); - filter$ = this.taskCalendarStore.selectFilter; + filter$ = new BehaviorSubject(undefined); fetching$ = this.taskCalendarStore.selectFetching; message$ = this.taskCalendarStore.selectMessage; - constructor(private taskCalendarStore: TaskCalendarStore) {} + constructor(private taskCalendarStore: TaskCalendarStore, private _router: Router, private _activatedRoute: ActivatedRoute) {} + + private _initFilter(filter: UiFilter) { + this.filter$.next(UiFilter.create(filter)); + } + + async ngOnInit() { + this.taskCalendarStore.selectFilter.pipe(takeUntil(this._onDestroy$)).subscribe((filter) => { + this._initFilter(filter); + }); + + const filter = await this.taskCalendarStore.selectFilter.pipe(first()).toPromise(); + this._initFilter(filter); + } + + ngOnDestroy(): void { + this._onDestroy$.next(); + this._onDestroy$.complete(); + } + + async applyFilter(filters: UiFilter) { + const queryParams = { ...filters?.getQueryParams() }; - applyFilter(filters: UiFilter) { this.taskCalendarStore.setFilter({ filters }); - this.taskCalendarStore.loadItems(); - this.exitFilter.emit(); + this.taskCalendarStore.setMessage({ message: '' }); + + const search = this.getSearch(filters); + if (search?.length >= 3 || search === '') { + this.navigate('/filiale/task-calendar/search', queryParams); + + // ActivatedRouteChange wird nicht aufgerufen, wenn sich die URL nicht verändert. In dem Fall erneute Suche ausführen + if (isEqual(filters.getQueryParams(), this._activatedRoute.snapshot.queryParams)) { + this.taskCalendarStore.search({ clear: true }); + } + + this.exitFilter.emit(); + } else { + this.taskCalendarStore.setMessage({ message: 'Mindestens 3 Zeichen' }); + } + } + + getSearch(filter: UiFilter) { + return filter?.input?.find((_) => true)?.input?.find((_) => true)?.value || ''; } resetFilter() { this.taskCalendarStore.loadFilter(); } + + navigate(uri: string, queryParams?: Params) { + this._router.navigate([uri], { queryParams }); + } } diff --git a/apps/page/task-calendar/src/lib/modals/info/info-modal.component.html b/apps/page/task-calendar/src/lib/modals/info/info-modal.component.html index 1014eca56..c6448c765 100644 --- a/apps/page/task-calendar/src/lib/modals/info/info-modal.component.html +++ b/apps/page/task-calendar/src/lib/modals/info/info-modal.component.html @@ -3,7 +3,7 @@

{{ info?.title }}

- +
- +
- +
diff --git a/apps/page/task-calendar/src/lib/modals/task/task-modal.component.ts b/apps/page/task-calendar/src/lib/modals/task/task-modal.component.ts index ff667cb37..1bb6e3c3e 100644 --- a/apps/page/task-calendar/src/lib/modals/task/task-modal.component.ts +++ b/apps/page/task-calendar/src/lib/modals/task/task-modal.component.ts @@ -122,19 +122,26 @@ export class TaskModalComponent { this.modalRef.close({ successorId }); } + changed() { + this.modalRef.markChanged(); + } + async startEdit() { await this.domainTaskCalendarService.setToEdit({ infoId: this.info.id }).toPromise(); this.reloadInfo(); + this.changed(); } async completeEdit() { await this.domainTaskCalendarService.complete({ infoId: this.info.id }).toPromise(); + this.changed(); this.close(); } async edit() { await this.domainTaskCalendarService.setToEdit({ infoId: this.info.id }).toPromise(); this.reloadInfo(); + this.changed(); } reloadInfo() { @@ -159,6 +166,7 @@ export class TaskModalComponent { if (result.data === '') { this.captureInput?.nativeElement?.click(); } else if (result.data?.length > 0) { + this.changed(); this.close(); } }); diff --git a/apps/page/task-calendar/src/lib/page-task-calendar-routing.module.ts b/apps/page/task-calendar/src/lib/page-task-calendar-routing.module.ts index ab30d5982..b4287cc21 100644 --- a/apps/page/task-calendar/src/lib/page-task-calendar-routing.module.ts +++ b/apps/page/task-calendar/src/lib/page-task-calendar-routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { PageTaskCalendarComponent } from './page-task-calendar.component'; import { CalendarComponent } from './pages/calendar/calendar.component'; import { CalendarModule } from './pages/calendar/calendar.module'; +import { TaskSearchComponent } from './pages/task-search'; import { TasksComponent } from './pages/tasks/tasks.component'; import { TasksModule } from './pages/tasks/tasks.module'; @@ -13,7 +14,8 @@ const routes: Routes = [ children: [ { path: 'calendar', component: CalendarComponent }, { path: 'tasks', component: TasksComponent }, - { path: '', pathMatch: 'full', redirectTo: 'calendar' }, + { path: 'search', component: TaskSearchComponent }, + { path: '', pathMatch: 'full', redirectTo: 'tasks' }, ], }, ]; diff --git a/apps/page/task-calendar/src/lib/page-task-calendar.component.html b/apps/page/task-calendar/src/lib/page-task-calendar.component.html index 3792fad37..1a09e350f 100644 --- a/apps/page/task-calendar/src/lib/page-task-calendar.component.html +++ b/apps/page/task-calendar/src/lib/page-task-calendar.component.html @@ -1,21 +1,16 @@ - +
+ +
- +
+ + +
diff --git a/apps/page/task-calendar/src/lib/page-task-calendar.component.scss b/apps/page/task-calendar/src/lib/page-task-calendar.component.scss index 787b90ad0..c876e5f8c 100644 --- a/apps/page/task-calendar/src/lib/page-task-calendar.component.scss +++ b/apps/page/task-calendar/src/lib/page-task-calendar.component.scss @@ -1,20 +1,27 @@ :host { @apply flex flex-col w-full box-content relative; + height: calc(100vh - 14rem); } shell-breadcrumb { - @apply sticky z-sticky top-0 py-4; + @apply sticky z-sticky top-0 h-12 bg-white p-0; + box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5); + border-radius: 5px; } .filter { - @apply absolute font-sans flex items-center font-bold bg-gray-400 border-0 text-regular -top-12 right-0 py-px-8 px-px-15 rounded-filter justify-center z-sticky; - - right: 0; - top: 10px; - min-width: 106px; + @apply font-sans flex items-center font-bold text-lg py-2 px-4 justify-center ml-3 h-14; + width: 108px; + background-color: #aeb7c1; + box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5); + border-radius: 5px; .label { - @apply ml-px-5; + @apply ml-2; + } + + ui-icon { + width: 18px; } &.active { @@ -22,35 +29,27 @@ shell-breadcrumb { } } -.content-container { - max-height: calc(100vh - 267px); - overflow: scroll; - @apply bg-white rounded-card; +.content-header { + @apply grid items-center mt-4 shadow-input gap-1; + height: 56px; + grid-template-columns: 1fr 120px; } -.switch-group { - @apply flex flex-row justify-center my-9; - - a.calendar-switch { - @apply flex flex-row items-center px-7 py-2 no-underline text-black bg-munsell; - &:first-child { - @apply rounded-l-card; - } - - &:last-child { - @apply rounded-r-card; - } - ui-icon { - @apply mr-3; - } - } - - a.calendar-switch.active, - a.calendar-switch:active { - @apply text-white bg-active-branch; - } +.content-container { + @apply overflow-auto; + max-height: calc(100vh - 267px); + border-radius: 0px 0px 5px 5px; } :host ::ng-deep ui-calendar .navigation .title { margin-left: -140px; } + +:host ::ng-deep shell-breadcrumb .link-breadcrumb a { + color: #596470 !important; +} + +:host ::ng-deep shell-breadcrumb .link-back { + @apply top-3 pl-4; + color: #596470 !important; +} diff --git a/apps/page/task-calendar/src/lib/page-task-calendar.component.ts b/apps/page/task-calendar/src/lib/page-task-calendar.component.ts index ab1f5a596..6ed1fcb0d 100644 --- a/apps/page/task-calendar/src/lib/page-task-calendar.component.ts +++ b/apps/page/task-calendar/src/lib/page-task-calendar.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, ViewChild, ElementRef } from '@angular/core'; import { Config } from '@core/config'; import { isEqual } from 'lodash'; import { BehaviorSubject, combineLatest } from 'rxjs'; @@ -12,6 +12,9 @@ import { TaskCalendarStore } from './task-calendar.store'; providers: [TaskCalendarStore], }) export class PageTaskCalendarComponent { + @ViewChild('searchInput', { static: true }) + searchInput: ElementRef; + filterActive$ = new BehaviorSubject(false); taskCalendarKey = this._config.get('process.ids.taskCalendar'); diff --git a/apps/page/task-calendar/src/lib/page-task-calendar.module.ts b/apps/page/task-calendar/src/lib/page-task-calendar.module.ts index a67eb5ce5..b7c5c43fd 100644 --- a/apps/page/task-calendar/src/lib/page-task-calendar.module.ts +++ b/apps/page/task-calendar/src/lib/page-task-calendar.module.ts @@ -4,6 +4,7 @@ import { ShellBreadcrumbModule } from '@shell/breadcrumb'; import { ShellFilterOverlayModule } from '@shell/filter-overlay'; import { UiFilterNextModule } from '@ui/filter'; import { UiIconModule } from '@ui/icon'; +import { TaskSearchbarModule } from './components/task-searchbar/task-searchbar.module'; import { TaskCalendarFilterComponent } from './containers/task-calendar-filter/task-calendar-filter.component'; import { ModalsModule } from './modals/modals.module'; import { PageTaskCalendarRoutingModule } from './page-task-calendar-routing.module'; @@ -19,6 +20,7 @@ import { PageTaskCalendarComponent } from './page-task-calendar.component'; ModalsModule, UiFilterNextModule, ShellFilterOverlayModule, + TaskSearchbarModule, ], exports: [PageTaskCalendarComponent], }) diff --git a/apps/page/task-calendar/src/lib/pages/calendar/calendar.component.html b/apps/page/task-calendar/src/lib/pages/calendar/calendar.component.html index 3383468f6..dce4024fb 100644 --- a/apps/page/task-calendar/src/lib/pages/calendar/calendar.component.html +++ b/apps/page/task-calendar/src/lib/pages/calendar/calendar.component.html @@ -1,10 +1,12 @@ - +
+ +
diff --git a/apps/page/task-calendar/src/lib/pages/task-search/index.ts b/apps/page/task-calendar/src/lib/pages/task-search/index.ts new file mode 100644 index 000000000..d0e10077e --- /dev/null +++ b/apps/page/task-calendar/src/lib/pages/task-search/index.ts @@ -0,0 +1,4 @@ +// start:ng42.barrel +export * from './task-search.component'; +export * from './task-search.module'; +// end:ng42.barrel diff --git a/apps/page/task-calendar/src/lib/pages/task-search/task-search.component.html b/apps/page/task-calendar/src/lib/pages/task-search/task-search.component.html new file mode 100644 index 000000000..d7c144b4a --- /dev/null +++ b/apps/page/task-calendar/src/lib/pages/task-search/task-search.component.html @@ -0,0 +1,39 @@ + + +
+
+
+

{{ group.group | date: 'EEEE' }}, {{ group.group | date }}

+
+
+ + {{ group.items?.length }} Aufgaben und Infos +
+
+
+ + +
+
+
+
+
+ + +
+ Keine Suchergebnisse +
+
+ + diff --git a/apps/page/task-calendar/src/lib/pages/task-search/task-search.component.scss b/apps/page/task-calendar/src/lib/pages/task-search/task-search.component.scss new file mode 100644 index 000000000..a6096db92 --- /dev/null +++ b/apps/page/task-calendar/src/lib/pages/task-search/task-search.component.scss @@ -0,0 +1,73 @@ +:host { + @apply flex flex-col box-border; +} + +.task-list { + @apply pt-4; + + .head-row { + @apply flex flex-row justify-between items-center px-5 py-3; + height: 53px; + background-color: #f5f7fa; + border-radius: 5px 5px 0px 0px; + box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5); + + .head-row-title { + @apply flex flex-row text-lg; + + .muted { + @apply ml-4 self-end; + } + } + + .head-row-info { + @apply flex flex-col text-right justify-center items-end; + + .cta-print { + @apply border-none outline-none bg-transparent text-brand text-cta-l font-bold pr-0; + } + } + + h4 { + @apply m-0 font-bold; + } + + .muted { + @apply text-active-branch font-semibold text-sm; + } + } +} + +.empty-message { + @apply bg-white text-center font-semibold text-inactive-customer py-10 rounded-card; +} + +:host ::ng-deep ui-scroll-container .scroll-container { + @apply flex flex-col; + max-height: calc(100vh - 23rem) !important; +} + +.task-list ::ng-deep page-task-list-item.Removed { + .date, + .shirt-size, + .task-content { + @apply opacity-30 line-through; + } +} + +.actions { + @apply fixed bottom-28 inline-grid grid-flow-col gap-7; + left: 50%; + transform: translateX(-50%); + + .cta-back { + @apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap; + &:disabled { + @apply bg-inactive-branch border-inactive-branch; + } + } + + .cta-action-primary { + @apply bg-brand text-white; + } +} diff --git a/apps/page/task-calendar/src/lib/pages/task-search/task-search.component.ts b/apps/page/task-calendar/src/lib/pages/task-search/task-search.component.ts new file mode 100644 index 000000000..b9a89fdf9 --- /dev/null +++ b/apps/page/task-calendar/src/lib/pages/task-search/task-search.component.ts @@ -0,0 +1,135 @@ +import { DatePipe } from '@angular/common'; +import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { BreadcrumbService } from '@core/breadcrumb'; +import { Config } from '@core/config'; +import { DomainPrinterService } from '@domain/printer'; +import { DomainTaskCalendarService } from '@domain/task-calendar'; +import { PrintModalComponent, PrintModalData } from '@modal/printer'; +import { DisplayInfoDTO } from '@swagger/eis'; +import { DateAdapter, groupBy } from '@ui/common'; +import { UiFilter } from '@ui/filter'; +import { UiModalService } from '@ui/modal'; +import { combineLatest, Subject } from 'rxjs'; +import { first, map, takeUntil, debounceTime } from 'rxjs/operators'; +import { TaskCalendarStore } from '../../task-calendar.store'; + +@Component({ + selector: 'page-task-search', + templateUrl: 'task-search.component.html', + styleUrls: ['task-search.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [DatePipe], +}) +export class TaskSearchComponent implements OnInit { + private _onDestroy$ = new Subject(); + + today = this.dateAdapter.today(); + byDate = (item: DisplayInfoDTO) => this.domainTaskCalendarService.getDateGroupKey(item.taskDate || item.publicationDate); + + searchResults$ = this.taskCalendarStore.searchResults$; + searchResultsLength$ = this.searchResults$.pipe(map((r) => r.length || 0)); + fetching$ = this.taskCalendarStore.isSearching$; + + displayItems$ = this.searchResults$.pipe( + map((r) => { + const grouped = groupBy(r, this.byDate); + grouped.sort((a, b) => new Date(b.group).getTime() - new Date(a.group).getTime()); + return grouped; + }), + // Sortierung der aufgaben nach Rot => Gelb => Grau => Grün + map((grouped) => + grouped.map((g) => ({ + ...g, + items: this.domainTaskCalendarService.sort(g.items, ['Overdue', 'InProcess', 'Approved', 'Completed', 'Removed']), + })) + ), + // Entfernte ans ende der Gruppe setzen + map((grouped) => grouped.map((g) => ({ ...g, items: g.items.sort((a, b) => this.domainTaskCalendarService.moveRemovedToEnd(a, b)) }))) + ); + + showEmptyMessage$ = combineLatest([this.fetching$, this.searchResultsLength$, this.taskCalendarStore.hits$]).pipe( + map(([fetching, length, hits]) => !fetching && length <= 0 && hits === 0) + ); + + constructor( + private dateAdapter: DateAdapter, + private taskCalendarStore: TaskCalendarStore, + private domainTaskCalendarService: DomainTaskCalendarService, + private uiModal: UiModalService, + private domainPrinterService: DomainPrinterService, + private datePipe: DatePipe, + private _activatedRoute: ActivatedRoute, + private _config: Config, + private _breadcrumb: BreadcrumbService + ) {} + + ngOnInit() { + this._activatedRoute.queryParams.pipe(takeUntil(this._onDestroy$), debounceTime(500)).subscribe(async (queryParams) => { + if (queryParams) { + const filters = UiFilter.create(await this.taskCalendarStore.selectFilter.pipe(first()).toPromise()); + if (queryParams) { + filters.fromQueryParams(queryParams); + } + this.taskCalendarStore.setFilter({ filters }); + } + + this.taskCalendarStore.search({ clear: true }); + this.updateBreadcrumb(queryParams); + }); + + this.taskCalendarStore.searchTarget$.pipe(takeUntil(this._onDestroy$)).subscribe((target) => { + setTimeout(() => { + document.getElementById(target)?.scrollIntoView(); + }, 150); + }); + } + + open(item: DisplayInfoDTO) { + this.taskCalendarStore.open(item); + } + + async print(date: string) { + let displayInfos = await this.searchResults$.pipe(first()).toPromise(); + displayInfos = displayInfos.filter((di) => !this.domainTaskCalendarService.getProcessingStatusList(di).includes('Completed')); + this.uiModal.open({ + content: PrintModalComponent, + data: { + printerType: 'Office', + print: (printer) => + this.domainPrinterService + .printDisplayInfoDTOList({ + displayInfos, + printer, + title: `Tätigkeitskalender \n Liste für ${this.datePipe.transform(date, 'EEEE, dd. MMMM yyyy')}`, + }) + .toPromise(), + } as PrintModalData, + config: { + panelClass: [], + showScrollbarY: false, + }, + }); + } + + async search() { + const hits = await this.taskCalendarStore.hits$.pipe(first()).toPromise(); + const results = await this.taskCalendarStore.searchResults$.pipe(first()).toPromise(); + const fetching = await this.taskCalendarStore.selectFetching.pipe(first()).toPromise(); + + if (hits > results.length && !fetching) { + this.taskCalendarStore.search({}); + } + } + + updateBreadcrumb(queryParams?: Params) { + this._breadcrumb.addOrUpdateBreadcrumbIfNotExists({ + key: this._config.get('process.ids.taskCalendar'), + name: queryParams?.main_qs || 'Suchergebnisse', + path: '/filiale/task-calendar/search', + tags: ['task-calendar', 'search'], + section: 'branch', + params: queryParams, + }); + } +} diff --git a/apps/page/task-calendar/src/lib/pages/task-search/task-search.module.ts b/apps/page/task-calendar/src/lib/pages/task-search/task-search.module.ts new file mode 100644 index 000000000..d6a3e3130 --- /dev/null +++ b/apps/page/task-calendar/src/lib/pages/task-search/task-search.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { UiCommonModule } from '@ui/common'; +import { UiIconModule } from '@ui/icon'; +import { RouterModule } from '@angular/router'; +import { UiScrollContainerModule } from '@ui/scroll-container'; +import { TaskSearchComponent } from './task-search.component'; +import { TaskListModule } from '../../components/task-list'; + +@NgModule({ + imports: [CommonModule, UiCommonModule, UiIconModule, RouterModule, UiScrollContainerModule, TaskListModule], + exports: [TaskSearchComponent], + declarations: [TaskSearchComponent], +}) +export class TaskSearchModule {} diff --git a/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.html b/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.html index d65d6b507..b88e91262 100644 --- a/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.html +++ b/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.html @@ -1,12 +1,19 @@ - +
+ +
- + diff --git a/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.scss b/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.scss index a9804769f..249c71ad1 100644 --- a/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.scss +++ b/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.scss @@ -1,3 +1,3 @@ :host { - @apply block pb-24; + @apply block; } diff --git a/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.ts b/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.ts index 740024ef2..eb14284bb 100644 --- a/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.ts +++ b/apps/page/task-calendar/src/lib/pages/tasks/tasks.component.ts @@ -21,6 +21,10 @@ export class TasksComponent implements OnInit { readonly items$ = this.taskCalendarStore.selectDisplayInfos; + readonly searchResults$ = this.taskCalendarStore.searchResults$; + + readonly fetching$ = this.taskCalendarStore.select((s) => s.fetching); + readonly minDate = this.dateAdapter.addCalendarMonths(this.dateAdapter.today(), -6); readonly maxDate = this.dateAdapter.addCalendarMonths(this.dateAdapter.today(), 6); @@ -46,8 +50,11 @@ export class TasksComponent implements OnInit { this.updateBreadcrumb({}); } - this.taskCalendarStore.setMode({ mode: 'week' }); this.taskCalendarStore.loadItems(); + this.removeSearchBreadcrumbs(); + + this.taskCalendarStore.resetSearch(); + this.taskCalendarStore.setMode({ mode: 'week' }); } setSelectedAndDisplayedDate({ displayDate, selectedDate }: { displayDate?: Date; selectedDate?: Date }) { @@ -56,7 +63,6 @@ export class TasksComponent implements OnInit { if (displayDate && selectedDate) { this.taskCalendarStore.setSelectedDate({ date: selectedDate }); this.taskCalendarStore.setDisplayedDate({ date: displayDate }); - this.taskCalendarStore.loadItems(); this.router.navigate([], { queryParams: { displayDate: displayDate?.toJSON(), selectedDate: selectedDate?.toJSON() }, }); @@ -74,7 +80,6 @@ export class TasksComponent implements OnInit { if (displayDate) { this.taskCalendarStore.setDisplayedDate({ date: displayDate }); - this.taskCalendarStore.loadItems(); this.router.navigate([], { queryParams: { ...queryParams, displayDate: displayDate?.toJSON() }, }); @@ -86,6 +91,10 @@ export class TasksComponent implements OnInit { this.taskCalendarStore.open(item); } + removeSearchBreadcrumbs() { + this.breadcrumb.removeBreadcrumbsByKeyAndTags(this._config.get('process.ids.taskCalendar'), ['task-calendar', 'search']); + } + updateBreadcrumb({ displayDate, selectedDate }: { displayDate?: Date; selectedDate?: Date }) { const queryParams = this.activatedRoute.snapshot.queryParams; this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({ diff --git a/apps/page/task-calendar/src/lib/pages/tasks/tasks.module.ts b/apps/page/task-calendar/src/lib/pages/tasks/tasks.module.ts index 5346d5055..83db4f426 100644 --- a/apps/page/task-calendar/src/lib/pages/tasks/tasks.module.ts +++ b/apps/page/task-calendar/src/lib/pages/tasks/tasks.module.ts @@ -4,7 +4,6 @@ import { CommonModule } from '@angular/common'; import { TasksComponent } from './tasks.component'; import { UiCalendarModule } from '@ui/calendar'; import { TaskListModule } from '../../components/task-list'; - @NgModule({ imports: [CommonModule, UiCalendarModule, TaskListModule], exports: [TasksComponent], diff --git a/apps/page/task-calendar/src/lib/task-calendar.store.ts b/apps/page/task-calendar/src/lib/task-calendar.store.ts index eb4de4eeb..dcccff4f3 100644 --- a/apps/page/task-calendar/src/lib/task-calendar.store.ts +++ b/apps/page/task-calendar/src/lib/task-calendar.store.ts @@ -1,14 +1,14 @@ import { Injectable } from '@angular/core'; import { DomainTaskCalendarService } from '@domain/task-calendar'; import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { DisplayInfoDTO, InputDTO, QuerySettingsDTO, ResponseArgsOfIEnumerableOfInputDTO } from '@swagger/eis'; +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 } from 'rxjs'; -import { debounceTime, map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { debounceTime, map, switchMap, tap, withLatestFrom, first, 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'; @@ -21,7 +21,11 @@ export interface TaskCalendarState { initialFilter: UiFilter; filter: UiFilter; message: string; + searchResults: DisplayInfoDTO[]; + searchTarget: string; fetching: boolean; + isSearching: boolean; + hits: number; } @Injectable() @@ -38,6 +42,14 @@ export class TaskCalendarStore extends ComponentStore { 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 isSearching$ = this.select((s) => s.isSearching); + readonly selectCalendarIndicators = this.select(this.selectDisplayInfos, (displayItems) => displayItems.reduce((agg, item) => { const calendarIndicator = this.mapDisplayInfoToCalendarIndicator(item); @@ -60,6 +72,7 @@ export class TaskCalendarStore extends ComponentStore { readonly selectFilter = this.select((s) => s.filter); readonly selectFetching = this.select((s) => s.fetching); + readonly fetching = this.get((s) => s.fetching); readonly selectMessage = this.select((s) => s.message); @@ -88,9 +101,11 @@ export class TaskCalendarStore extends ComponentStore { } }); + hits = this.get((s) => s.hits); + constructor( + public domainTaskCalendarService: DomainTaskCalendarService, private dateAdapter: DateAdapter, - private domainTaskCalendarService: DomainTaskCalendarService, private uiModal: UiModalService, private uiFilterMappingService: UiFilterMappingService ) { @@ -103,6 +118,10 @@ export class TaskCalendarStore extends ComponentStore { filter: undefined, message: undefined, fetching: false, + searchResults: [], + isSearching: false, + hits: undefined, + searchTarget: '', }); } @@ -126,11 +145,89 @@ export class TaskCalendarStore extends ComponentStore { 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, + })); + + readonly search = this.effect((options$: Observable<{ clear?: boolean }>) => + options$.pipe( + tap(() => this.patchState({ isSearching: true })), + debounceTime(500), + withLatestFrom(this.domainTaskCalendarService.currentBranchId$, this.selectFilter), + switchMap(([options, branchId, filter]) => { + const querytoken = { + ...filter?.getQueryToken(), + // Paging ist vorbereitet aber vorerst deaktiviert + // skip: results.length || 0, + // take: 50, + }; + + // Im Zeitraum von 6 Monaten in der Vergangenheit und 6 Monate in der Zukunft abfragen + const start = this.dateAdapter.addCalendarMonths(this.dateAdapter.today(), -6); + const stop = this.dateAdapter.addCalendarMonths(this.dateAdapter.today(), 6); + 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)) + ); + this.patchState({ + searchResults, + searchTarget: + sorted?.length > 0 && response.skip === 0 + ? this.domainTaskCalendarService.getDateGroupKey(sorted[0].taskDate || sorted[0].publicationDate) + : '', + isSearching: false, + message: undefined, + hits: response.hits, + }); + } else { + this.uiModal.open({ content: UiMessageModalComponent, data: response }); + this.patchState({ searchResults: [], isSearching: false, message: 'Keine Suchergebnisse' }); + } + }, + (error) => { + console.error(error); + this.patchState({ searchResults: [], isSearching: false, message: 'Keine Suchergebnisse' }); + } + ) + ); + }) + ) + ); + + resetSearch() { + this.patchState({ searchResults: [], hits: undefined }); + } + readonly loadItems = this.effect(($: Observable) => $.pipe( tap(() => this.patchState({ fetching: true })), debounceTime(500), - withLatestFrom(this.domainTaskCalendarService.currentBranchId$, this.selectStartStop, this.selectFilter), + withLatestFrom(this.domainTaskCalendarService.currentBranchId$, this.selectStartStop, this.selectInitialFilter), switchMap(([_, branchId, date, filter]) => { const querytoken = { ...filter?.getQueryToken() }; return this.domainTaskCalendarService @@ -146,13 +243,7 @@ export class TaskCalendarStore extends ComponentStore { tapResponse( (response) => { if (!response.error) { - 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); - }); - + response = this.preparePreInfos(response); this.patchState({ displayInfos: response.result, fetching: false, message: undefined }); } else { this.uiModal.open({ @@ -172,6 +263,16 @@ export class TaskCalendarStore extends ComponentStore { ) ); + 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({ fetching: true })), @@ -227,7 +328,20 @@ export class TaskCalendarStore extends ComponentStore { } } }, - complete: () => this.loadItems(), + }); + + 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 }); + } + } + }, }); } diff --git a/apps/ui/calendar/src/lib/calendar-body/calendar-body.component.scss b/apps/ui/calendar/src/lib/calendar-body/calendar-body.component.scss index 307c6f9f0..77d3ea450 100644 --- a/apps/ui/calendar/src/lib/calendar-body/calendar-body.component.scss +++ b/apps/ui/calendar/src/lib/calendar-body/calendar-body.component.scss @@ -1,5 +1,5 @@ :host { - @apply grid grid-cols-8; + @apply grid grid-cols-8 pr-4; } .cell { @@ -11,7 +11,8 @@ } .cell-day-of-week:not(.cell-first) { - @apply border-0 border-t-2 border-solid border-gray-200; + @apply border-0 border-t-2 border-solid; + border-color: #edeff0; } .cell-first { diff --git a/apps/ui/calendar/src/lib/calendar-header/calendar-header.component.html b/apps/ui/calendar/src/lib/calendar-header/calendar-header.component.html index e459468ee..a13e58445 100644 --- a/apps/ui/calendar/src/lib/calendar-header/calendar-header.component.html +++ b/apps/ui/calendar/src/lib/calendar-header/calendar-header.component.html @@ -1,15 +1,26 @@