From db7da0699e1d108844601e33d71bb8e52088fba5 Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Thu, 20 Mar 2025 21:25:20 +0100 Subject: [PATCH] feat: add data access and feature libraries for return process, including models, schemas, and routes --- .vscode/settings.json | 7 +- libs/catalogue/data-access/README.md | 7 ++ libs/catalogue/data-access/eslint.config.mjs | 34 +++++++ libs/catalogue/data-access/jest.config.ts | 21 +++++ libs/catalogue/data-access/project.json | 20 ++++ libs/catalogue/data-access/src/index.ts | 2 + .../src/lib/catalouge-search.service.ts | 21 +++++ libs/catalogue/data-access/src/lib/index.ts | 2 + .../data-access/src/lib/models/index.ts | 2 + .../data-access/src/lib/models/item.ts | 7 ++ .../data-access/src/lib/models/product.ts | 7 ++ libs/catalogue/data-access/src/test-setup.ts | 6 ++ libs/catalogue/data-access/tsconfig.json | 28 ++++++ libs/catalogue/data-access/tsconfig.lib.json | 17 ++++ libs/catalogue/data-access/tsconfig.spec.json | 16 ++++ libs/common/result/src/index.ts | 1 + .../result/src/lib/entity-container.schema.ts | 6 +- .../common/result/src/lib/entity-cotnainer.ts | 7 ++ libs/core/storage/src/index.ts | 1 + .../storage/src/lib/signal-store-feature.ts | 26 ++++++ libs/core/storage/src/lib/storage.ts | 6 +- .../return-details-data.component.html | 2 +- .../return-details-data.component.ts | 2 +- ...turn-details-order-group-data.component.ts | 6 +- ...rn-details-order-group-item.component.html | 2 +- ...turn-details-order-group-item.component.ts | 25 ++--- .../lib/details/details-page.component.html | 1 + .../src/lib/details/details-page.component.ts | 53 ++++++++++- libs/feature/return/pages/src/lib/routes.ts | 4 + libs/feature/return/services/src/index.ts | 2 - .../src/lib/schemas/receipt-item.schema.ts | 4 +- .../src/lib/schemas/receipt.schema.ts | 6 +- libs/icons/src/index.ts | 3 +- libs/icons/src/lib/icon-groups.ts | 16 ++++ libs/icons/src/lib/icons.ts | 38 ++++++-- libs/oms/data-access/README.md | 7 ++ libs/oms/data-access/eslint.config.mjs | 34 +++++++ libs/oms/data-access/jest.config.ts | 21 +++++ libs/oms/data-access/project.json | 20 ++++ libs/oms/data-access/src/index.ts | 6 ++ libs/oms/data-access/src/lib/index.ts | 6 ++ libs/oms/data-access/src/lib/models/buyer.ts | 5 + libs/oms/data-access/src/lib/models/index.ts | 8 ++ .../oms/data-access/src/lib/models/product.ts | 7 ++ .../data-access/src/lib/models/quantity.ts | 5 + .../src/lib/models/receipt-item.ts | 9 ++ .../oms/data-access/src/lib/models/receipt.ts | 10 ++ .../src/lib/models/return-process-answers.ts | 3 + .../src/lib/models/return-process-question.ts | 31 +++++++ .../src/lib/models/return-process.ts | 7 ++ .../src/lib/return-details.service.ts | 15 +-- .../src/lib/return-details.store.ts | 4 +- .../src/lib/return-process.service.ts | 46 ++++++++++ .../src/lib/return-process.store.ts | 92 +++++++++++++++++++ .../src/lib/schemas/fetch-return-details.ts | 7 ++ libs/oms/data-access/src/lib/schemas/index.ts | 1 + libs/oms/data-access/src/test-setup.ts | 6 ++ libs/oms/data-access/tsconfig.json | 28 ++++++ libs/oms/data-access/tsconfig.lib.json | 17 ++++ libs/oms/data-access/tsconfig.spec.json | 16 ++++ libs/oms/feature/return-process/README.md | 7 ++ .../feature/return-process/eslint.config.mjs | 34 +++++++ .../oms/feature/return-process/jest.config.ts | 21 +++++ libs/oms/feature/return-process/project.json | 20 ++++ libs/oms/feature/return-process/src/index.ts | 1 + .../lib/return-process-feature.component.html | 18 ++++ .../lib/return-process-feature.component.scss | 14 +++ .../lib/return-process-feature.component.ts | 31 +++++++ .../return-process-item.component.html | 28 ++++++ .../return-process-item.component.scss | 49 ++++++++++ .../return-process-item.component.ts | 26 ++++++ ...rn-process-product-question.component.html | 35 +++++++ ...rn-process-product-question.component.scss | 3 + ...turn-process-product-question.component.ts | 49 ++++++++++ .../return-process-questions.component.html | 21 +++++ .../return-process-questions.component.scss | 12 +++ .../return-process-questions.component.ts | 79 ++++++++++++++++ ...urn-process-select-question.component.html | 10 ++ ...urn-process-select-question.component.scss | 7 ++ ...eturn-process-select-question.component.ts | 41 +++++++++ .../feature/return-process/src/lib/routes.ts | 4 + .../feature/return-process/src/test-setup.ts | 6 ++ libs/oms/feature/return-process/tsconfig.json | 28 ++++++ .../feature/return-process/tsconfig.lib.json | 17 ++++ .../feature/return-process/tsconfig.spec.json | 16 ++++ libs/ui/input-controls/src/index.ts | 2 + .../src/lib/chips/chip-option.component.scss | 18 ++++ .../src/lib/chips/chip-option.component.ts | 37 ++++++++ .../src/lib/chips/chips.component.scss | 5 + .../src/lib/chips/chips.component.ts | 81 ++++++++++++++++ tsconfig.base.json | 5 + 91 files changed, 1477 insertions(+), 67 deletions(-) create mode 100644 libs/catalogue/data-access/README.md create mode 100644 libs/catalogue/data-access/eslint.config.mjs create mode 100644 libs/catalogue/data-access/jest.config.ts create mode 100644 libs/catalogue/data-access/project.json create mode 100644 libs/catalogue/data-access/src/index.ts create mode 100644 libs/catalogue/data-access/src/lib/catalouge-search.service.ts create mode 100644 libs/catalogue/data-access/src/lib/index.ts create mode 100644 libs/catalogue/data-access/src/lib/models/index.ts create mode 100644 libs/catalogue/data-access/src/lib/models/item.ts create mode 100644 libs/catalogue/data-access/src/lib/models/product.ts create mode 100644 libs/catalogue/data-access/src/test-setup.ts create mode 100644 libs/catalogue/data-access/tsconfig.json create mode 100644 libs/catalogue/data-access/tsconfig.lib.json create mode 100644 libs/catalogue/data-access/tsconfig.spec.json create mode 100644 libs/common/result/src/lib/entity-cotnainer.ts create mode 100644 libs/core/storage/src/lib/signal-store-feature.ts create mode 100644 libs/icons/src/lib/icon-groups.ts create mode 100644 libs/oms/data-access/README.md create mode 100644 libs/oms/data-access/eslint.config.mjs create mode 100644 libs/oms/data-access/jest.config.ts create mode 100644 libs/oms/data-access/project.json create mode 100644 libs/oms/data-access/src/index.ts create mode 100644 libs/oms/data-access/src/lib/index.ts create mode 100644 libs/oms/data-access/src/lib/models/buyer.ts create mode 100644 libs/oms/data-access/src/lib/models/index.ts create mode 100644 libs/oms/data-access/src/lib/models/product.ts create mode 100644 libs/oms/data-access/src/lib/models/quantity.ts create mode 100644 libs/oms/data-access/src/lib/models/receipt-item.ts create mode 100644 libs/oms/data-access/src/lib/models/receipt.ts create mode 100644 libs/oms/data-access/src/lib/models/return-process-answers.ts create mode 100644 libs/oms/data-access/src/lib/models/return-process-question.ts create mode 100644 libs/oms/data-access/src/lib/models/return-process.ts rename libs/{feature/return/services => oms/data-access}/src/lib/return-details.service.ts (62%) rename libs/{feature/return/services => oms/data-access}/src/lib/return-details.store.ts (95%) create mode 100644 libs/oms/data-access/src/lib/return-process.service.ts create mode 100644 libs/oms/data-access/src/lib/return-process.store.ts create mode 100644 libs/oms/data-access/src/lib/schemas/fetch-return-details.ts create mode 100644 libs/oms/data-access/src/lib/schemas/index.ts create mode 100644 libs/oms/data-access/src/test-setup.ts create mode 100644 libs/oms/data-access/tsconfig.json create mode 100644 libs/oms/data-access/tsconfig.lib.json create mode 100644 libs/oms/data-access/tsconfig.spec.json create mode 100644 libs/oms/feature/return-process/README.md create mode 100644 libs/oms/feature/return-process/eslint.config.mjs create mode 100644 libs/oms/feature/return-process/jest.config.ts create mode 100644 libs/oms/feature/return-process/project.json create mode 100644 libs/oms/feature/return-process/src/index.ts create mode 100644 libs/oms/feature/return-process/src/lib/return-process-feature.component.html create mode 100644 libs/oms/feature/return-process/src/lib/return-process-feature.component.scss create mode 100644 libs/oms/feature/return-process/src/lib/return-process-feature.component.ts create mode 100644 libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.html create mode 100644 libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.scss create mode 100644 libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.ts create mode 100644 libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.html create mode 100644 libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.scss create mode 100644 libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.ts create mode 100644 libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.html create mode 100644 libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.scss create mode 100644 libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.ts create mode 100644 libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.html create mode 100644 libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.scss create mode 100644 libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.ts create mode 100644 libs/oms/feature/return-process/src/lib/routes.ts create mode 100644 libs/oms/feature/return-process/src/test-setup.ts create mode 100644 libs/oms/feature/return-process/tsconfig.json create mode 100644 libs/oms/feature/return-process/tsconfig.lib.json create mode 100644 libs/oms/feature/return-process/tsconfig.spec.json create mode 100644 libs/ui/input-controls/src/lib/chips/chip-option.component.scss create mode 100644 libs/ui/input-controls/src/lib/chips/chip-option.component.ts create mode 100644 libs/ui/input-controls/src/lib/chips/chips.component.scss create mode 100644 libs/ui/input-controls/src/lib/chips/chips.component.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index c22ea5550..9ebb3f8df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,10 @@ }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "exportall.config.folderListener": [ + "/libs/oms/data-access/src/lib/models", + "/libs/oms/data-access/src/lib/schemas", + "/libs/catalogue/data-access/src/lib/models" + ] } diff --git a/libs/catalogue/data-access/README.md b/libs/catalogue/data-access/README.md new file mode 100644 index 000000000..0c93c16ea --- /dev/null +++ b/libs/catalogue/data-access/README.md @@ -0,0 +1,7 @@ +# catalogue-data-access + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test catalogue-data-access` to execute the unit tests. diff --git a/libs/catalogue/data-access/eslint.config.mjs b/libs/catalogue/data-access/eslint.config.mjs new file mode 100644 index 000000000..3bf3b5ae0 --- /dev/null +++ b/libs/catalogue/data-access/eslint.config.mjs @@ -0,0 +1,34 @@ +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: {}, + }, +]; diff --git a/libs/catalogue/data-access/jest.config.ts b/libs/catalogue/data-access/jest.config.ts new file mode 100644 index 000000000..63b3e8034 --- /dev/null +++ b/libs/catalogue/data-access/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'catalogue-data-access', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/catalogue/data-access', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/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', + ], +}; diff --git a/libs/catalogue/data-access/project.json b/libs/catalogue/data-access/project.json new file mode 100644 index 000000000..a2abc5c7a --- /dev/null +++ b/libs/catalogue/data-access/project.json @@ -0,0 +1,20 @@ +{ + "name": "catalogue-data-access", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/catalogue/data-access/src", + "prefix": "lib", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/catalogue/data-access/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/catalogue/data-access/src/index.ts b/libs/catalogue/data-access/src/index.ts new file mode 100644 index 000000000..d5c611885 --- /dev/null +++ b/libs/catalogue/data-access/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/catalouge-search.service'; +export * from './lib/models'; diff --git a/libs/catalogue/data-access/src/lib/catalouge-search.service.ts b/libs/catalogue/data-access/src/lib/catalouge-search.service.ts new file mode 100644 index 000000000..b8292f9c3 --- /dev/null +++ b/libs/catalogue/data-access/src/lib/catalouge-search.service.ts @@ -0,0 +1,21 @@ +import { inject, Injectable } from '@angular/core'; +import { SearchService } from '@generated/swagger/cat-search-api'; +import { map, Observable } from 'rxjs'; +import { Item } from './models'; + +@Injectable({ providedIn: 'root' }) +export class CatalougeSearchService { + #searchService = inject(SearchService); + + searchByEans(...ean: string[]): Observable { + return this.#searchService.SearchByEAN(ean).pipe( + map((res) => { + if (res.error) { + throw new Error(res.message); + } + + return res.result as Item[]; + }), + ); + } +} diff --git a/libs/catalogue/data-access/src/lib/index.ts b/libs/catalogue/data-access/src/lib/index.ts new file mode 100644 index 000000000..019f684af --- /dev/null +++ b/libs/catalogue/data-access/src/lib/index.ts @@ -0,0 +1,2 @@ +export * from './catalouge-search.service'; +export * from './models'; diff --git a/libs/catalogue/data-access/src/lib/models/index.ts b/libs/catalogue/data-access/src/lib/models/index.ts new file mode 100644 index 000000000..3d3352801 --- /dev/null +++ b/libs/catalogue/data-access/src/lib/models/index.ts @@ -0,0 +1,2 @@ +export * from './item'; +export * from './product'; diff --git a/libs/catalogue/data-access/src/lib/models/item.ts b/libs/catalogue/data-access/src/lib/models/item.ts new file mode 100644 index 000000000..5d98743ab --- /dev/null +++ b/libs/catalogue/data-access/src/lib/models/item.ts @@ -0,0 +1,7 @@ +import { ItemDTO } from '@generated/swagger/cat-search-api'; +import { Product } from './product'; + +export interface Item extends ItemDTO { + id: number; + product: Product; +} diff --git a/libs/catalogue/data-access/src/lib/models/product.ts b/libs/catalogue/data-access/src/lib/models/product.ts new file mode 100644 index 000000000..d79c13614 --- /dev/null +++ b/libs/catalogue/data-access/src/lib/models/product.ts @@ -0,0 +1,7 @@ +import { ProductDTO } from '@generated/swagger/cat-search-api'; + +export interface Product extends ProductDTO { + ean: string; + format: string; + formatDetails: string; +} diff --git a/libs/catalogue/data-access/src/test-setup.ts b/libs/catalogue/data-access/src/test-setup.ts new file mode 100644 index 000000000..ea414013f --- /dev/null +++ b/libs/catalogue/data-access/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/catalogue/data-access/tsconfig.json b/libs/catalogue/data-access/tsconfig.json new file mode 100644 index 000000000..fde35eab0 --- /dev/null +++ b/libs/catalogue/data-access/tsconfig.json @@ -0,0 +1,28 @@ +{ + "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 + } +} diff --git a/libs/catalogue/data-access/tsconfig.lib.json b/libs/catalogue/data-access/tsconfig.lib.json new file mode 100644 index 000000000..9b49be758 --- /dev/null +++ b/libs/catalogue/data-access/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "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"] +} diff --git a/libs/catalogue/data-access/tsconfig.spec.json b/libs/catalogue/data-access/tsconfig.spec.json new file mode 100644 index 000000000..f858ef78c --- /dev/null +++ b/libs/catalogue/data-access/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "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" + ] +} diff --git a/libs/common/result/src/index.ts b/libs/common/result/src/index.ts index 42335a3a9..25fd059b1 100644 --- a/libs/common/result/src/index.ts +++ b/libs/common/result/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/response-args.schema'; +export * from './lib/entity-cotnainer'; export * from './lib/entity-container.schema'; export * from './lib/result'; diff --git a/libs/common/result/src/lib/entity-container.schema.ts b/libs/common/result/src/lib/entity-container.schema.ts index ceb2d995c..0dd51fcd5 100644 --- a/libs/common/result/src/lib/entity-container.schema.ts +++ b/libs/common/result/src/lib/entity-container.schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -const EntityContainerSchema = z.object({ +const _EntityContainerSchema = z.object({ id: z.number(), uId: z.string().optional(), displayLabel: z.string().optional(), @@ -8,8 +8,8 @@ const EntityContainerSchema = z.object({ selected: z.boolean().optional(), }); -export function EntityContainer(data: z.ZodType) { - return EntityContainerSchema.extend({ +export function EntityContainerSchema(data: z.ZodType) { + return _EntityContainerSchema.extend({ data: data.optional(), }); } diff --git a/libs/common/result/src/lib/entity-cotnainer.ts b/libs/common/result/src/lib/entity-cotnainer.ts new file mode 100644 index 000000000..a5840cd58 --- /dev/null +++ b/libs/common/result/src/lib/entity-cotnainer.ts @@ -0,0 +1,7 @@ +export interface EntityContainer { + id: number; + displayName?: string; + data?: T; + enabled?: boolean; + selected?: boolean; +} diff --git a/libs/core/storage/src/index.ts b/libs/core/storage/src/index.ts index d26496d2d..2993f92fb 100644 --- a/libs/core/storage/src/index.ts +++ b/libs/core/storage/src/index.ts @@ -1,5 +1,6 @@ export * from './lib/idb.storage-provider'; export * from './lib/local.storage-provider'; export * from './lib/session.storage-provider'; +export * from './lib/signal-store-feature'; export * from './lib/storage-provider'; export * from './lib/storage'; diff --git a/libs/core/storage/src/lib/signal-store-feature.ts b/libs/core/storage/src/lib/signal-store-feature.ts new file mode 100644 index 000000000..b29729c6d --- /dev/null +++ b/libs/core/storage/src/lib/signal-store-feature.ts @@ -0,0 +1,26 @@ +import { Type } from '@angular/core'; +import { getState, patchState, signalStoreFeature, withHooks, withMethods } from '@ngrx/signals'; +import { StorageProvider } from './storage-provider'; +import { injectStorage } from './storage'; + +export function withStorage(storageKey: string, storageProvider: Type) { + return signalStoreFeature( + withMethods((store, storage = injectStorage(storageProvider)) => ({ + storeState: () => { + const state = getState(store); + storage.set(storageKey, state); + }, + restoreState: async () => { + const data = await storage.get(storageKey); + if (data) { + patchState(store, data); + } + }, + })), + withHooks((store) => ({ + onInit() { + store.restoreState(); + }, + })), + ); +} diff --git a/libs/core/storage/src/lib/storage.ts b/libs/core/storage/src/lib/storage.ts index d3e915833..ed8d2faf5 100644 --- a/libs/core/storage/src/lib/storage.ts +++ b/libs/core/storage/src/lib/storage.ts @@ -18,7 +18,7 @@ export class Storage { return this.storageProvider.set(key, value); } - async get(token: string | object, schema?: z.ZodType): Promise { + async get(token: string | object, schema?: z.ZodType): Promise { let key: string; if (typeof token === 'string') { @@ -31,13 +31,13 @@ export class Storage { if (schema) { return await schema.parse(data); } - return undefined; + return data; } } const storageMap = new WeakMap, Storage>(); -export function storage(storageProvider: Type): Storage { +export function injectStorage(storageProvider: Type): Storage { if (storageMap.has(storageProvider)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return storageMap.get(storageProvider)!; diff --git a/libs/feature/return/containers/src/lib/return-details-data/return-details-data.component.html b/libs/feature/return/containers/src/lib/return-details-data/return-details-data.component.html index 4990e2dd9..5756c061f 100644 --- a/libs/feature/return/containers/src/lib/return-details-data/return-details-data.component.html +++ b/libs/feature/return/containers/src/lib/return-details-data/return-details-data.component.html @@ -2,7 +2,7 @@ Email: - {{ communicationDetails().email }} + {{ communicationDetails().email }} diff --git a/libs/feature/return/containers/src/lib/return-details-data/return-details-data.component.ts b/libs/feature/return/containers/src/lib/return-details-data/return-details-data.component.ts index 3912b0a03..b5ffac8af 100644 --- a/libs/feature/return/containers/src/lib/return-details-data/return-details-data.component.ts +++ b/libs/feature/return/containers/src/lib/return-details-data/return-details-data.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { Buyer, Receipt } from '@feature/return/services'; +import { Buyer, Receipt } from '@isa/oms/data-access'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; import { ItemRowDataImports } from '@isa/ui/item-rows'; diff --git a/libs/feature/return/containers/src/lib/return-details-order-group-data/return-details-order-group-data.component.ts b/libs/feature/return/containers/src/lib/return-details-order-group-data/return-details-order-group-data.component.ts index f0af44eb5..ff0905b74 100644 --- a/libs/feature/return/containers/src/lib/return-details-order-group-data/return-details-order-group-data.component.ts +++ b/libs/feature/return/containers/src/lib/return-details-order-group-data/return-details-order-group-data.component.ts @@ -1,6 +1,6 @@ import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { Receipt } from '@feature/return/services'; +import { Receipt } from '@isa/oms/data-access'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; import { ItemRowDataImports } from '@isa/ui/item-rows'; @@ -22,8 +22,6 @@ export class ReturnDetailsOrderGroupDataComponent { shippingName = computed(() => { const receipt = this.receipt(); - return [receipt.shipping?.person?.firstName, receipt.shipping?.person?.lastName] - .filter(Boolean) - .join(' '); + return [receipt.shipping?.firstName, receipt.shipping?.lastName].filter(Boolean).join(' '); }); } diff --git a/libs/feature/return/containers/src/lib/return-details-order-group-item/return-details-order-group-item.component.html b/libs/feature/return/containers/src/lib/return-details-order-group-item/return-details-order-group-item.component.html index 3cae30c40..75bdfdf5d 100644 --- a/libs/feature/return/containers/src/lib/return-details-order-group-item/return-details-order-group-item.component.html +++ b/libs/feature/return/containers/src/lib/return-details-order-group-item/return-details-order-group-item.component.html @@ -24,7 +24,7 @@
- + {{ item().product.formatDetail }} diff --git a/libs/feature/return/containers/src/lib/return-details-order-group-item/return-details-order-group-item.component.ts b/libs/feature/return/containers/src/lib/return-details-order-group-item/return-details-order-group-item.component.ts index 693f20fe3..fa4151f49 100644 --- a/libs/feature/return/containers/src/lib/return-details-order-group-item/return-details-order-group-item.component.ts +++ b/libs/feature/return/containers/src/lib/return-details-order-group-item/return-details-order-group-item.component.ts @@ -1,21 +1,13 @@ -import { CurrencyPipe, DatePipe } from '@angular/common'; +import { CurrencyPipe, DatePipe, LowerCasePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, input, model } from '@angular/core'; -import { ReceiptItem } from '@feature/return/services'; -import { - isaArtikelTaschenbuch, - isaArtikelCd, - isaArtikelEbook, - isaArtikelKartoniert, -} from '@isa/icons'; + +import { ProductFormatIconGroup } from '@isa/icons'; +import { ReceiptItem } from '@isa/oms/data-access'; import { ProductImageDirective } from '@isa/shared/product-image'; import { CheckboxComponent } from '@isa/ui/input-controls'; import { ItemRowComponent } from '@isa/ui/item-rows'; import { NgIconComponent, provideIcons } from '@ng-icons/core'; -const FORMAT_ICON_MAP: Record = { - HC: 'isaArtikelTaschenbuch', -}; - @Component({ selector: 'lib-return-details-order-group-item', templateUrl: './return-details-order-group-item.component.html', @@ -29,17 +21,12 @@ const FORMAT_ICON_MAP: Record = { NgIconComponent, DatePipe, CurrencyPipe, + LowerCasePipe, ], - providers: [ - provideIcons({ isaArtikelTaschenbuch, isaArtikelCd, isaArtikelEbook, isaArtikelKartoniert }), - ], + providers: [provideIcons(ProductFormatIconGroup)], }) export class ReturnDetailsOrderGroupItemComponent { item = input.required(); selected = model(false); - - formatIcons = computed(() => { - return FORMAT_ICON_MAP[this.item().product.format] || 'isaArtikelTaschenbuch'; - }); } diff --git a/libs/feature/return/pages/src/lib/details/details-page.component.html b/libs/feature/return/pages/src/lib/details/details-page.component.html index acef0e369..51082f42f 100644 --- a/libs/feature/return/pages/src/lib/details/details-page.component.html +++ b/libs/feature/return/pages/src/lib/details/details-page.component.html @@ -6,6 +6,7 @@ color="brand" size="large" [disabled]="selectedItems().length === 0" + (click)="startProcess()" > Rückgabe starten diff --git a/libs/feature/return/pages/src/lib/details/details-page.component.ts b/libs/feature/return/pages/src/lib/details/details-page.component.ts index a9e442919..f140d034a 100644 --- a/libs/feature/return/pages/src/lib/details/details-page.component.ts +++ b/libs/feature/return/pages/src/lib/details/details-page.component.ts @@ -7,7 +7,7 @@ import { signal, untracked, } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { toSignal } from '@angular/core/rxjs-interop'; import { ReturnDetailsDataComponent, @@ -17,10 +17,12 @@ import { ReturnDetailsOrderGroupItemComponent, } from '@feature/return/containers'; import { z } from 'zod'; -import { ReturnDetailsStore } from '@feature/return/services'; + import { NgIconComponent, provideIcons } from '@ng-icons/core'; 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'; @Component({ selector: 'lib-return-details-page', @@ -41,7 +43,13 @@ import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons'; providers: [provideIcons({ isaActionPlus, isaActionMinus })], }) export class DetailsPageComponent { - private _params = toSignal(inject(ActivatedRoute).params); + private processId = injectActivatedProcessId(); + + private _router = inject(Router); + + private _activatedRoute = inject(ActivatedRoute); + + params = toSignal(this._activatedRoute.params); // DUMMY Implementierung selectedItems = signal([]); @@ -50,8 +58,10 @@ export class DetailsPageComponent { #returnDetailsStore = inject(ReturnDetailsStore); + #returnProcessStore = inject(ReturnProcessStore); + itemId = computed(() => { - const params = this._params(); + const params = this.params(); if (params) { return z.coerce.number().parse(params['receiptId']); } @@ -88,11 +98,44 @@ export class DetailsPageComponent { if (selected) { const receiptResult = this.receiptResult(); if (receiptResult) { - const allIds = receiptResult.data?.items.map((item) => item.id) || []; + const allIds = receiptResult.data?.items?.map((item) => item.id) || []; this.selectedItems.set(allIds); } } else { this.selectedItems.set([]); } } + + startProcess() { + const processId = this.processId(); + const selectedItems = this.selectedItems(); + if (!selectedItems.length || !processId) { + return; + } + + const data = this.receiptResult().data; + if (!data) { + return; + } + + const items = selectedItems.reduce((acc, id) => { + const item = data.items.find((item) => item.id === id); + if (item?.data) { + acc.push(item.data); + } + return acc; + }, [] as ReceiptItem[]); + + this.#returnProcessStore.startProcess({ + processId, + items: items.map((item) => ({ + product: item.product, + quantity: item.quantity, + receiptId: data.id, + receiptItemId: item.id, + })), + }); + + this._router.navigate(['../../', 'process'], { relativeTo: this._activatedRoute }); + } } diff --git a/libs/feature/return/pages/src/lib/routes.ts b/libs/feature/return/pages/src/lib/routes.ts index ba1a1fdce..c90f26d08 100644 --- a/libs/feature/return/pages/src/lib/routes.ts +++ b/libs/feature/return/pages/src/lib/routes.ts @@ -23,4 +23,8 @@ export const routes: Routes = [ }, ], }, + { + path: 'process', + loadChildren: () => import('@isa/oms/feature/return-process').then((feat) => feat.routes), + }, ]; diff --git a/libs/feature/return/services/src/index.ts b/libs/feature/return/services/src/index.ts index bf5b0b323..d71e61a89 100644 --- a/libs/feature/return/services/src/index.ts +++ b/libs/feature/return/services/src/index.ts @@ -1,5 +1,3 @@ export * from './lib/return-search.service'; export * from './lib/return-search.store'; -export * from './lib/return-details.service'; -export * from './lib/return-details.store'; export * from './lib/schemas'; diff --git a/libs/feature/return/services/src/lib/schemas/receipt-item.schema.ts b/libs/feature/return/services/src/lib/schemas/receipt-item.schema.ts index 94b04119a..925edc4de 100644 --- a/libs/feature/return/services/src/lib/schemas/receipt-item.schema.ts +++ b/libs/feature/return/services/src/lib/schemas/receipt-item.schema.ts @@ -1,4 +1,4 @@ -import { EntityContainer } from '@isa/common/result'; +import { EntityContainerSchema } from '@isa/common/result'; import { z } from 'zod'; import { BranchSchema } from './branch.schema'; import { OrderItemTypeSchema } from './order-item-type.schema'; @@ -9,7 +9,7 @@ import { PriceSchema } from './price.schema'; export const ReceiptItemSchema = z.object({ id: z.number(), lineNumber: z.number(), - orderBranch: EntityContainer(BranchSchema), + orderBranch: EntityContainerSchema(BranchSchema), orderItemType: OrderItemTypeSchema, paymentStatus: PaymentStatusSchema, product: ProductSchema, diff --git a/libs/feature/return/services/src/lib/schemas/receipt.schema.ts b/libs/feature/return/services/src/lib/schemas/receipt.schema.ts index e16cba67e..02738ef21 100644 --- a/libs/feature/return/services/src/lib/schemas/receipt.schema.ts +++ b/libs/feature/return/services/src/lib/schemas/receipt.schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { BuyerSchema } from './buyer.schema'; -import { EntityContainer } from '@isa/common/result'; +import { EntityContainerSchema } from '@isa/common/result'; import { ReceiptItemSchema } from './receipt-item.schema'; import { ReceiptTypeSchema } from './receipt-type.schema'; import { OrderSchema } from './order.schema'; @@ -11,10 +11,10 @@ export const ReceiptSchema = z.object({ id: z.number(), receiptType: ReceiptTypeSchema, buyer: BuyerSchema, - items: EntityContainer(ReceiptItemSchema).array(), + items: EntityContainerSchema(ReceiptItemSchema).array(), printedDate: z.coerce.date(), receiptNumber: z.string(), - order: EntityContainer(OrderSchema), + order: EntityContainerSchema(OrderSchema), billing: Payer2Schema, shipping: ShippingAddress2Schema, }); diff --git a/libs/icons/src/index.ts b/libs/icons/src/index.ts index a2c4f99b1..b72d231f2 100644 --- a/libs/icons/src/index.ts +++ b/libs/icons/src/index.ts @@ -1 +1,2 @@ -export * from './lib/icons' +export * from './lib/icons'; +export * from './lib/icon-groups'; diff --git a/libs/icons/src/lib/icon-groups.ts b/libs/icons/src/lib/icon-groups.ts new file mode 100644 index 000000000..36b911cf6 --- /dev/null +++ b/libs/icons/src/lib/icon-groups.ts @@ -0,0 +1,16 @@ +import { + isaArtikelCd, + isaArtikelEbook, + isaArtikelKartoniert, + isaArtikelDigital, + isaArtikelGame, + isaArtikelTaschenbuch, + isaArtikelSonstige, + isaArtikelTolino, + isaNavigationArtikelsuche, +} from './icons'; + +export const ProductFormatIconGroup = { + tb: isaArtikelTaschenbuch, + hc: isaArtikelKartoniert, +}; diff --git a/libs/icons/src/lib/icons.ts b/libs/icons/src/lib/icons.ts index f963da072..b20559714 100644 --- a/libs/icons/src/lib/icons.ts +++ b/libs/icons/src/lib/icons.ts @@ -116,17 +116,39 @@ export const isaFiliale = export const isaFilialeLocation = ' '; -export const isaArtikelKartoniert = - ' '; +export const isaArtikelKartoniert = ` + + +`; -export const isaArtikelEbook = - ' '; +export const isaArtikelTaschenbuch = ` + + +`; -export const isaArtikelTaschenbuch = - ' '; +export const isaArtikelCd = ` + +`; -export const isaArtikelCd = - ' '; +export const isaArtikelEbook = ` + +`; + +export const isaArtikelGame = ` + +`; + +export const isaArtikelDigital = ` + +`; + +export const isaArtikelTolino = ` + +`; + +export const isaArtikelSonstige = ` + +`; export const isaDeliveryWarenausgabe = ' '; diff --git a/libs/oms/data-access/README.md b/libs/oms/data-access/README.md new file mode 100644 index 000000000..1cf4c826d --- /dev/null +++ b/libs/oms/data-access/README.md @@ -0,0 +1,7 @@ +# oms-data-access + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test oms-data-access` to execute the unit tests. diff --git a/libs/oms/data-access/eslint.config.mjs b/libs/oms/data-access/eslint.config.mjs new file mode 100644 index 000000000..3bf3b5ae0 --- /dev/null +++ b/libs/oms/data-access/eslint.config.mjs @@ -0,0 +1,34 @@ +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: {}, + }, +]; diff --git a/libs/oms/data-access/jest.config.ts b/libs/oms/data-access/jest.config.ts new file mode 100644 index 000000000..024724b93 --- /dev/null +++ b/libs/oms/data-access/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'oms-data-access', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/oms/data-access', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/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', + ], +}; diff --git a/libs/oms/data-access/project.json b/libs/oms/data-access/project.json new file mode 100644 index 000000000..770f20c73 --- /dev/null +++ b/libs/oms/data-access/project.json @@ -0,0 +1,20 @@ +{ + "name": "oms-data-access", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/oms/data-access/src", + "prefix": "lib", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/oms/data-access/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/oms/data-access/src/index.ts b/libs/oms/data-access/src/index.ts new file mode 100644 index 000000000..6917f0f0d --- /dev/null +++ b/libs/oms/data-access/src/index.ts @@ -0,0 +1,6 @@ +export * from './lib/models'; +export * from './lib/schemas'; +export * from './lib/return-details.service'; +export * from './lib/return-details.store'; +export * from './lib/return-process.service'; +export * from './lib/return-process.store'; diff --git a/libs/oms/data-access/src/lib/index.ts b/libs/oms/data-access/src/lib/index.ts new file mode 100644 index 000000000..d4559ef6b --- /dev/null +++ b/libs/oms/data-access/src/lib/index.ts @@ -0,0 +1,6 @@ +export * from './models'; +export * from './return-details.service'; +export * from './return-details.store'; +export * from './return-process.service'; +export * from './return-process.store'; +export * from './schemas'; diff --git a/libs/oms/data-access/src/lib/models/buyer.ts b/libs/oms/data-access/src/lib/models/buyer.ts new file mode 100644 index 000000000..0ac9fddba --- /dev/null +++ b/libs/oms/data-access/src/lib/models/buyer.ts @@ -0,0 +1,5 @@ +import { BuyerDTO } from '@generated/swagger/oms-api'; + +export interface Buyer extends BuyerDTO { + buyerNumber: string; +} diff --git a/libs/oms/data-access/src/lib/models/index.ts b/libs/oms/data-access/src/lib/models/index.ts new file mode 100644 index 000000000..5e71d0b73 --- /dev/null +++ b/libs/oms/data-access/src/lib/models/index.ts @@ -0,0 +1,8 @@ +export * from './buyer'; +export * from './product'; +export * from './quantity'; +export * from './receipt-item'; +export * from './receipt'; +export * from './return-process-answers'; +export * from './return-process-question'; +export * from './return-process'; diff --git a/libs/oms/data-access/src/lib/models/product.ts b/libs/oms/data-access/src/lib/models/product.ts new file mode 100644 index 000000000..5c2474cae --- /dev/null +++ b/libs/oms/data-access/src/lib/models/product.ts @@ -0,0 +1,7 @@ +import { ProductDTO } from '@generated/swagger/oms-api'; + +export interface Product extends ProductDTO { + ean: string; + format: string; + formatDetails: string; +} diff --git a/libs/oms/data-access/src/lib/models/quantity.ts b/libs/oms/data-access/src/lib/models/quantity.ts new file mode 100644 index 000000000..c682afc6a --- /dev/null +++ b/libs/oms/data-access/src/lib/models/quantity.ts @@ -0,0 +1,5 @@ +import { QuantityDTO } from '@generated/swagger/oms-api'; + +export interface Quantity extends QuantityDTO { + quantity: number; +} diff --git a/libs/oms/data-access/src/lib/models/receipt-item.ts b/libs/oms/data-access/src/lib/models/receipt-item.ts new file mode 100644 index 000000000..03d150730 --- /dev/null +++ b/libs/oms/data-access/src/lib/models/receipt-item.ts @@ -0,0 +1,9 @@ +import { ReceiptItemDTO } from '@generated/swagger/oms-api'; +import { Product } from './product'; +import { Quantity } from './quantity'; + +export interface ReceiptItem extends ReceiptItemDTO { + id: number; + product: Product; + quantity: Quantity; +} diff --git a/libs/oms/data-access/src/lib/models/receipt.ts b/libs/oms/data-access/src/lib/models/receipt.ts new file mode 100644 index 000000000..427aa749d --- /dev/null +++ b/libs/oms/data-access/src/lib/models/receipt.ts @@ -0,0 +1,10 @@ +import { ReceiptDTO } from '@generated/swagger/oms-api'; +import { EntityContainer } from '@isa/common/result'; +import { ReceiptItem } from './receipt-item'; +import { Buyer } from './buyer'; + +export interface Receipt extends ReceiptDTO { + id: number; + items: EntityContainer[]; + buyer: Buyer; +} diff --git a/libs/oms/data-access/src/lib/models/return-process-answers.ts b/libs/oms/data-access/src/lib/models/return-process-answers.ts new file mode 100644 index 000000000..4e1529b8c --- /dev/null +++ b/libs/oms/data-access/src/lib/models/return-process-answers.ts @@ -0,0 +1,3 @@ +export interface ReturnProcessAnswers { + answerId: string; +} diff --git a/libs/oms/data-access/src/lib/models/return-process-question.ts b/libs/oms/data-access/src/lib/models/return-process-question.ts new file mode 100644 index 000000000..ccc5828f1 --- /dev/null +++ b/libs/oms/data-access/src/lib/models/return-process-question.ts @@ -0,0 +1,31 @@ +export const ReturnProcessQuestionType = { + Select: 'select', + Product: 'product', +} as const; + +export type ReturnProcessQuestionType = + (typeof ReturnProcessQuestionType)[keyof typeof ReturnProcessQuestionType]; + +export interface ReturnProcessQuestionBase { + id: string; + description: string; + type: ReturnProcessQuestionType; +} + +export interface ReturnProcessSelectQuestion extends ReturnProcessQuestionBase { + type: typeof ReturnProcessQuestionType.Select; + options: ReturnProcessQuestionSelectOptions[]; +} + +export interface ReturnProcessQuestionSelectOptions { + label: string; + value: string; + nextQuestionId?: string; +} + +export interface ReturnProcessProductQuestion extends ReturnProcessQuestionBase { + type: typeof ReturnProcessQuestionType.Product; + nextQuestionId?: string; +} + +export type ReturnProcessQuestion = ReturnProcessSelectQuestion | ReturnProcessProductQuestion; diff --git a/libs/oms/data-access/src/lib/models/return-process.ts b/libs/oms/data-access/src/lib/models/return-process.ts new file mode 100644 index 000000000..5b749ff61 --- /dev/null +++ b/libs/oms/data-access/src/lib/models/return-process.ts @@ -0,0 +1,7 @@ +export interface ItemToReturn { + id: number; + processId: number; + receiptId: number; + receiptItemId: number; + quantity: number; +} diff --git a/libs/feature/return/services/src/lib/return-details.service.ts b/libs/oms/data-access/src/lib/return-details.service.ts similarity index 62% rename from libs/feature/return/services/src/lib/return-details.service.ts rename to libs/oms/data-access/src/lib/return-details.service.ts index e3e523fb0..aeba1fc78 100644 --- a/libs/feature/return/services/src/lib/return-details.service.ts +++ b/libs/oms/data-access/src/lib/return-details.service.ts @@ -1,21 +1,14 @@ import { inject, Injectable } from '@angular/core'; -import { - FetchReturnDetails, - FetchReturnDetailsSchema, -} from './schemas/fetch-return-details.schema'; +import { FetchReturnDetails, FetchReturnDetailsSchema } from './schemas'; import { map, Observable, throwError } from 'rxjs'; import { ReceiptService } from '@generated/swagger/oms-api'; -import { safeParse } from '@isa/utils/z-safe-parse'; -import { - ResponseArgsOfReceipt, - ResponseArgsOfReceiptSchema, -} from './schemas/response-args-of-receipt.schema'; +import { Receipt } from './models'; @Injectable({ providedIn: 'root' }) export class ReturnDetailsService { #receiptService = inject(ReceiptService); - fetchReturnDetails(params: FetchReturnDetails): Observable { + fetchReturnDetails(params: FetchReturnDetails): Observable { try { const parsed = FetchReturnDetailsSchema.parse(params); @@ -27,7 +20,7 @@ export class ReturnDetailsService { throw new Error('Failed to fetch return details'); } - return safeParse(ResponseArgsOfReceiptSchema, res); + return res.result as Receipt; }), ); } catch (error) { diff --git a/libs/feature/return/services/src/lib/return-details.store.ts b/libs/oms/data-access/src/lib/return-details.store.ts similarity index 95% rename from libs/feature/return/services/src/lib/return-details.store.ts rename to libs/oms/data-access/src/lib/return-details.store.ts index a9d1fb98e..458f3b2b2 100644 --- a/libs/feature/return/services/src/lib/return-details.store.ts +++ b/libs/oms/data-access/src/lib/return-details.store.ts @@ -1,12 +1,12 @@ import { patchState, signalStore, type, withMethods } from '@ngrx/signals'; import { addEntity, entityConfig, updateEntity, withEntities } from '@ngrx/signals/entities'; import { Result, ResultStatus } from '@isa/common/result'; -import { Receipt } from './schemas'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { pipe, switchMap, tap } from 'rxjs'; import { inject } from '@angular/core'; import { ReturnDetailsService } from './return-details.service'; import { tapResponse } from '@ngrx/operators'; +import { Receipt } from './models'; export type ReturnResult = Result & { id: number }; @@ -53,7 +53,7 @@ export const ReturnDetailsStore = signalStore( returnDetailsService.fetchReturnDetails({ receiptId }).pipe( tapResponse({ next(value) { - store._fetchSuccess(receiptId, value.result); + store._fetchSuccess(receiptId, value); }, error(error) { store._fetchError(receiptId, error); diff --git a/libs/oms/data-access/src/lib/return-process.service.ts b/libs/oms/data-access/src/lib/return-process.service.ts new file mode 100644 index 000000000..94e8d7711 --- /dev/null +++ b/libs/oms/data-access/src/lib/return-process.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { Product, ReturnProcessQuestion, ReturnProcessQuestionType } from './models'; +import { Observable, of } from 'rxjs'; + +const bookAndCalendarQuestions: ReturnProcessQuestion[] = [ + { + id: 'item_condition', + description: 'Wie ist der Artkelzustand?', + type: ReturnProcessQuestionType.Select, + options: [ + { label: 'Neuwertig/Originalverpackt', value: 'ovp', nextQuestionId: 'return_reason' }, + { label: 'Beschädigt/Fehldruck', value: 'damaged' }, + ], + }, + { + id: 'return_reason', + description: 'Warum möchtest du den Artikel zurücksenden?', + type: ReturnProcessQuestionType.Select, + options: [ + { label: 'Gefällt nicht/Wiederruf', value: 'dislike' }, + { label: 'Fehllieferung', value: 'wrong_item', nextQuestionId: 'delivered_item' }, + ], + }, + { + id: 'delivered_item', + description: 'Welcher Artikel wurde geliefert?', + type: ReturnProcessQuestionType.Product, + }, +]; + +@Injectable({ providedIn: 'root' }) +export class ReturnProcessService { + returnProcessQuestions( + product: Product, + ): Observable<{ startWithQuestionId: string; questions: ReturnProcessQuestion[] }> { + if (product.format === 'HC' || product.format === 'TB') { + return of({ startWithQuestionId: 'item_condition', questions: bookAndCalendarQuestions }); + } + + return of({ startWithQuestionId: '', questions: [] }); + } + + eligibleForReturn(): Observable { + throw new Error('Method not implemented.'); + } +} diff --git a/libs/oms/data-access/src/lib/return-process.store.ts b/libs/oms/data-access/src/lib/return-process.store.ts new file mode 100644 index 000000000..d913b7849 --- /dev/null +++ b/libs/oms/data-access/src/lib/return-process.store.ts @@ -0,0 +1,92 @@ +import { patchState, signalStore, withComputed, withHooks, withMethods } from '@ngrx/signals'; +import { withEntities, setAllEntities, updateEntity } from '@ngrx/signals/entities'; +import { IDBStorageProvider, withStorage } from '@isa/core/storage'; +import { computed, effect, inject } from '@angular/core'; +import { ProcessService } from '@isa/core/process'; +import { Product, Quantity } from './models'; + +export interface ReturnProcess { + id: number; + processId: number; + receiptId: number; + receiptItemId: number; + product: Product; + quantity: Quantity; + answers: Record; +} + +export type StartProcess = { + processId: number; + items: Pick[]; +}; + +export const ReturnProcessStore = signalStore( + { providedIn: 'root' }, + withStorage('return-process', IDBStorageProvider), + withEntities(), + withComputed((store) => ({ + nextId: computed(() => Math.max(0, ...store.ids().map(Number)) + 1), + })), + withMethods((store) => ({ + removeAllEntitiesByProcessId: (...processIds: number[]) => { + const entitiesToRemove = store + .entities() + .filter((entity) => processIds.includes(entity.processId)); + patchState(store, setAllEntities(entitiesToRemove)); + store.storeState(); + }, + + setAnswer: (id: number, question: string, answer: unknown) => { + const entity = store.entityMap()[id]; + if (entity) { + const answers = { ...entity.answers, [question]: answer }; + patchState(store, updateEntity({ id: entity.id, changes: { answers } })); + store.storeState(); + } + }, + removeAnswer: (id: number, question: string) => { + const entity = store.entityMap()[id]; + if (entity) { + const answers = { ...entity.answers }; + delete answers[question]; + patchState(store, updateEntity({ id: entity.id, changes: { answers } })); + store.storeState(); + } + }, + })), + withMethods((store) => ({ + startProcess: (params: StartProcess) => { + store.removeAllEntitiesByProcessId(params.processId); + const entities: ReturnProcess[] = []; + const nextId = store.nextId(); + + for (let i = 0; i < params.items.length; i++) { + const item = params.items[i]; + entities.push({ + id: nextId + i, + processId: params.processId, + receiptId: item.receiptId, + receiptItemId: item.receiptItemId, + quantity: item.quantity, + product: item.product, + answers: {}, + }); + } + + patchState(store, setAllEntities(entities)); + store.storeState(); + }, + })), + + withHooks((store, processService = inject(ProcessService)) => ({ + onInit() { + effect(() => { + const processIds = processService.ids(); + const entities = store.entities().find((entity) => !processIds.includes(entity.processId)); + if (entities) { + store.removeAllEntitiesByProcessId(entities.processId); + } + }); + }, + })), +); diff --git a/libs/oms/data-access/src/lib/schemas/fetch-return-details.ts b/libs/oms/data-access/src/lib/schemas/fetch-return-details.ts new file mode 100644 index 000000000..8ced08148 --- /dev/null +++ b/libs/oms/data-access/src/lib/schemas/fetch-return-details.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const FetchReturnDetailsSchema = z.object({ + receiptId: z.number(), +}); + +export type FetchReturnDetails = z.infer; diff --git a/libs/oms/data-access/src/lib/schemas/index.ts b/libs/oms/data-access/src/lib/schemas/index.ts new file mode 100644 index 000000000..ee6e56d27 --- /dev/null +++ b/libs/oms/data-access/src/lib/schemas/index.ts @@ -0,0 +1 @@ +export * from './fetch-return-details'; diff --git a/libs/oms/data-access/src/test-setup.ts b/libs/oms/data-access/src/test-setup.ts new file mode 100644 index 000000000..ea414013f --- /dev/null +++ b/libs/oms/data-access/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/oms/data-access/tsconfig.json b/libs/oms/data-access/tsconfig.json new file mode 100644 index 000000000..fde35eab0 --- /dev/null +++ b/libs/oms/data-access/tsconfig.json @@ -0,0 +1,28 @@ +{ + "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 + } +} diff --git a/libs/oms/data-access/tsconfig.lib.json b/libs/oms/data-access/tsconfig.lib.json new file mode 100644 index 000000000..9b49be758 --- /dev/null +++ b/libs/oms/data-access/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "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"] +} diff --git a/libs/oms/data-access/tsconfig.spec.json b/libs/oms/data-access/tsconfig.spec.json new file mode 100644 index 000000000..f858ef78c --- /dev/null +++ b/libs/oms/data-access/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "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" + ] +} diff --git a/libs/oms/feature/return-process/README.md b/libs/oms/feature/return-process/README.md new file mode 100644 index 000000000..ccde5d015 --- /dev/null +++ b/libs/oms/feature/return-process/README.md @@ -0,0 +1,7 @@ +# oms-feature-return-process + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test oms-feature-return-process` to execute the unit tests. diff --git a/libs/oms/feature/return-process/eslint.config.mjs b/libs/oms/feature/return-process/eslint.config.mjs new file mode 100644 index 000000000..7323f5897 --- /dev/null +++ b/libs/oms/feature/return-process/eslint.config.mjs @@ -0,0 +1,34 @@ +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: {}, + }, +]; diff --git a/libs/oms/feature/return-process/jest.config.ts b/libs/oms/feature/return-process/jest.config.ts new file mode 100644 index 000000000..152813dc3 --- /dev/null +++ b/libs/oms/feature/return-process/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'oms-feature-return-process', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/oms/feature/return-process', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/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', + ], +}; diff --git a/libs/oms/feature/return-process/project.json b/libs/oms/feature/return-process/project.json new file mode 100644 index 000000000..ca1449ff2 --- /dev/null +++ b/libs/oms/feature/return-process/project.json @@ -0,0 +1,20 @@ +{ + "name": "oms-feature-return-process", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/oms/feature/return-process/src", + "prefix": "lib", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/oms/feature/return-process/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/oms/feature/return-process/src/index.ts b/libs/oms/feature/return-process/src/index.ts new file mode 100644 index 000000000..ece2c025d --- /dev/null +++ b/libs/oms/feature/return-process/src/index.ts @@ -0,0 +1 @@ +export * from './lib/routes'; diff --git a/libs/oms/feature/return-process/src/lib/return-process-feature.component.html b/libs/oms/feature/return-process/src/lib/return-process-feature.component.html new file mode 100644 index 000000000..25566cb7b --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-feature.component.html @@ -0,0 +1,18 @@ +@if (returnProcesses(); as processes) { +
+
+ +
+

+ Wählen Sie den Artikelzustand und den
+ Rückgabegrund aus +

+
+
+ @for (process of processes; track process.id) { + + } +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-feature.component.scss b/libs/oms/feature/return-process/src/lib/return-process-feature.component.scss new file mode 100644 index 000000000..123289526 --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-feature.component.scss @@ -0,0 +1,14 @@ +:host { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 1.5rem; + align-self: stretch; +} + +.return-process-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + align-self: stretch; +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-feature.component.ts b/libs/oms/feature/return-process/src/lib/return-process-feature.component.ts new file mode 100644 index 000000000..9b61ef23a --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-feature.component.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; +import { ButtonComponent } from '@isa/ui/buttons'; +import { NgIconComponent, provideIcons } from '@ng-icons/core'; +import { isaActionChevronLeft } from '@isa/icons'; +import { ReturnProcess, ReturnProcessStore } from '@isa/oms/data-access'; +import { injectActivatedProcessId } from '@isa/core/process'; +import { ReturnProcessItemComponent } from './return-process-item/return-process-item.component'; + +@Component({ + selector: 'lib-return-process-feature', + templateUrl: './return-process-feature.component.html', + styleUrls: ['./return-process-feature.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ButtonComponent, NgIconComponent, ReturnProcessItemComponent], + providers: [provideIcons({ isaActionChevronLeft })], +}) +export class ReturnProcessFeatureComponent { + #returnProcessStore = inject(ReturnProcessStore); + + processId = injectActivatedProcessId(); + + returnProcesses = computed(() => { + const processId = this.processId(); + + if (!processId) { + throw new Error('No process id found'); + } + + return this.#returnProcessStore.entities().filter((process) => process.processId === processId); + }); +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.html b/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.html new file mode 100644 index 000000000..d51eb2a19 --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.html @@ -0,0 +1,28 @@ +@if (returnProcess(); as process) { +
+

Rückgabe informationen

+
+
+ +
+
+ {{ process.product.contributors }} +
+
+ {{ process.product.name }} +
+
+ + {{ process.product.formatDetail }} +
+
+ +
+} diff --git a/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.scss b/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.scss new file mode 100644 index 000000000..1aaffe24b --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.scss @@ -0,0 +1,49 @@ +:host { + display: flex; + padding: 1.5rem; + flex-direction: column; + align-items: flex-start; + gap: 1.5rem; + align-self: stretch; + border-radius: 1rem; + background: #fff; + @apply text-neutral-900; +} + +.return-process-item-header { + display: flex; + padding-bottom: 1.5rem; + justify-content: space-between; + align-items: center; + align-self: stretch; + + @apply border-b border-neutral-300; +} + +.return-process-item-body { + display: flex; + align-items: flex-start; + gap: 1.5rem; + width: 100%; +} + +.return-process-item-body__product-info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + flex: 1 0 0; +} + +.return-process-item-body__product-info-format { + display: flex; + align-items: center; + gap: 0.5rem; + align-self: stretch; +} + +[lib-return-process-questions] { + flex-shrink: 0; + flex-grow: 1; + align-self: stretch; +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.ts b/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.ts new file mode 100644 index 000000000..73907d141 --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-item/return-process-item.component.ts @@ -0,0 +1,26 @@ +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; +import { ProductImageDirective } from '@isa/shared/product-image'; +import { ReturnProcessStore } from '@isa/oms/data-access'; +import { NgIconComponent, provideIcons } from '@ng-icons/core'; +import { ProductFormatIconGroup } from '@isa/icons'; +import { LowerCasePipe } from '@angular/common'; +import { ReturnProcessQuestionsComponent } from '../return-process-questions/return-process-questions.component'; + +@Component({ + selector: 'lib-return-process-item', + templateUrl: './return-process-item.component.html', + styleUrls: ['./return-process-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ProductImageDirective, NgIconComponent, LowerCasePipe, ReturnProcessQuestionsComponent], + providers: [provideIcons(ProductFormatIconGroup)], +}) +export class ReturnProcessItemComponent { + returnProcessStore = inject(ReturnProcessStore); + + returnProcessId = input.required(); + + returnProcess = computed(() => { + return this.returnProcessStore.entityMap()[this.returnProcessId()]; + }); +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.html b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.html new file mode 100644 index 000000000..57b6891bd --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.html @@ -0,0 +1,35 @@ +
+
+ {{ question().description }} +
+
+
+ + + + +
+
+
+ +@if (product(); as p) { +
+
Gelieferter Artikel:
+
+ +
+
{{ p.contributors }}
+
{{ p.name }}
+
+
+
+
+ Falls es sich um den falschen Artikel handelt, geben sie die EAN erneut ein, oder Scannen Sie + das Produkt erneut. +
+} diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.scss b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.scss new file mode 100644 index 000000000..acd6c7ea2 --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.scss @@ -0,0 +1,3 @@ +:host { + @apply flex flex-col gap-6 justify-between text-isa-neutral-900; +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.ts b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.ts new file mode 100644 index 000000000..f52884ebf --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-product-question/return-process-product-question.component.ts @@ -0,0 +1,49 @@ +import { ChangeDetectionStrategy, Component, inject, input, signal } from '@angular/core'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Product, ReturnProcessProductQuestion, ReturnProcessStore } from '@isa/oms/data-access'; +import { TextButtonComponent } from '@isa/ui/buttons'; +import { TextFieldComponent } from '@isa/ui/input-controls'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { filter, pipe, switchMap } from 'rxjs'; +import { CatalougeSearchService } from '@isa/catalogue/data-access'; +import { tapResponse } from '@ngrx/operators'; +import { ProductImageDirective } from '@isa/shared/product-image'; + +@Component({ + selector: 'lib-return-process-product-question', + templateUrl: './return-process-product-question.component.html', + styleUrls: ['./return-process-product-question.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ReactiveFormsModule, TextFieldComponent, TextButtonComponent, ProductImageDirective], +}) +export class ReturnProcessProductQuestionComponent { + // #returnProcessStore = inject(ReturnProcessStore); + #catalogueSearchService = inject(CatalougeSearchService); + + processId = input.required(); + + question = input.required(); + + control = new FormControl('9783551557438', [Validators.required]); + + product = signal(undefined); + + check = rxMethod( + pipe( + filter(() => this.control.valid), + switchMap(() => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.#catalogueSearchService.searchByEans(this.control.value!).pipe( + tapResponse({ + next: (value) => { + this.product.set(value[0].product); + }, + error: (error) => { + console.error(error); + }, + }), + ), + ), + ), + ); +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.html b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.html new file mode 100644 index 000000000..a599e8197 --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.html @@ -0,0 +1,21 @@ +@for (question of renderQuestions(); track question.id; let last = $last) { +
+ @switch (question.type) { + @case ('select') { + + } + @case ('product') { + + } + @default { + {{ question | json }} + } + } +
+} diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.scss b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.scss new file mode 100644 index 000000000..a5eb94b73 --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.scss @@ -0,0 +1,12 @@ +:host { + @apply flex flex-col items-stretch justify-between gap-6; + @apply text-isa-neutral-900; +} + +.item-content { + padding-bottom: 1.5rem; + + &.with-border-bottom { + @apply border-b border-solid border-isa-neutral-300; + } +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.ts b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.ts new file mode 100644 index 000000000..934e4cc27 --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-questions.component.ts @@ -0,0 +1,79 @@ +import { JsonPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { + ReturnProcess, + ReturnProcessQuestion, + ReturnProcessQuestionType, + ReturnProcessService, + ReturnProcessStore, +} from '@isa/oms/data-access'; +import { filter, switchMap } from 'rxjs'; +import { ReturnProcessSelectQuestionComponent } from './return-process-select-question/return-process-select-question.component'; +import { ReturnProcessProductQuestionComponent } from './return-process-product-question/return-process-product-question.component'; + +@Component({ + selector: 'lib-return-process-questions', + templateUrl: './return-process-questions.component.html', + styleUrls: ['./return-process-questions.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [JsonPipe, ReturnProcessSelectQuestionComponent, ReturnProcessProductQuestionComponent], +}) +export class ReturnProcessQuestionsComponent { + #returnProcessStore = inject(ReturnProcessStore); + #returnProcessSerivce = inject(ReturnProcessService); + + returnProcessId = input.required(); + + returnProcess = computed(() => { + const returnProcessId = this.returnProcessId(); + return this.#returnProcessStore.entityMap()[returnProcessId]; + }); + + questions = toSignal( + toObservable(this.returnProcess).pipe( + filter((returnProcess) => !!returnProcess), + switchMap((returnProcess) => + this.#returnProcessSerivce.returnProcessQuestions(returnProcess.product), + ), + ), + ); + + renderQuestions = computed(() => { + const answers = this.returnProcess()?.answers; + + if (!answers) { + return []; + } + + const questions = this.questions(); + + if (!questions) { + return []; + } + + let questionId: string | undefined = questions?.startWithQuestionId; + const result: ReturnProcessQuestion[] = []; + + while (questionId) { + const question = questions?.questions.find((q) => q.id === questionId); + if (question) { + result.push(question); + + if (question.type === ReturnProcessQuestionType.Select) { + const option = question.options.find((o) => o.value === answers[question.id]); + questionId = option?.nextQuestionId; + } else if (question.type === ReturnProcessQuestionType.Product) { + questionId = answers[question.id] ? question.nextQuestionId : undefined; + } else { + console.error('Unknown question type', question); + break; + } + } else { + break; + } + } + + return result; + }); +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.html b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.html new file mode 100644 index 000000000..4dcf94186 --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.html @@ -0,0 +1,10 @@ +
+ {{ question().description }} +
+ + @for (option of question().options; track option.value) { + + {{ option.label }} + + } + diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.scss b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.scss new file mode 100644 index 000000000..9779e41e9 --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.scss @@ -0,0 +1,7 @@ +:host { + display: flex; + align-items: center; + gap: 1.5rem; + flex-shrink: 0; + @apply justify-between; +} diff --git a/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.ts b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.ts new file mode 100644 index 000000000..25257560b --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/return-process-questions/return-process-select-question/return-process-select-question.component.ts @@ -0,0 +1,41 @@ +import { ChangeDetectionStrategy, Component, effect, inject, input } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { ReturnProcessSelectQuestion, ReturnProcessStore } from '@isa/oms/data-access'; +import { ChipsComponent, ChipOptionComponent } from '@isa/ui/input-controls'; + +@Component({ + selector: 'lib-return-process-select-question', + templateUrl: './return-process-select-question.component.html', + styleUrls: ['./return-process-select-question.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ChipsComponent, ChipOptionComponent, ReactiveFormsModule], +}) +export class ReturnProcessSelectQuestionComponent { + #returnProcessStore = inject(ReturnProcessStore); + + processId = input.required(); + + question = input.required(); + + control = new FormControl(undefined, [Validators.required]); + + constructor() { + this.control.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { + if (this.control.valid) { + this.#returnProcessStore.setAnswer(this.processId(), this.question().id, value); + } else { + this.#returnProcessStore.removeAnswer(this.processId(), this.question().id); + } + }); + + effect(() => { + const value = + this.#returnProcessStore.entityMap()[this.processId()]?.answers[this.question().id]; + if (value !== this.control.value) { + this.control.setValue(value as string); + } + }); + } +} diff --git a/libs/oms/feature/return-process/src/lib/routes.ts b/libs/oms/feature/return-process/src/lib/routes.ts new file mode 100644 index 000000000..ce3870c6a --- /dev/null +++ b/libs/oms/feature/return-process/src/lib/routes.ts @@ -0,0 +1,4 @@ +import { Route } from '@angular/router'; +import { ReturnProcessFeatureComponent } from './return-process-feature.component'; + +export const routes: Route[] = [{ path: '', component: ReturnProcessFeatureComponent }]; diff --git a/libs/oms/feature/return-process/src/test-setup.ts b/libs/oms/feature/return-process/src/test-setup.ts new file mode 100644 index 000000000..ea414013f --- /dev/null +++ b/libs/oms/feature/return-process/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/oms/feature/return-process/tsconfig.json b/libs/oms/feature/return-process/tsconfig.json new file mode 100644 index 000000000..52a0866e0 --- /dev/null +++ b/libs/oms/feature/return-process/tsconfig.json @@ -0,0 +1,28 @@ +{ + "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 + } +} diff --git a/libs/oms/feature/return-process/tsconfig.lib.json b/libs/oms/feature/return-process/tsconfig.lib.json new file mode 100644 index 000000000..912738705 --- /dev/null +++ b/libs/oms/feature/return-process/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "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"] +} diff --git a/libs/oms/feature/return-process/tsconfig.spec.json b/libs/oms/feature/return-process/tsconfig.spec.json new file mode 100644 index 000000000..6e5925e5c --- /dev/null +++ b/libs/oms/feature/return-process/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "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" + ] +} diff --git a/libs/ui/input-controls/src/index.ts b/libs/ui/input-controls/src/index.ts index 7aa320f0e..a0701ec32 100644 --- a/libs/ui/input-controls/src/index.ts +++ b/libs/ui/input-controls/src/index.ts @@ -2,3 +2,5 @@ export * from './lib/checkbox/checkbox.component'; export * from './lib/dropdown/dropdown.component'; export * from './lib/dropdown/dropdown.types'; export * from './lib/text-field/text-field.component'; +export * from './lib/chips/chips.component'; +export * from './lib/chips/chip-option.component'; diff --git a/libs/ui/input-controls/src/lib/chips/chip-option.component.scss b/libs/ui/input-controls/src/lib/chips/chip-option.component.scss new file mode 100644 index 000000000..10101dfb7 --- /dev/null +++ b/libs/ui/input-controls/src/lib/chips/chip-option.component.scss @@ -0,0 +1,18 @@ +.ui-chip-option { + display: inline-flex; + height: 2.5rem; + min-width: 10rem; + padding: 0.5rem 1.25rem; + justify-content: center; + align-items: center; + flex-shrink: 0; + + border-radius: 6.25rem; + @apply border border-solid border-neutral-600; + + @apply isa-text-body-2-bold text-isa-neutral-600 cursor-pointer; +} + +.ui-chip-option__selected { + @apply bg-isa-neutral-200 text-isa-neutral-800; +} diff --git a/libs/ui/input-controls/src/lib/chips/chip-option.component.ts b/libs/ui/input-controls/src/lib/chips/chip-option.component.ts new file mode 100644 index 000000000..4165d87c7 --- /dev/null +++ b/libs/ui/input-controls/src/lib/chips/chip-option.component.ts @@ -0,0 +1,37 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + input, + ViewEncapsulation, +} from '@angular/core'; +import { ChipsComponent } from './chips.component'; + +@Component({ + selector: 'ui-chip-option', + template: ``, + styleUrls: ['./chip-option.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + standalone: true, + host: { '[class]': '["ui-chip-option", selectedClass()]', '(click)': 'toggle()' }, +}) +export class ChipOptionComponent { + private host = inject(ChipsComponent, { host: true }); + + selected = computed(() => { + this.host.value(); + return this.host['selectionModel'].isSelected(this.value()); + }); + + selectedClass = computed(() => { + return this.selected() ? 'ui-chip-option__selected' : ''; + }); + + value = input.required(); + + toggle() { + this.host.select(this.value()); + } +} diff --git a/libs/ui/input-controls/src/lib/chips/chips.component.scss b/libs/ui/input-controls/src/lib/chips/chips.component.scss new file mode 100644 index 000000000..b78a68048 --- /dev/null +++ b/libs/ui/input-controls/src/lib/chips/chips.component.scss @@ -0,0 +1,5 @@ +.ui-chips { + display: flex; + align-items: center; + gap: 0.75rem; +} diff --git a/libs/ui/input-controls/src/lib/chips/chips.component.ts b/libs/ui/input-controls/src/lib/chips/chips.component.ts new file mode 100644 index 000000000..7cd7e5782 --- /dev/null +++ b/libs/ui/input-controls/src/lib/chips/chips.component.ts @@ -0,0 +1,81 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + model, + ViewEncapsulation, +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { SelectionModel } from '@angular/cdk/collections'; + +@Component({ + selector: 'ui-chips', + template: ``, + styleUrls: ['./chips.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { + '[class]': '["ui-chips", disabledClass()]', + }, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: ChipsComponent, + multi: true, + }, + ], +}) +export class ChipsComponent implements ControlValueAccessor { + value = model(); + + disabled = model(false); + + disabledClass = computed(() => { + return this.disabled() ? 'ui-chips__disabled' : ''; + }); + + private selectionModel: SelectionModel; + + onChange?: (value: T) => void; + + onTouched?: () => void; + + constructor() { + this.selectionModel = new SelectionModel(false); + } + + writeValue(obj: unknown): void { + this.selectionModel?.select(obj as T); + this.value.set(obj as T); + } + + registerOnChange(fn: () => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled.set(isDisabled); + } + + select(value: T, options: { emitEvent: boolean } = { emitEvent: true }) { + this.selectionModel.select(value); + this.onTouched?.(); + this.value.set(this.selectionModel.selected[0]); + if (options.emitEvent) { + this.onChange?.(this.selectionModel.selected[0]); + } + } + + toggle(value: T, options: { emitEvent: boolean } = { emitEvent: true }) { + this.selectionModel.toggle(value); + this.onTouched?.(); + this.value.set(this.selectionModel.selected[0]); + if (options.emitEvent) { + this.onChange?.(this.selectionModel.selected[0]); + } + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 7ad7c8d08..ff6d7e1a8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -44,6 +44,7 @@ ], "@generated/swagger/wws-api": ["generated/swagger/wws-api/src/index.ts"], "@hub/*": ["apps/isa-app/src/hub/*/index.ts"], + "@isa/catalogue/data-access": ["libs/catalogue/data-access/src/index.ts"], "@isa/common/result": ["libs/common/result/src/index.ts"], "@isa/core/config": ["libs/core/config/src/index.ts"], "@isa/core/process": ["libs/core/process/src/index.ts"], @@ -51,6 +52,10 @@ "@isa/core/scroll-position": ["libs/core/scroll-position/src/index.ts"], "@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-process": [ + "libs/oms/feature/return-process/src/index.ts" + ], "@isa/oms/utils/translation": ["libs/oms/utils/translation/src/index.ts"], "@isa/shared/filter": ["libs/shared/filter/src/index.ts"], "@isa/shared/product-image": ["libs/shared/product-image/src/index.ts"],