diff --git a/.vscode/settings.json b/.vscode/settings.json
index a11e0188e..7c1a60c0f 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -37,7 +37,7 @@
"file": ".github/testing-instructions.md"
},
{
- "file": "docs/text-stack.md"
+ "file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
@@ -60,7 +60,7 @@
"file": ".github/testing-instructions.md"
},
{
- "file": "docs/text-stack.md"
+ "file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
@@ -77,7 +77,7 @@
"file": ".github/review-instructions.md"
},
{
- "file": "docs/text-stack.md"
+ "file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
diff --git a/apps/isa-app/src/app/app-routing.module.ts b/apps/isa-app/src/app/app-routing.module.ts
index be53146e9..e547f3877 100644
--- a/apps/isa-app/src/app/app-routing.module.ts
+++ b/apps/isa-app/src/app/app-routing.module.ts
@@ -163,7 +163,7 @@ const routes: Routes = [
children: [
{
path: 'return',
- loadChildren: () => import('@feature/return/pages').then((m) => m.routes),
+ loadChildren: () => import('@isa/oms/feature/return-search').then((m) => m.routes),
},
],
},
diff --git a/libs/feature/return/pages/src/lib/main/main-page.component.css b/libs/feature/return/pages/src/lib/main/main-page.component.css
index ea7e7eabb..63c1ceeb7 100644
--- a/libs/feature/return/pages/src/lib/main/main-page.component.css
+++ b/libs/feature/return/pages/src/lib/main/main-page.component.css
@@ -1,7 +1,3 @@
:host {
@apply flex flex-col pt-12 items-center;
}
-
-.lib-return-main-page__loading-spinner {
- @apply h-12 w-full flex items-center justify-center mb-12;
-}
diff --git a/libs/oms/feature/return-search/src/index.ts b/libs/oms/feature/return-search/src/index.ts
index b283e35c7..ece2c025d 100644
--- a/libs/oms/feature/return-search/src/index.ts
+++ b/libs/oms/feature/return-search/src/index.ts
@@ -1 +1 @@
-export * from './lib/oms-feature-return-search/oms-feature-return-search.component';
+export * from './lib/routes';
diff --git a/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.html b/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.html
index e69de29bb..0e169f790 100644
--- a/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.html
+++ b/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.html
@@ -0,0 +1,27 @@
+
+
Rückgabe starten
+
+ Scannen Sie den QR-Code auf der Rechnung oder suchen Sie den Beleg
+
+ via Rechnungsnummer, E-Mail-Adresse oder Kundennamen
+
+
+
+
+
+@if (entityPending()) {
+
+
+
+}
+
+
+ @for (filterInput of filterInputs(); track filterInput.key) {
+
+
+ }
+
diff --git a/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.scss b/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.scss
index e69de29bb..63c1ceeb7 100644
--- a/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.scss
+++ b/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.scss
@@ -0,0 +1,3 @@
+:host {
+ @apply flex flex-col pt-12 items-center;
+}
diff --git a/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.ts b/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.ts
index e1bc27b44..ea6120d35 100644
--- a/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.ts
+++ b/libs/oms/feature/return-search/src/lib/return-search-main/return-search-main.component.ts
@@ -1,9 +1,62 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { injectActivatedProcessId } from '@isa/core/process';
+import { ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
+import {
+ FilterService,
+ SearchBarInputComponent,
+ FilterInputMenuButtonComponent,
+} from '@isa/shared/filter';
+import { IconButtonComponent } from '@isa/ui/buttons';
@Component({
selector: 'oms-feature-return-search-main',
templateUrl: './return-search-main.component.html',
styleUrls: ['./return-search-main.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [SearchBarInputComponent, IconButtonComponent, FilterInputMenuButtonComponent],
})
-export class ReturnSearchMainComponent {}
+export class ReturnSearchMainComponent {
+ #route = inject(ActivatedRoute);
+ #router = inject(Router);
+
+ private _processId = injectActivatedProcessId();
+ private _filterService = inject(FilterService);
+ private _returnSearchStore = inject(ReturnSearchStore);
+
+ private _entity = computed(() => {
+ const processId = this._processId();
+ if (processId) {
+ return this._returnSearchStore.getEntity(processId);
+ }
+ return undefined;
+ });
+
+ entityPending = computed(() => {
+ return this._entity()?.status === ReturnSearchStatus.Pending;
+ });
+
+ filterInputs = computed(() =>
+ this._filterService.inputs().filter((input) => input.group === 'filter'),
+ );
+
+ // TODO: Suche als Provider in FilterService auslagern (+ Cancel Search, + Fetching Status)
+ async onSearch() {
+ const processId = this._processId();
+ if (processId) {
+ await this._updateQueryParams();
+ this._returnSearchStore.search({
+ processId,
+ params: this._filterService.toParams(),
+ });
+ }
+ }
+
+ private async _updateQueryParams() {
+ await this.#router.navigate([], {
+ queryParams: this._filterService.toParams(),
+ relativeTo: this.#route,
+ replaceUrl: true,
+ });
+ }
+}
diff --git a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.html b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.html
new file mode 100644
index 000000000..7e1566789
--- /dev/null
+++ b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.html
@@ -0,0 +1,37 @@
+
+
+ {{ name() }}
+
+
+
+ Belegdatum
+
+
+ {{ receiptDate() | date: 'dd.MM.yy' }}
+
+
+
+
+ Rechnugsnr.
+
+ {{ receiptNumber() }}
+
+
+
+ Vorgangs-ID
+
+ {{ orderNumber() }}
+
+
+
+
+
+ Email
+ {{ email() }}
+
+
+ Anschrift
+ {{ address() }}
+
+
+
diff --git a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.scss b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.ts b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.ts
new file mode 100644
index 000000000..83eed0b5f
--- /dev/null
+++ b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result-item/return-search-result-item.component.ts
@@ -0,0 +1,45 @@
+import { DatePipe } from '@angular/common';
+import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
+import { ReceiptListItem } from '@isa/oms/data-access';
+import { ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
+
+@Component({
+ selector: 'oms-feature-return-search-result-item',
+ templateUrl: './return-search-result-item.component.html',
+ styleUrls: ['./return-search-result-item.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [ClientRowImports, ItemRowDataImports, DatePipe],
+})
+export class ReturnSearchResultItemComponent {
+ item = input.required();
+
+ name = computed(() => {
+ const firstName = this.item()?.billing?.person?.firstName;
+ const lastName = this.item()?.billing?.person?.lastName;
+ const buyerName = [lastName, firstName].filter((f) => !!f);
+ const organisation = [this.item()?.billing?.organisation?.name].filter((f) => !!f);
+
+ return [organisation.join(), buyerName.join(' ')].filter((f) => !!f).join(' - ');
+ });
+
+ receiptDate = computed(() => {
+ return this.item()?.printedDate;
+ });
+
+ receiptNumber = computed(() => {
+ return this.item()?.receiptNumber;
+ });
+
+ orderNumber = computed(() => {
+ return this.item()?.orderNumber;
+ });
+
+ email = computed(() => {
+ return this.item()?.billing?.communicationDetails?.email;
+ });
+
+ address = computed(() => {
+ const address = this.item()?.billing?.address;
+ return address ? [address.zipCode, address.city].join(' ') : '';
+ });
+}
diff --git a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.html b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.html
index e69de29bb..65dbf86dd 100644
--- a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.html
+++ b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+ @if (isMobileDevice()) {
+
+
+
+ } @else {
+
+ }
+
+
+
+@if (isMobileDevice() && toggleOrderByToolbar()) {
+
+}
+
+
+ {{ entityHits() }} Einträge
+
+
+@let items = entityItems();
+@if (items.length > 0) {
+
+ @for (item of items; track item.id) {
+ @defer (on viewport) {
+
+
+
+ } @placeholder {
+
+
+
+
+ }
+ }
+ @if (entityStatus() === ReturnSearchStatus.Pending) {
+
+
+
+ }
+
+} @else if (items.length === 0 && entityStatus() === ReturnSearchStatus.Pending) {
+
+
+
+} @else if (entityStatus() !== ReturnSearchStatus.Idle) {
+
+
+}
diff --git a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.scss b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.scss
index e69de29bb..8950d55b8 100644
--- a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.scss
+++ b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.scss
@@ -0,0 +1,3 @@
+:host {
+ @apply flex flex-col gap-4 w-full justify-start items-center;
+}
diff --git a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.ts b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.ts
index 0ca39eb5a..027a0214c 100644
--- a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.ts
+++ b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.ts
@@ -1,9 +1,176 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ effect,
+ inject,
+ QueryList,
+ signal,
+ untracked,
+ viewChildren,
+} from '@angular/core';
+import { ReturnOrderByListComponent } from '@feature/return/containers';
+import { injectActivatedProcessId } from '@isa/core/process';
+
+import { ActivatedRoute, Router, RouterLink } from '@angular/router';
+import {
+ FilterMenuButtonComponent,
+ FilterService,
+ SearchBarInputComponent,
+} from '@isa/shared/filter';
+import { IconButtonComponent } from '@isa/ui/buttons';
+import { EmptyStateComponent } from '@isa/ui/empty-state';
+import { restoreScrollPosition } from '@isa/core/scroll-position';
+import { Platform } from '@angular/cdk/platform';
+import { NgIconComponent, provideIcons } from '@ng-icons/core';
+import { isaActionSort } from '@isa/icons';
+import { ReturnSearchEntity, ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
+import { ReturnSearchResultItemComponent } from './return-search-result-item/return-search-result-item.component';
+
+type EmptyState = {
+ title: string;
+ description: string;
+};
@Component({
selector: 'oms-feature-return-search-result',
templateUrl: './return-search-result.component.html',
styleUrls: ['./return-search-result.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [
+ RouterLink,
+ ReturnSearchResultItemComponent,
+ ReturnOrderByListComponent,
+ IconButtonComponent,
+ SearchBarInputComponent,
+ EmptyStateComponent,
+ NgIconComponent,
+ FilterMenuButtonComponent,
+ ],
+ providers: [provideIcons({ isaActionSort })],
})
-export class ReturnSearchResultComponent {}
+export class ReturnSearchResultComponent {
+ #route = inject(ActivatedRoute);
+ #router = inject(Router);
+ #platform = inject(Platform);
+
+ private _processId = injectActivatedProcessId();
+ private _returnSearchStore = inject(ReturnSearchStore);
+ private _filterService = inject(FilterService);
+
+ ReturnSearchStatus = ReturnSearchStatus;
+
+ filterInputs = computed(() =>
+ this._filterService.inputs().filter((input) => input.group === 'filter'),
+ );
+
+ private _entity = computed(() => {
+ const processId = this._processId();
+ if (processId) {
+ return this._returnSearchStore.getEntity(processId);
+ }
+ return undefined;
+ });
+
+ entityItems = computed(() => {
+ return this._entity()?.items ?? [];
+ });
+
+ entityHits = computed(() => {
+ return this._entity()?.hits ?? 0;
+ });
+
+ entityStatus = computed(() => {
+ return this._entity()?.status ?? ReturnSearchStatus.Idle;
+ });
+
+ emptyState = computed(() => {
+ return {
+ title: 'Keine Suchergebnisse',
+ description: 'Suchen Sie nach einer Rechnungsnummer oder Kundennamen.',
+ };
+ });
+
+ listElements = viewChildren>('listElement');
+
+ isMobileDevice = signal(this.#platform.ANDROID || this.#platform.IOS);
+ toggleOrderByToolbar = signal(false);
+
+ searchEffectFn = () =>
+ effect(() => {
+ const processId = this._processId();
+ const listLength = this.listElements().length;
+
+ untracked(async () => {
+ if (processId) {
+ const entity = this._entity();
+ if (entity) {
+ const isPending = this.entityStatus() === ReturnSearchStatus.Pending;
+ // Trigger reload search if no search request is already pending and
+ // 1. List scrolled to bottom
+ // 2. After Process change AND no items in entity
+ if (!isPending) {
+ this._reload({ processId, entity, listLength });
+ }
+ } else {
+ // Init Search after F5 / Refresh Page / No Entity Available
+ await this._initSearch();
+ }
+ }
+ });
+ });
+
+ constructor() {
+ this.searchEffectFn();
+ restoreScrollPosition();
+ }
+
+ // TODO: Suche als Provider in FilterService auslagern (+ Cancel Search, + Fetching Status)
+ async onSearch() {
+ const processId = this._processId();
+ if (processId) {
+ await this._updateQueryParams();
+ this._returnSearchStore.search({
+ processId,
+ params: this._filterService.toParams(),
+ });
+ }
+ }
+
+ private async _updateQueryParams() {
+ return await this.#router.navigate([], {
+ queryParams: this._filterService.toParams(),
+ relativeTo: this.#route,
+ replaceUrl: true,
+ });
+ }
+
+ private async _reload({
+ processId,
+ entity,
+ listLength,
+ }: {
+ processId: number;
+ entity: ReturnSearchEntity;
+ listLength: number;
+ }) {
+ const entityItemsLength = entity?.items?.length ?? 0;
+ const hits = entity?.hits ?? 0;
+ // Soll reloaden wenn man am Ende der Liste in der View angekommen ist und noch nicht alle Items insgesamt geladen wurden
+ if (listLength === entityItemsLength && hits !== entityItemsLength) {
+ await this._updateQueryParams();
+ this._returnSearchStore.reload({
+ processId,
+ params: this._filterService.toParams(),
+ });
+ }
+ }
+
+ private async _initSearch() {
+ const entities = this._returnSearchStore.entities();
+ // For routing away from the list this entities?.length === 0 check is necessary, otherwise the init search would trigger again
+ if (entities?.length === 0) {
+ await this.onSearch();
+ }
+ }
+}
diff --git a/libs/oms/feature/return-search/src/lib/return-search.component.ts b/libs/oms/feature/return-search/src/lib/return-search.component.ts
index c2b5cfb68..3f387554f 100644
--- a/libs/oms/feature/return-search/src/lib/return-search.component.ts
+++ b/libs/oms/feature/return-search/src/lib/return-search.component.ts
@@ -3,13 +3,14 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { injectActivatedProcessId } from '@isa/core/process';
import { ReturnSearchStore } from '@isa/oms/data-access';
-import { FilterService } from '@isa/shared/filter';
+import { FilterService, provideQuerySettings } from '@isa/shared/filter';
@Component({
selector: 'oms-feature-return-search',
template: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterOutlet],
+ providers: [provideQuerySettings(() => inject(ActivatedRoute).snapshot.data['querySettings'])],
host: {
'[class]': '"flex flex-col gap-5 isa-desktop:gap-6 items-center overflow-x-hidden"',
},
@@ -54,11 +55,11 @@ export class ReturnSearchComponent {
if (items) {
if (items?.length === 1) {
- return await this._navigateTo(['receipt', items[0].id.toString()]);
+ return await this._navigateTo(['receipts', items[0].id.toString()]);
}
if (items?.length >= 0) {
- return await this._navigateTo(['results']);
+ return await this._navigateTo(['receipts']);
}
}
diff --git a/libs/oms/feature/return-search/src/lib/routes.ts b/libs/oms/feature/return-search/src/lib/routes.ts
index fa626c736..9f7277a7a 100644
--- a/libs/oms/feature/return-search/src/lib/routes.ts
+++ b/libs/oms/feature/return-search/src/lib/routes.ts
@@ -1,19 +1,21 @@
-import { ActivatedRoute, Routes } from '@angular/router';
+import { Routes } from '@angular/router';
import { ReturnSearchMainComponent } from './return-search-main/return-search-main.component';
import { ReturnSearchComponent } from './return-search.component';
import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
-import { provideQuerySettings } from '@isa/shared/filter';
-import { inject } from '@angular/core';
+import { ReturnSearchResultComponent } from './return-search-result/return-search-result.component';
export const routes: Routes = [
{
path: '',
component: ReturnSearchComponent,
resolve: { querySettings: querySettingsResolverFn },
- providers: [provideQuerySettings(() => inject(ActivatedRoute).snapshot.data['querySettings'])],
children: [
{ path: '', component: ReturnSearchMainComponent },
- { path: 'returns', component: ReturnSearchMainComponent },
+ { path: 'receipts', component: ReturnSearchResultComponent },
],
},
+ {
+ path: 'process',
+ loadChildren: () => import('@isa/oms/feature/return-process').then((feat) => feat.routes),
+ },
];
diff --git a/libs/shared/filter/src/lib/actions/filter-actions.component.ts b/libs/shared/filter/src/lib/actions/filter-actions.component.ts
index 01d873428..5d305b8c0 100644
--- a/libs/shared/filter/src/lib/actions/filter-actions.component.ts
+++ b/libs/shared/filter/src/lib/actions/filter-actions.component.ts
@@ -51,7 +51,9 @@ export class FilterActionsComponent {
const inputKey = this.inputKey();
if (!inputKey) {
- this.filterService.reset();
+ this.filterInputs().forEach((input) => {
+ this.filterService.resetInput(input.key);
+ });
} else {
this.filterService.resetInput(inputKey);
}
diff --git a/libs/shared/filter/src/lib/core/filter.service.ts b/libs/shared/filter/src/lib/core/filter.service.ts
index 5bff99cf5..49ffac2d8 100644
--- a/libs/shared/filter/src/lib/core/filter.service.ts
+++ b/libs/shared/filter/src/lib/core/filter.service.ts
@@ -1,7 +1,9 @@
-import { inject, Injectable, InjectionToken, Provider } from '@angular/core';
+import { computed, inject, Injectable, InjectionToken, Provider } from '@angular/core';
import { InputType, QuerySettingsDTO } from '../types';
import { getState, patchState, signalState } from '@ngrx/signals';
import { mapToFilter } from './mappings';
+import { isEqual } from 'lodash-es';
+import { FilterInput } from './schemas';
export const QUERY_SETTINGS = new InjectionToken('QuerySettings');
@@ -13,7 +15,9 @@ export function provideQuerySettings(factory: () => QuerySettingsDTO): Provider[
export class FilterService {
readonly settings = inject(QUERY_SETTINGS);
- #commitdState = mapToFilter(this.settings);
+ private readonly defaultState = mapToFilter(this.settings);
+
+ #commitdState = structuredClone(this.defaultState);
#state = signalState(this.#commitdState);
@@ -65,6 +69,58 @@ export class FilterService {
}
}
+ /**
+ * Indicates whether the current state is the default state.
+ * This computed property checks if the current state is equal to the default state.
+ */
+ isDefaultFilter = computed(() => {
+ const currentState = getState(this.#state);
+ return isEqual(currentState, this.defaultState);
+ });
+
+ isDefaultFilterInput(filterInput: FilterInput) {
+ const currentInputState = this.#state.inputs().find((i) => i.key === filterInput.key);
+ const defaultInputState = this.defaultState.inputs.find((i) => i.key === filterInput.key);
+
+ return isEqual(currentInputState, defaultInputState);
+ }
+
+ /**
+ * Indicates whether the current state is empty.
+ */
+ isEmptyFilter = computed(() => {
+ const currentState = getState(this.#state);
+ return currentState.inputs.every((input) => {
+ if (input.type === InputType.Text) {
+ return !input.value;
+ }
+
+ if (input.type === InputType.Checkbox) {
+ return !input.selected?.length;
+ }
+
+ console.warn(`Input type not supported: ${input.type}`);
+
+ return true;
+ });
+ });
+
+ isEmptyFilterInput(filterInput: FilterInput) {
+ const currentInputState = this.#state.inputs().find((i) => i.key === filterInput.key);
+
+ if (currentInputState?.type === InputType.Text) {
+ return !currentInputState.value;
+ }
+
+ if (currentInputState?.type === InputType.Checkbox) {
+ return !currentInputState.selected?.length;
+ }
+
+ console.warn(`Input type not supported: ${currentInputState?.type}`);
+
+ return true;
+ }
+
/**
* Reverts the current state to the last committed state.
* This method restores the state by applying the previously saved committed state.
diff --git a/libs/shared/filter/src/lib/inputs/checkbox-input/checkbox-input.component.html b/libs/shared/filter/src/lib/inputs/checkbox-input/checkbox-input.component.html
index 8a698413d..30a5e6753 100644
--- a/libs/shared/filter/src/lib/inputs/checkbox-input/checkbox-input.component.html
+++ b/libs/shared/filter/src/lib/inputs/checkbox-input/checkbox-input.component.html
@@ -2,9 +2,7 @@
@let options = input().options;
@if (inp && options) {
-