Refactored return feature components and removed unused files.

- 🛠️ **Refactor**: Cleaned up return feature components and styles
- 🗑️ **Chore**: Deleted obsolete files and updated exports
- 📚 **Docs**: Added README for return details library
This commit is contained in:
Lorenz Hilpert
2025-04-03 18:45:33 +02:00
parent da27745ebe
commit b21395ed61
68 changed files with 126 additions and 771 deletions

View File

@@ -1,7 +0,0 @@
# feature-return-containers
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test feature-return-containers` to execute the unit tests.

View File

@@ -1,34 +0,0 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lib',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'lib',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -1,20 +0,0 @@
{
"name": "feature-return-containers",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/feature/return/containers/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/feature/return/containers/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -1,7 +0,0 @@
export * from './lib/return-details-data/return-details-data.component';
export * from './lib/return-details-header/return-details-header.component';
export * from './lib/return-details-order-group/return-details-order-group.component';
export * from './lib/return-details-order-group-item/return-details-order-group-item.component';
export * from './lib/return-details-order-group-data/return-details-order-group-data.component';
export * from './lib/return-results-item-list/';
export * from './lib/return-order-by-list/';

View File

@@ -1 +0,0 @@
export * from './return-order-by-list.component';

View File

@@ -1,8 +0,0 @@
<ui-toolbar>
<span class="text-isa-neutral-600 isa-text-body-2-regular">Sortieren</span>
<div class="flex-grow"></div>
<button (click)="onClick('Belegdatum')" uiTextButton>Belegdatum</button>
<button (click)="onClick('Email')" uiTextButton>Email</button>
<button (click)="onClick('Name')" uiTextButton>Name</button>
<button (click)="onClick('PLZ')" uiTextButton>PLZ</button>
</ui-toolbar>

View File

@@ -1,18 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TextButtonComponent } from '@isa/ui/buttons';
import { ToolbarComponent } from '@isa/ui/toolbar';
@Component({
selector: 'lib-return-order-by-list',
templateUrl: './return-order-by-list.component.html',
styleUrls: ['./return-order-by-list.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ToolbarComponent, TextButtonComponent],
})
export class ReturnOrderByListComponent {
onClick(label: string) {
console.log('Order By click -', label);
}
}

View File

@@ -1 +0,0 @@
export * from './return-results-item-list.component';

View File

@@ -1,37 +0,0 @@
<ui-client-row>
<ui-client-row-content>
<h3 class="isa-text-subtitle-1-regular">{{ name() }}</h3>
</ui-client-row-content>
<ui-item-row-data>
<ui-item-row-data-row>
<ui-item-row-data-label>Belegdatum</ui-item-row-data-label>
<ui-item-row-data-value>
<span class="isa-text-body-2-bold">
{{ receiptDate() | date: "dd.MM.yy" }}
</span>
</ui-item-row-data-value>
</ui-item-row-data-row>
<ui-item-row-data-row>
<ui-item-row-data-label>Rechnugsnr.</ui-item-row-data-label>
<ui-item-row-data-value>
<span class="isa-text-body-2-bold"> {{ receiptNumber() }} </span>
</ui-item-row-data-value>
</ui-item-row-data-row>
<ui-item-row-data-row>
<ui-item-row-data-label>Vorgangs-ID</ui-item-row-data-label>
<ui-item-row-data-value>
<span class="isa-text-body-2-bold"> {{ orderNumber() }} </span>
</ui-item-row-data-value>
</ui-item-row-data-row>
</ui-item-row-data>
<ui-item-row-data>
<ui-item-row-data-row>
<ui-item-row-data-label>Email</ui-item-row-data-label>
<ui-item-row-data-value>{{ email() }}</ui-item-row-data-value>
</ui-item-row-data-row>
<ui-item-row-data-row>
<ui-item-row-data-label>Anschrift</ui-item-row-data-label>
<ui-item-row-data-value>{{ address() }}</ui-item-row-data-value>
</ui-item-row-data-row>
</ui-item-row-data>
</ui-client-row>

View File

@@ -1,46 +0,0 @@
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: 'lib-return-results-item-list',
templateUrl: './return-results-item-list.component.html',
styleUrls: ['./return-results-item-list.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ClientRowImports, ItemRowDataImports, DatePipe],
})
export class ReturnResultsItemListComponent {
item = input.required<ReceiptListItem>();
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(' ') : '';
});
}

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": ["src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}

View File

@@ -1,11 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"module": "commonjs",
"target": "es2016",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@@ -1,7 +0,0 @@
# feature-return-pages
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test feature-return-pages` to execute the unit tests.

View File

@@ -1,21 +0,0 @@
export default {
displayName: 'feature-return-pages',
preset: '../../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../../coverage/libs/feature/return/pages',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};

View File

@@ -1,3 +0,0 @@
:host {
@apply flex flex-col pt-12 items-center;
}

View File

@@ -1,27 +0,0 @@
<div class="flex flex-col items-center justify-center gap-4">
<h1 class="isa-text-subtitle-1-regular">Rückgabe starten</h1>
<p class="isa-text-body-1-regular text-center">
Scannen Sie den QR-Code auf der Rechnung oder suchen Sie den Beleg
<br />
via Rechnungsnummer, E-Mail-Adresse oder Kundennamen
</p>
</div>
<filter-search-bar-input
class="mt-[1.88rem] mb-[3.12rem]"
inputKey="qs"
(triggerSearch)="onSearch()"
></filter-search-bar-input>
@if (entityPending()) {
<div class="lib-return-main-page__loading-spinner">
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
</div>
}
<div class="flex flex-row gap-4">
@for (filterInput of filterInputs(); track filterInput.key) {
<filter-input-menu-button [filterInput]="filterInput" (applied)="onSearch()">
</filter-input-menu-button>
}
</div>

View File

@@ -1,63 +0,0 @@
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: 'lib-return-main-page',
templateUrl: './main-page.component.html',
styleUrls: ['./main-page.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [SearchBarInputComponent, IconButtonComponent, FilterInputMenuButtonComponent],
})
export class MainPageComponent {
#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,
});
}
}

View File

@@ -1,7 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { QuerySettingsDTO } from '@generated/swagger/oms-api';
import { ReturnSearchService } from '@isa/oms/data-access';
export const querySettingsResolverFn: ResolveFn<QuerySettingsDTO> = () =>
inject(ReturnSearchService).querySettings();

View File

@@ -1,3 +0,0 @@
:host {
@apply flex flex-col gap-4 w-full justify-start items-center;
}

View File

@@ -1,61 +0,0 @@
<div class="w-full flex flex-row justify-between items-start">
<filter-search-bar-input
class="flex flex-row gap-4 h-12"
[appearance]="'results'"
inputKey="qs"
(triggerSearch)="onSearch()"
></filter-search-bar-input>
<div class="flex flex-row gap-4 items-center">
<filter-filter-menu-button
(applied)="onSearch()"
[rollbackOnClose]="true"
></filter-filter-menu-button>
@if (isMobileDevice()) {
<ui-icon-button (click)="toggleOrderByToolbar.set(!toggleOrderByToolbar())">
<ng-icon name="isaActionSort"></ng-icon>
</ui-icon-button>
} @else {
<lib-return-order-by-list></lib-return-order-by-list>
}
</div>
</div>
@if (isMobileDevice() && toggleOrderByToolbar()) {
<lib-return-order-by-list class="w-full"></lib-return-order-by-list>
}
<span class="text-isa-neutral-900 isa-text-body-2-regular self-start">
{{ entityHits() }} Einträge
</span>
@let items = entityItems();
@if (items.length > 0) {
<div class="flex flex-col gap-4 w-full items-center justify-center">
@for (item of items; track item.id) {
@defer (on viewport) {
<a [routerLink]="['../', 'receipt', item.id]" class="w-full">
<lib-return-results-item-list #listElement [item]="item"></lib-return-results-item-list>
</a>
} @placeholder {
<!-- TODO: Den Spinner durch Skeleton Loader Kacheln ersetzen -->
<div class="h-[7.75rem] w-full flex items-center justify-center">
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
</div>
}
}
@if (entityStatus() === ReturnSearchStatus.Pending) {
<div class="h-[7.75rem] w-full flex items-center justify-center">
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
</div>
}
</div>
} @else if (items.length === 0 && entityStatus() === ReturnSearchStatus.Pending) {
<div class="h-[7.75rem] w-full flex items-center justify-center">
<ui-icon-button [pending]="true" [color]="'tertiary'"></ui-icon-button>
</div>
} @else if (entityStatus() !== ReturnSearchStatus.Idle) {
<ui-empty-state [title]="emptyState().title" [description]="emptyState().description">
</ui-empty-state>
}

View File

@@ -1,179 +0,0 @@
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
QueryList,
signal,
untracked,
viewChildren,
} from '@angular/core';
import {
ReturnResultsItemListComponent,
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';
type EmptyState = {
title: string;
description: string;
};
@Component({
selector: 'lib-return-results-page',
templateUrl: './results-page.component.html',
styleUrls: ['./results-page.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
RouterLink,
ReturnResultsItemListComponent,
ReturnOrderByListComponent,
IconButtonComponent,
SearchBarInputComponent,
EmptyStateComponent,
NgIconComponent,
FilterMenuButtonComponent,
],
providers: [provideIcons({ isaActionSort })],
})
export class ResultsPageComponent {
#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<EmptyState>(() => {
return {
title: 'Keine Suchergebnisse',
description: 'Suchen Sie nach einer Rechnungsnummer oder Kundennamen.',
};
});
listElements = viewChildren<QueryList<ReturnResultsItemListComponent>>('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();
}
}
}

View File

@@ -1,3 +0,0 @@
:host {
@apply flex flex-col gap-5 isa-desktop:gap-6 items-center overflow-x-hidden;
}

View File

@@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@@ -1,80 +0,0 @@
import { Component, effect, inject, untracked } from '@angular/core';
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, provideQuerySettings } from '@isa/shared/filter';
@Component({
selector: 'lib-return-pages',
imports: [RouterOutlet],
templateUrl: './return-pages.component.html',
styleUrl: './return-pages.component.css',
providers: [provideQuerySettings(() => inject(ActivatedRoute).snapshot.data['querySettings'])],
})
export class ReturnPagesComponent {
#route = inject(ActivatedRoute);
#router = inject(Router);
#filterService = inject(FilterService);
#returnSearchStore = inject(ReturnSearchStore);
#processId = injectActivatedProcessId();
#queryParams = toSignal(inject(ActivatedRoute).queryParams);
parseFilterParamsEffectFn = () =>
effect(() => {
const processId = this.#processId();
untracked(() => {
if (processId) {
const params = this.#queryParams();
const entity = this.#returnSearchStore.getEntity(processId);
// TODO: Caching der Entities einbauen - Aktuell kommt er sonst in den Filter Reset bei F5/Refresh Page
if (!entity || !params) {
this.#filterService.reset({ commit: true });
return;
}
this.#filterService.parseParams(params, { commit: true });
}
});
});
searchResultsNavigationEffectFn = () =>
effect(() => {
// TODO: Wenn er hier beim Prozesswechsel nicht sofort navigieren soll, dann muss auf ReturnSearchStatus.Success geprüft werden und nach der Navigation auf ReturnSearchStatus.Idle gesetzt werden
const processId = this.#processId();
if (processId) {
const entity = this.#returnSearchStore.getEntity(processId);
if (entity && entity.status !== 'error') {
untracked(async () => {
const items = entity?.items;
if (items) {
if (items?.length === 1) {
return await this._navigateTo(['receipt', items[0].id.toString()]);
}
if (items?.length >= 0) {
return await this._navigateTo(['results']);
}
}
return;
});
}
}
});
constructor() {
this.parseFilterParamsEffectFn();
this.searchResultsNavigationEffectFn();
}
private async _navigateTo(url: string[]) {
return await this.#router.navigate(url, {
queryParams: this.#filterService.toParams(),
relativeTo: this.#route,
});
}
}

View File

@@ -1,30 +0,0 @@
import { Routes } from '@angular/router';
import { ReturnPagesComponent } from './return-pages.component';
import { querySettingsResolverFn } from './resolvers/query-settings.resolver-fn';
import { MainPageComponent } from './main/main-page.component';
import { ResultsPageComponent } from './results/results-page.component';
import { DetailsPageComponent } from './details/details-page.component';
export const routes: Routes = [
{
path: '',
component: ReturnPagesComponent,
resolve: { querySettings: querySettingsResolverFn },
children: [
{ path: '', component: MainPageComponent },
{
path: 'results',
component: ResultsPageComponent,
data: { scrollPositionRestoration: true },
},
{
path: 'receipt/:receiptId',
component: DetailsPageComponent,
},
],
},
{
path: 'process',
loadChildren: () => import('@isa/oms/feature/return-process').then((feat) => feat.routes),
},
];

View File

@@ -1,6 +0,0 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});

View File

@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "es2022",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -0,0 +1,7 @@
# oms-feature-return-details
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test oms-feature-return-details` to execute the unit tests.

View File

@@ -12,7 +12,7 @@ export default [
'error',
{
type: 'attribute',
prefix: 'lib',
prefix: 'omsFeature',
style: 'camelCase',
},
],
@@ -20,7 +20,7 @@ export default [
'error',
{
type: 'element',
prefix: 'lib',
prefix: 'oms-feature',
style: 'kebab-case',
},
],

View File

@@ -1,8 +1,8 @@
export default {
displayName: 'feature-return-containers',
displayName: 'oms-feature-return-details',
preset: '../../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../../coverage/libs/feature/return/containers',
coverageDirectory: '../../../../coverage/libs/oms/feature/return-details',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',

View File

@@ -1,8 +1,8 @@
{
"name": "feature-return-pages",
"name": "oms-feature-return-details",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/feature/return/pages/src",
"prefix": "lib",
"sourceRoot": "libs/oms/feature/return-details/src",
"prefix": "oms-feature",
"projectType": "library",
"tags": [],
"targets": {
@@ -10,7 +10,7 @@
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/feature/return/pages/jest.config.ts"
"jestConfig": "libs/oms/feature/return-details/jest.config.ts"
}
},
"lint": {

View File

@@ -12,7 +12,3 @@
</ui-item-row-data-value>
</ui-item-row-data-row>
</ui-item-row-data>
<!-- <button class="mt-3" uiTextButton type="button" color="strong" size="small">
<ng-icon name="isaActionPlus" size="1rem"></ng-icon>
<span>Bestelldetails anzeigen</span>
</button> -->

View File

@@ -2,13 +2,11 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
import { Buyer, Receipt } from '@isa/oms/data-access';
import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation';
import { ItemRowDataImports } from '@isa/ui/item-rows';
@Component({
selector: 'lib-return-details-data',
selector: 'oms-feature-return-details-data',
templateUrl: './return-details-data.component.html',
styleUrls: ['./return-details-data.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ItemRowDataImports, ReceiptTypeTranslationPipe],
})
export class ReturnDetailsDataComponent {

View File

@@ -5,7 +5,7 @@ import { InfoButtonComponent } from '@isa/ui/buttons';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
@Component({
selector: 'lib-return-details-header',
selector: 'oms-feature-return-details-header',
templateUrl: './return-details-header.component.html',
styleUrls: ['./return-details-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -5,7 +5,7 @@ import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation';
import { ItemRowDataImports } from '@isa/ui/item-rows';
@Component({
selector: 'lib-return-details-order-group-data',
selector: 'oms-feature-return-details-order-group-data',
templateUrl: './return-details-order-group-data.component.html',
styleUrls: ['./return-details-order-group-data.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -9,7 +9,7 @@ import { ItemRowComponent } from '@isa/ui/item-rows';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
@Component({
selector: 'lib-return-details-order-group-item',
selector: 'oms-feature-return-details-order-group-item',
templateUrl: './return-details-order-group-item.component.html',
styleUrls: ['./return-details-order-group-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, computed, input, model, output } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, input, model } from '@angular/core';
import { ReceiptItem } from '@isa/oms/data-access';
import { TextButtonComponent } from '@isa/ui/buttons';
import { ToolbarComponent } from '@isa/ui/toolbar';
@Component({
selector: 'lib-return-details-order-group',
selector: 'oms-feature-return-details-order-group',
templateUrl: './return-details-order-group.component.html',
styleUrls: ['./return-details-order-group.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -12,10 +12,12 @@
</button>
@if (receipt) {
<lib-return-details-header [buyer]="receipt.buyer"></lib-return-details-header>
<oms-feature-return-details-header [buyer]="receipt.buyer"></oms-feature-return-details-header>
@if (showMore()) {
<lib-return-details-order-group-data [receipt]="receipt"></lib-return-details-order-group-data>
<oms-feature-return-details-order-group-data
[receipt]="receipt"
></oms-feature-return-details-order-group-data>
<button
class="-ml-3"
uiTextButton
@@ -28,7 +30,7 @@
Weniger anzeigen
</button>
} @else {
<lib-return-details-data [receipt]="receipt"></lib-return-details-data>
<oms-feature-return-details-data [receipt]="receipt"></oms-feature-return-details-data>
<button
class="-ml-3"
uiTextButton
@@ -42,16 +44,16 @@
</button>
}
<div></div>
<lib-return-details-order-group
<oms-feature-return-details-order-group
[items]="receiptItems()"
[(selectedItems)]="selectedItems"
></lib-return-details-order-group>
></oms-feature-return-details-order-group>
@for (item of receipt.items; track item.id; let last = $last) {
<lib-return-details-order-group-item
<oms-feature-return-details-order-group-item
class="border-b border-solid border-isa-neutral-300 last:border-none"
[item]="item.data"
(selectedChange)="selectItemById(item.id, $event)"
[selected]="selectedItemIds().includes(item.id)"
></lib-return-details-order-group-item>
></oms-feature-return-details-order-group-item>
}
}

View File

@@ -9,13 +9,6 @@ import {
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import {
ReturnDetailsDataComponent,
ReturnDetailsHeaderComponent,
ReturnDetailsOrderGroupComponent,
ReturnDetailsOrderGroupDataComponent,
ReturnDetailsOrderGroupItemComponent,
} from '@feature/return/containers';
import { z } from 'zod';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
@@ -23,13 +16,17 @@ import { isaActionPlus, isaActionMinus } from '@isa/icons';
import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
import { ReceiptItem, ReturnDetailsStore, ReturnProcessStore } from '@isa/oms/data-access';
import { injectActivatedProcessId } from '@isa/core/process';
import { ReturnDetailsDataComponent } from './return-details-data/return-details-data.component';
import { ReturnDetailsHeaderComponent } from './return-details-header/return-details-header.component';
import { ReturnDetailsOrderGroupComponent } from './return-details-order-group/return-details-order-group.component';
import { ReturnDetailsOrderGroupItemComponent } from './return-details-order-group-item/return-details-order-group-item.component';
import { ReturnDetailsOrderGroupDataComponent } from './return-details-order-group-data/return-details-order-group-data.component';
@Component({
selector: 'lib-return-details-page',
templateUrl: './details-page.component.html',
styleUrls: ['./details-page.component.scss'],
selector: 'oms-feature-return-details',
templateUrl: './return-details.component.html',
styleUrls: ['./return-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
NgIconComponent,
TextButtonComponent,
@@ -42,7 +39,7 @@ import { injectActivatedProcessId } from '@isa/core/process';
],
providers: [provideIcons({ isaActionPlus, isaActionMinus })],
})
export class DetailsPageComponent {
export class ReturnDetailsComponent {
private processId = injectActivatedProcessId();
private _router = inject(Router);

View File

@@ -0,0 +1,4 @@
import { Routes } from '@angular/router';
import { ReturnDetailsComponent } from './return-details.component';
export const routes: Routes = [{ path: ':receiptId', component: ReturnDetailsComponent }];

View File

@@ -7,6 +7,11 @@
"inlineSources": true,
"types": []
},
"exclude": ["src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts"],
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
"jest.config.ts",
"src/**/*.test.ts"
],
"include": ["src/**/*.ts"]
}

View File

@@ -7,5 +7,10 @@
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@@ -17,13 +17,13 @@
<ng-icon name="isaActionSort"></ng-icon>
</ui-icon-button>
} @else {
<lib-return-order-by-list></lib-return-order-by-list>
<filter-order-by-toolbar></filter-order-by-toolbar>
}
</div>
</div>
@if (isMobileDevice() && toggleOrderByToolbar()) {
<lib-return-order-by-list class="w-full"></lib-return-order-by-list>
<filter-order-by-toolbar class="w-full"></filter-order-by-toolbar>
}
<span class="text-isa-neutral-900 isa-text-body-2-regular self-start">

View File

@@ -9,7 +9,6 @@ import {
untracked,
viewChildren,
} from '@angular/core';
import { ReturnOrderByListComponent } from '@feature/return/containers';
import { injectActivatedProcessId } from '@isa/core/process';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
@@ -17,7 +16,9 @@ import {
FilterMenuButtonComponent,
FilterService,
SearchBarInputComponent,
OrderByToolbarComponent,
} from '@isa/shared/filter';
import { IconButtonComponent } from '@isa/ui/buttons';
import { EmptyStateComponent } from '@isa/ui/empty-state';
import { restoreScrollPosition } from '@isa/core/scroll-position';
@@ -40,7 +41,7 @@ type EmptyState = {
imports: [
RouterLink,
ReturnSearchResultItemComponent,
ReturnOrderByListComponent,
OrderByToolbarComponent,
IconButtonComponent,
SearchBarInputComponent,
EmptyStateComponent,

View File

@@ -14,6 +14,10 @@ export const routes: Routes = [
{ path: 'receipts', component: ReturnSearchResultComponent },
],
},
{
path: 'receipt',
loadChildren: () => import('@isa/oms/feature/return-details').then((feat) => feat.routes),
},
{
path: 'process',
loadChildren: () => import('@isa/oms/feature/return-process').then((feat) => feat.routes),

View File

@@ -4,3 +4,4 @@ export * from './lib/types';
export * from './lib/actions';
export * from './lib/menus/filter-menu';
export * from './lib/menus/input-menu';
export * from './lib/order-by';

View File

@@ -25,6 +25,8 @@ export class FilterService {
inputs = this.#state.inputs;
orderBy = this.#state.orderBy;
setInputTextValue(key: string, value: string | undefined, options?: { commit: boolean }): void {
const inputs = this.#state.inputs().map((input) => {
if (input.key !== key) {

View File

@@ -10,6 +10,7 @@ import {
FilterGroup,
FilterGroupSchema,
FilterInput,
OrderBySchema,
TextFilterInput,
TextFilterInputSchema,
} from './schemas';
@@ -18,6 +19,7 @@ export function mapToFilter(settings: QuerySettingsDTO): Filter {
const filter: Filter = {
groups: [],
inputs: [],
orderBy: [],
};
const groups = [...settings.filter, ...settings.input];
@@ -30,6 +32,20 @@ export function mapToFilter(settings: QuerySettingsDTO): Filter {
}
}
if (settings.orderBy) {
const bys = new Set<string>();
for (const orderBy of settings.orderBy) {
if (orderBy.by && bys.has(orderBy.by)) {
continue;
}
if (orderBy.by) {
filter.orderBy.push(OrderBySchema.parse(orderBy));
bys.add(orderBy.by);
}
}
}
return filter;
}

View File

@@ -52,10 +52,21 @@ export const FilterInputSchema = z.union([
DateRangeFilterInputSchema,
]);
export const OrderByDirectionSchema = z.enum(['asc', 'desc']);
export const OrderBySchema = z
.object({
by: z.string(),
label: z.string(),
dir: OrderByDirectionSchema.optional(),
})
.describe('OrderBy');
export const FilterSchema = z
.object({
groups: z.array(FilterGroupSchema),
inputs: z.array(FilterInputSchema),
orderBy: z.array(OrderBySchema),
})
.describe('Filter');
@@ -72,3 +83,7 @@ export type FilterInput = z.infer<typeof FilterInputSchema>;
export type CheckboxFilterInputOption = z.infer<typeof CheckboxFilterInputOptionSchema>;
export type DateRangeFilterInput = z.infer<typeof DateRangeFilterInputSchema>;
export type OrderByDirection = z.infer<typeof OrderByDirectionSchema>;
export type OrderBy = z.infer<typeof OrderBySchema>;

View File

@@ -0,0 +1 @@
export * from './order-by-toolbar.component';

View File

@@ -0,0 +1,7 @@
<ui-toolbar>
<span class="text-isa-neutral-600 isa-text-body-2-regular">Sortieren</span>
<div class="flex-grow"></div>
@for (orderBy of orderByOptions(); track orderBy.by) {
<button uiTextButton>{{ orderBy.label }}</button>
}
</ui-toolbar>

View File

@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { TextButtonComponent } from '@isa/ui/buttons';
import { ToolbarComponent } from '@isa/ui/toolbar';
import { FilterService } from '../core';
@Component({
selector: 'filter-order-by-toolbar',
templateUrl: './order-by-toolbar.component.html',
styleUrls: ['./order-by-toolbar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ToolbarComponent, TextButtonComponent],
})
export class OrderByToolbarComponent {
#filter = inject(FilterService);
orderByOptions = this.#filter.orderBy;
}

View File

@@ -18,10 +18,6 @@
"@core/config": ["libs/core/config/src/index.ts"],
"@domain/*": ["apps/isa-app/src/domain/*/index.ts"],
"@external/*": ["apps/isa-app/src/external/*/index.ts"],
"@feature/return/containers": [
"libs/feature/return/containers/src/index.ts"
],
"@feature/return/pages": ["libs/feature/return/pages/src/index.ts"],
"@generated/swagger/availability-api": [
"generated/swagger/availability-api/src/index.ts"
],
@@ -53,6 +49,9 @@
"@isa/core/storage": ["libs/core/storage/src/index.ts"],
"@isa/icons": ["libs/icons/src/index.ts"],
"@isa/oms/data-access": ["libs/oms/data-access/src/index.ts"],
"@isa/oms/feature/return-details": [
"libs/oms/feature/return-details/src/index.ts"
],
"@isa/oms/feature/return-process": [
"libs/oms/feature/return-process/src/index.ts"
],