Compare commits

...

38 Commits

Author SHA1 Message Date
Lorenz Hilpert
9623a8ede5 feat(remission): enhance has-pending-remission-hint component with dynamic days threshold and loading/error handling 2025-07-11 16:09:40 +02:00
Lorenz Hilpert
741e1e6d15 feat(remission): add has-pending-remission-hint component and directive
Introduce a new component and directive to display a hint when there are
incomplete remissions older than 7 days. The directive manages the rendering
of the hint based on the state of the remission return receipts.

Refs: #5136
2025-07-10 19:19:33 +02:00
Lorenz Hilpert
a36d746fb8 Merged PR 1879: Warenbegleitschein Übersicht und Details
Related work items: #5137, #5138
2025-07-10 16:00:16 +00:00
Nino
f6b2b554bb Merge branch 'release/4.0' into develop 2025-07-10 14:31:03 +02:00
Nino
465df27858 Merge branch 'release/4.0' into develop 2025-07-10 14:16:29 +02:00
Nino Righi
7c907645dc Merged PR 1880: hotfix(oms-data-access): initial implementation of OMS data access layer
hotfix(oms-data-access): initial implementation of OMS data access layer

Introduce the foundational OMS data access module, including service scaffolding and integration points for future API communication. This establishes a clear separation of concerns for order management system data retrieval and manipulation, following project architecture guidelines.

Ref: #5210
2025-07-10 11:32:42 +00:00
Nino Righi
b7e7155577 Merged PR 1877: #4769, #5194 Remission List Item - StockInfos - ItemInfos
feat(remission-list, remission-shared-product-stock-info): implement product stock info display

Add product stock information to the remission list and shared product components.
This enhances user visibility into current stock levels directly within remission-related views,
improving workflow efficiency and reducing the need for context switching.

Ref: #4769, #5194
2025-07-02 14:23:04 +00:00
Lorenz Hilpert
b28c204f23 refactor(tabs): add metadata and navigation properties to Tab model 2025-07-01 12:00:12 +02:00
Lorenz Hilpert
e7a807cfbd refactor(tabs): enhance Tab interface with navigation and metadata 2025-07-01 11:46:30 +02:00
Lorenz Hilpert
344dc61a90 fix: resolve CSS cascade issue with UI components and Tailwind utilities
- Move UI component styles to @layer components in tailwind.scss
- Remove ui.scss and integrate imports directly into component layer
- Add SCSS files to Tailwind content config to prevent CSS class purging
- Update Angular project configuration to remove ui.scss references
- Ensure Tailwind utilities can override component styles properly

Refs: #5195
2025-06-30 23:01:58 +02:00
Lorenz Hilpert
8d063428fc Merge branch 'refactor/convert-buildable-libs-to-non-buildable' into develop
Resolved conflicts:
- .gitignore: Added .claude to ignored files
- nx.json: Kept HEAD version with extra eslint.config.js exclusion
- package.json: Merged dependencies, updated vitest to v3.1.1 for compatibility
- eslint config files: Fixed merge conflicts and accepted conversion from .mjs to .js
- Removed deleted files from refactor branch
- Regenerated package-lock.json with --legacy-peer-deps

Build and tests pass successfully.
2025-06-30 20:52:05 +02:00
Lorenz Hilpert
06b0c6264a chore: add .claude to .gitignore 2025-06-30 20:13:33 +02:00
Lorenz Hilpert
4fe633e973 chore: update package dependencies and remove unused shared imports in tsconfig 2025-06-30 20:13:19 +02:00
Lorenz Hilpert
2463a803ea Merged PR 1876: Fix Workspace Build Issues 2025-06-30 09:17:03 +00:00
Lorenz Hilpert
1663dcec73 test(search-item-to-remit-dialog): enhance unit tests for component behavior and signal integration 2025-06-30 11:00:00 +02:00
Lorenz Hilpert
827aa565c5 feat(tests): update test command to include tuiAutoExit and add unit tests for SearchItemToRemitDialogComponent 2025-06-27 17:34:13 +02:00
Lorenz Hilpert
39fc4ce1ce refactor(styles): update styles to use Tailwind CSS and clean up code 2025-06-27 16:45:47 +02:00
Lorenz Hilpert
4f4b072e25 refactor(sass): migrate @import to @use syntax
- Replace deprecated @import with modern @use in _components.scss
- Replace deprecated @import with modern @use in tailwind.scss
- Move @use statements before @tailwind directives per Sass requirements
- Eliminates all 5 Sass deprecation warnings from build
- Future-proofs codebase for Dart Sass 3.0.0
2025-06-27 16:42:49 +02:00
Lorenz Hilpert
9af4a72a76 fix: resolve build warnings and improve code quality
- Remove unused Angular component and pipe imports to eliminate TS-998113 warnings
- Fix Sass mixed declarations warnings by reordering CSS properties
- Remove empty ngOnInit method from preview component
- Clean up unused imports across customer search and OMS components
- Move animation/transition properties above nested rules in SCSS files

Reduces build warnings significantly and improves code maintainability.
2025-06-27 16:19:18 +02:00
Lorenz Hilpert
7a44101e90 refactor: convert buildable libraries to non-buildable and migrate eslint configs
- Convert eslint.config.mjs files to eslint.config.js format across workspace
- Remove build targets from remission libraries (data-access, feature/remission-list, helpers, shared)
- Remove build target from icons library
- Delete ng-package.json and tsconfig.lib.prod.json files from buildable libraries
- Update tsconfig.lib.json configurations to remove bundler moduleResolution
- Clean up build artifacts and simplify library configurations
- Libraries now compile on-demand during application build instead of pre-compilation
2025-06-27 15:44:34 +02:00
Nino Righi
6fee35c756 Merged PR 1872: fix(isa-app-moment-locale): correct locale initialization for date formatting
fix(isa-app-moment-locale): correct locale initialization for date formatting

Ensures proper setup of moment.js locale in the ISA app to provide accurate date and time formatting for users. Addresses issues with incorrect or inconsistent locale application.

Ref: #5188
2025-06-25 08:35:43 +00:00
Nino Righi
c15077aa86 Merged PR 1870: fix(oms-return-search): fix display and logic issues in return search results
fix(oms-return-search): fix display and logic issues in return search results

Resolve display inconsistencies and correct logic in the return search result component to improve user experience and maintain alignment with design and business requirements.

Ref: #5009
2025-06-23 21:23:27 +00:00
Nino Righi
f051a97e53 Merged PR 1871: fix(ui-dropdown): improve dropdown usability and conditional rendering
fix(ui-dropdown): improve dropdown usability and conditional rendering

Refines the logic for displaying quantity and product category dropdowns in the return details order group item controls. Ensures dropdowns are only shown when appropriate and maintains accessibility and user experience.

Ref: #5189
2025-06-23 15:32:56 +00:00
Nino Righi
1b26a44a37 Merged PR 1869: fix(oms-task-list-item): address styling and layout issues in return task lis...
fix(oms-task-list-item): address styling and layout issues in return task list item

Improves SCSS for the return task list item component to ensure consistent appearance and resolve layout inconsistencies. Enhances maintainability and visual alignment with design standards.

Ref: #5191
2025-06-23 15:25:34 +00:00
Nino Righi
80b2508708 Merged PR 1868: fix(oms-return-search): resolve issues in return search result item rendering
fix(oms-return-search): resolve issues in return search result item rendering

Corrects rendering logic and improves template structure for the return search result item component. Ensures compliance with Angular control flow best practices and enhances maintainability.

Ref: #5190
2025-06-23 15:24:26 +00:00
Nino
e9affd2359 fix(return-details): Small Layout Fix, Refs: #5171 2025-06-17 16:52:03 +02:00
Nino
8f8b9153b0 Merge branch 'develop' into release/4.0 2025-06-17 16:45:37 +02:00
Lorenz Hilpert
9a4121e2bf fix(return-details): correct storage key retrieval in ReturnDetailsStore 2025-06-16 10:53:58 +02:00
Lorenz Hilpert
50b7f21394 Merge branch 'develop' into release/4.0 2025-06-12 21:12:08 +02:00
Lorenz Hilpert
a67375557d Merge branch 'develop' into release/4.0 2025-06-02 11:41:54 +02:00
Lorenz Hilpert
6e7c56fcb9 style(errors): standardize quotation marks in error exports 2025-05-28 21:32:41 +02:00
Lorenz Hilpert
05e257b922 Merge branch 'develop' into release/4.0 2025-05-13 18:52:00 +02:00
Lorenz Hilpert
d7d61915fa Merge branch 'develop' into release/4.0 2025-05-13 18:36:09 +02:00
Nino
d0220b6246 Merge branch 'develop' into release/4.0 2025-05-09 17:26:47 +02:00
Nino
32336ba5b4 Update index file return data-access 2025-05-09 12:13:25 +02:00
Nino
1f26d5285b Merge branch 'develop' into release/4.0 2025-05-09 12:12:19 +02:00
Michael Auer
be0bff0535 Cherry Pick: PR 1824: ISA-Frontend - Expliziter Pfad für Traefik IngressRoute
(cherry picked from commit c9b2762bbc)
2025-02-28 09:36:06 +01:00
Lorenz Hilpert
cb7391e66f Update version numbers in azure-pipelines.yml to 4.0 2025-02-10 10:43:23 +01:00
299 changed files with 17307 additions and 11023 deletions

View File

@@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"mcp__context7__resolve-library-id",
"mcp__context7__get-library-docs"
],
"deny": []
}
}

View File

@@ -4,7 +4,7 @@ applyTo: '**'
// This file is automatically generated by Nx Console
You are in an nx workspace using Nx 21.2.0 and npm as the package manager.
You are in an nx workspace using Nx 21.2.1 and npm as the package manager.
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
@@ -37,4 +37,11 @@ If the user wants help with tasks or commands (which include keywords like "test
- If the task was marked as "continuous" do not offer to rerun the task. This task is already running and the user can see the output in the terminal. You can use 'nx_current_running_task_output' to get the output of the task to verify the output.
# CI Error Guidelines
If the user wants help with fixing an error in their CI pipeline, use the following flow:
- Retrieve the list of current CI Pipeline Executions (CIPEs) using the 'nx_cloud_cipe_details' tool
- If there are any errors, use the 'nx_cloud_fix_cipe_failure' tool to retrieve the logs for a specific task
- Use the task logs to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
- Make sure that the problem is fixed by running the task that you passed into the 'nx_cloud_fix_cipe_failure' tool

145
.gitignore vendored
View File

@@ -1,70 +1,75 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
.matomo
junit.xml
# compiled output
/dist
/tmp
/out-tsc
/
# dependencies
/node_modules
# profiling files
chrome-profiler-events.json
speed-measure-plugin.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
/testresults
/libpeerconnection.log
npm-debug.log
yarn-error.log
yarn.lock
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
libs/swagger/src/lib/*
*storybook.log
.nx/cache
.nx/workspace-data
.angular
storybook-static
.cursor\rules\nx-rules.mdc
.github\instructions\nx.instructions.md
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
vite.config.*.timestamp*
# See http://help.github.com/ignore-files/ for more about ignoring files.
.matomo
junit.xml
# compiled output
/dist
/tmp
/out-tsc
/
# dependencies
/node_modules
# profiling files
chrome-profiler-events.json
speed-measure-plugin.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
/testresults
/libpeerconnection.log
npm-debug.log
yarn-error.log
yarn.lock
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
libs/swagger/src/lib/*
*storybook.log
.nx/cache
.nx/workspace-data
.angular
.claude
storybook-static
.cursor\rules\nx-rules.mdc
.github\instructions\nx.instructions.md
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
vite.config.*.timestamp*
vitest.config.*.timestamp*
.mcp.json
.memory.json

View File

@@ -1,7 +1,7 @@
{
"*.ts": "npx eslint --fix --config eslint.config.mjs",
"*.tsx": "npx eslint --fix --config eslint.config.mjs",
"*.js": "npx eslint --fix --config eslint.config.mjs",
"*.jsx": "npx eslint --fix --config eslint.config.mjs",
"*.html": "npx eslint --fix --config eslint.config.mjs"
}
{
"*.ts": "npx eslint --fix --config eslint.config.js",
"*.tsx": "npx eslint --fix --config eslint.config.js",
"*.js": "npx eslint --fix --config eslint.config.js",
"*.jsx": "npx eslint --fix --config eslint.config.js",
"*.html": "npx eslint --fix --config eslint.config.js"
}

View File

@@ -1,55 +1,55 @@
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: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/no-unused-expressions': 'warn',
'prefer-const': 'warn',
'@angular-eslint/contextual-lifecycle': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@angular-eslint/no-empty-lifecycle-method': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'@angular-eslint/component-selector': 'warn',
'@angular-eslint/prefer-standalone': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'no-empty-function': 'warn',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@angular-eslint/directive-selector': 'warn',
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {
'@angular-eslint/template/elements-content': 'warn',
'@angular-eslint/template/no-autofocus': 'warn',
},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/no-unused-expressions': 'warn',
'prefer-const': 'warn',
'@angular-eslint/contextual-lifecycle': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@angular-eslint/no-empty-lifecycle-method': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'@angular-eslint/component-selector': 'warn',
'@angular-eslint/prefer-standalone': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'no-empty-function': 'warn',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@angular-eslint/directive-selector': 'warn',
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {
'@angular-eslint/template/elements-content': 'warn',
'@angular-eslint/template/no-autofocus': 'warn',
},
},
];

View File

@@ -1,162 +1,162 @@
{
"name": "isa-app",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/isa-app/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"options": {
"allowedCommonJsDependencies": [
"lodash",
"moment",
"jsrsasign",
"pdfjs-dist/build/pdf",
"pdfjs-dist/web/pdf_viewer",
"pdfjs-dist/es5/build/pdf",
"pdfjs-dist/es5/web/pdf_viewer"
],
"outputPath": "dist/isa-app",
"index": "apps/isa-app/src/index.html",
"browser": "apps/isa-app/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/isa-app/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/isa-app/src/favicon.ico",
"apps/isa-app/src/assets",
"apps/isa-app/src/config",
"apps/isa-app/src/silent-refresh.html",
"apps/isa-app/src/manifest.webmanifest",
{
"glob": "**/*",
"input": "node_modules/scandit-web-datacapture-barcode/build/engine",
"output": "scandit"
}
],
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/ui.scss",
"apps/isa-app/src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "25kb"
}
],
"fileReplacements": [
{
"replace": "apps/isa-app/src/environments/environment.ts",
"with": "apps/isa-app/src/environments/environment.prod.ts"
}
],
"outputHashing": "all",
"serviceWorker": "apps/isa-app/ngsw-config.json"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production",
"outputs": ["{options.outputPath}"]
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "isa-app:build:production"
},
"development": {
"buildTarget": "isa-app:build:development"
}
},
"defaultConfiguration": "development",
"continuous": true
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "isa-app:build"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/isa-app/jest.config.ts"
}
},
"serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "isa-app:build",
"staticFilePath": "dist/apps/isa-app/browser",
"spa": true
}
},
"storybook": {
"executor": "@storybook/angular:start-storybook",
"options": {
"port": 4400,
"configDir": "apps/isa-app/.storybook",
"browserTarget": "isa-app:build",
"compodoc": false,
"open": false,
"assets": [
{
"glob": "**/*",
"input": "apps/isa-app/src/assets",
"output": "/assets"
}
],
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/ui.scss",
"apps/isa-app/src/styles.scss"
]
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@storybook/angular:build-storybook",
"outputs": ["{options.outputDir}"],
"options": {
"outputDir": "dist/storybook/isa-app",
"configDir": "apps/isa-app/.storybook",
"browserTarget": "isa-app:build",
"compodoc": false,
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/ui.scss",
"apps/isa-app/src/styles.scss"
]
},
"configurations": {
"ci": {
"quiet": true
}
}
}
}
}
{
"name": "isa-app",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/isa-app/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"options": {
"allowedCommonJsDependencies": [
"lodash",
"moment",
"jsrsasign",
"pdfjs-dist/build/pdf",
"pdfjs-dist/web/pdf_viewer",
"pdfjs-dist/es5/build/pdf",
"pdfjs-dist/es5/web/pdf_viewer"
],
"outputPath": "dist/isa-app",
"index": "apps/isa-app/src/index.html",
"browser": "apps/isa-app/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/isa-app/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/isa-app/src/favicon.ico",
"apps/isa-app/src/assets",
"apps/isa-app/src/config",
"apps/isa-app/src/silent-refresh.html",
"apps/isa-app/src/manifest.webmanifest",
{
"glob": "**/*",
"input": "node_modules/scandit-web-datacapture-barcode/build/engine",
"output": "scandit"
}
],
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/tailwind.scss",
"apps/isa-app/src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "25kb"
}
],
"fileReplacements": [
{
"replace": "apps/isa-app/src/environments/environment.ts",
"with": "apps/isa-app/src/environments/environment.prod.ts"
}
],
"outputHashing": "all",
"serviceWorker": "apps/isa-app/ngsw-config.json"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production",
"outputs": ["{options.outputPath}"]
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "isa-app:build:production"
},
"development": {
"buildTarget": "isa-app:build:development"
}
},
"defaultConfiguration": "development",
"continuous": true
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "isa-app:build"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/isa-app/jest.config.ts"
}
},
"serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "isa-app:build",
"staticFilePath": "dist/apps/isa-app/browser",
"spa": true
}
},
"storybook": {
"executor": "@storybook/angular:start-storybook",
"options": {
"port": 4400,
"configDir": "apps/isa-app/.storybook",
"browserTarget": "isa-app:build",
"compodoc": false,
"open": false,
"assets": [
{
"glob": "**/*",
"input": "apps/isa-app/src/assets",
"output": "/assets"
}
],
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/tailwind.scss",
"apps/isa-app/src/styles.scss"
]
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@storybook/angular:build-storybook",
"outputs": ["{options.outputDir}"],
"options": {
"outputDir": "dist/storybook/isa-app",
"configDir": "apps/isa-app/.storybook",
"browserTarget": "isa-app:build",
"compodoc": false,
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/tailwind.scss",
"apps/isa-app/src/styles.scss"
]
},
"configurations": {
"ci": {
"quiet": true
}
}
}
}
}

View File

@@ -1,214 +1,231 @@
import { isDevMode, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {
CanActivateCartGuard,
CanActivateCartWithProcessIdGuard,
CanActivateCustomerGuard,
CanActivateCustomerOrdersGuard,
CanActivateCustomerOrdersWithProcessIdGuard,
CanActivateCustomerWithProcessIdGuard,
CanActivateGoodsInGuard,
CanActivateProductGuard,
CanActivateProductWithProcessIdGuard,
CanActivateRemissionGuard,
CanActivateTaskCalendarGuard,
IsAuthenticatedGuard,
} from './guards';
import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
import { MainComponent } from './main.component';
import { PreviewComponent } from './preview';
import {
BranchSectionResolver,
CustomerSectionResolver,
ProcessIdResolver,
} from './resolvers';
import { TokenLoginComponent, TokenLoginModule } from './token-login';
import { ProcessIdGuard } from './guards/process-id.guard';
import {
ActivateProcessIdGuard,
ActivateProcessIdWithConfigKeyGuard,
} from './guards/activate-process-id.guard';
import { MatomoRouteData } from 'ngx-matomo-client';
import { tabResolverFn } from '@isa/core/tabs';
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
const routes: Routes = [
{ path: '', redirectTo: 'kunde/dashboard', pathMatch: 'full' },
{
path: 'login',
children: [
{ path: ':token', component: TokenLoginComponent },
{ path: '**', redirectTo: 'kunde', pathMatch: 'full' },
],
},
{
path: '',
canActivate: [IsAuthenticatedGuard],
children: [
{
path: 'kunde',
component: MainComponent,
children: [
{
path: 'dashboard',
loadChildren: () =>
import('@page/dashboard').then((m) => m.DashboardModule),
data: {
matomo: {
title: 'Dashboard',
} as MatomoRouteData,
},
},
{
path: 'product',
loadChildren: () =>
import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductGuard],
},
{
path: ':processId/product',
loadChildren: () =>
import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'order',
loadChildren: () =>
import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersGuard],
},
{
path: ':processId/order',
loadChildren: () =>
import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'customer',
loadChildren: () =>
import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () =>
import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'cart',
loadChildren: () =>
import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartGuard],
},
{
path: ':processId/cart',
loadChildren: () =>
import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartWithProcessIdGuard],
},
{
path: 'pickup-shelf',
canActivate: [ProcessIdGuard],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{
path: ':processId/pickup-shelf',
canActivate: [ActivateProcessIdGuard],
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
],
resolve: { section: CustomerSectionResolver },
},
{
path: 'filiale',
component: MainComponent,
children: [
{
path: 'task-calendar',
loadChildren: () =>
import('@page/task-calendar').then(
(m) => m.PageTaskCalendarModule,
),
canActivate: [CanActivateTaskCalendarGuard],
},
{
path: 'pickup-shelf',
canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfInModule),
},
{
path: 'goods/in',
loadChildren: () =>
import('@page/goods-in').then((m) => m.GoodsInModule),
canActivate: [CanActivateGoodsInGuard],
},
{
path: 'remission',
loadChildren: () =>
import('@page/remission').then((m) => m.PageRemissionModule),
canActivate: [CanActivateRemissionGuard],
},
{
path: 'package-inspection',
loadChildren: () =>
import('@page/package-inspection').then(
(m) => m.PackageInspectionModule,
),
canActivate: [CanActivatePackageInspectionGuard],
},
{
path: 'assortment',
loadChildren: () =>
import('@page/assortment').then((m) => m.AssortmentModule),
canActivate: [CanActivateAssortmentGuard],
},
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
],
resolve: { section: BranchSectionResolver },
},
],
},
{
path: ':tabId',
component: MainComponent,
resolve: { process: tabResolverFn, tab: tabResolverFn },
canActivate: [IsAuthenticatedGuard],
children: [
{
path: 'return',
loadChildren: () =>
import('@isa/oms/feature/return-search').then((m) => m.routes),
},
{
path: 'remission',
loadChildren: () =>
import('@isa/remission/feature/remission-list').then((m) => m.routes),
},
],
},
];
if (isDevMode()) {
routes.unshift({
path: 'preview',
component: PreviewComponent,
});
}
@NgModule({
imports: [RouterModule.forRoot(routes), TokenLoginModule],
exports: [RouterModule],
providers: [provideScrollPositionRestoration()],
})
export class AppRoutingModule {}
import { isDevMode, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {
CanActivateCartGuard,
CanActivateCartWithProcessIdGuard,
CanActivateCustomerGuard,
CanActivateCustomerOrdersGuard,
CanActivateCustomerOrdersWithProcessIdGuard,
CanActivateCustomerWithProcessIdGuard,
CanActivateGoodsInGuard,
CanActivateProductGuard,
CanActivateProductWithProcessIdGuard,
CanActivateRemissionGuard,
CanActivateTaskCalendarGuard,
IsAuthenticatedGuard,
} from './guards';
import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
import { MainComponent } from './main.component';
import { PreviewComponent } from './preview';
import {
BranchSectionResolver,
CustomerSectionResolver,
ProcessIdResolver,
} from './resolvers';
import { TokenLoginComponent, TokenLoginModule } from './token-login';
import { ProcessIdGuard } from './guards/process-id.guard';
import {
ActivateProcessIdGuard,
ActivateProcessIdWithConfigKeyGuard,
} from './guards/activate-process-id.guard';
import { MatomoRouteData } from 'ngx-matomo-client';
import { tabResolverFn } from '@isa/core/tabs';
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
const routes: Routes = [
{ path: '', redirectTo: 'kunde/dashboard', pathMatch: 'full' },
{
path: 'login',
children: [
{ path: ':token', component: TokenLoginComponent },
{ path: '**', redirectTo: 'kunde', pathMatch: 'full' },
],
},
{
path: '',
canActivate: [IsAuthenticatedGuard],
children: [
{
path: 'kunde',
component: MainComponent,
children: [
{
path: 'dashboard',
loadChildren: () =>
import('@page/dashboard').then((m) => m.DashboardModule),
data: {
matomo: {
title: 'Dashboard',
} as MatomoRouteData,
},
},
{
path: 'product',
loadChildren: () =>
import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductGuard],
},
{
path: ':processId/product',
loadChildren: () =>
import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'order',
loadChildren: () =>
import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersGuard],
},
{
path: ':processId/order',
loadChildren: () =>
import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'customer',
loadChildren: () =>
import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () =>
import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'cart',
loadChildren: () =>
import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartGuard],
},
{
path: ':processId/cart',
loadChildren: () =>
import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartWithProcessIdGuard],
},
{
path: 'pickup-shelf',
canActivate: [ProcessIdGuard],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{
path: ':processId/pickup-shelf',
canActivate: [ActivateProcessIdGuard],
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
],
resolve: { section: CustomerSectionResolver },
},
{
path: 'filiale',
component: MainComponent,
children: [
{
path: 'task-calendar',
loadChildren: () =>
import('@page/task-calendar').then(
(m) => m.PageTaskCalendarModule,
),
canActivate: [CanActivateTaskCalendarGuard],
},
{
path: 'pickup-shelf',
canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfInModule),
},
{
path: 'goods/in',
loadChildren: () =>
import('@page/goods-in').then((m) => m.GoodsInModule),
canActivate: [CanActivateGoodsInGuard],
},
{
path: 'remission',
loadChildren: () =>
import('@page/remission').then((m) => m.PageRemissionModule),
canActivate: [CanActivateRemissionGuard],
},
{
path: 'package-inspection',
loadChildren: () =>
import('@page/package-inspection').then(
(m) => m.PackageInspectionModule,
),
canActivate: [CanActivatePackageInspectionGuard],
},
{
path: 'assortment',
loadChildren: () =>
import('@page/assortment').then((m) => m.AssortmentModule),
canActivate: [CanActivateAssortmentGuard],
},
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
],
resolve: { section: BranchSectionResolver },
},
],
},
{
path: ':tabId',
component: MainComponent,
resolve: { process: tabResolverFn, tab: tabResolverFn },
canActivate: [IsAuthenticatedGuard],
children: [
{
path: 'return',
loadChildren: () =>
import('@isa/oms/feature/return-search').then((m) => m.routes),
},
{
path: 'remission',
children: [
{
path: 'return-receipt',
loadChildren: () =>
import(
'@isa/remission/feature/remission-return-receipt-list'
).then((m) => m.routes),
},
{
path: '',
loadChildren: () =>
import('@isa/remission/feature/remission-list').then(
(m) => m.routes,
),
},
],
},
],
},
];
if (isDevMode()) {
routes.unshift({
path: 'preview',
component: PreviewComponent,
});
}
@NgModule({
imports: [
RouterModule.forRoot(routes, { bindToComponentInputs: true }),
TokenLoginModule,
],
exports: [RouterModule],
providers: [provideScrollPositionRestoration()],
})
export class AppRoutingModule {}

View File

@@ -1,7 +1,6 @@
import { Platform, PlatformModule } from '@angular/cdk/platform';
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { Component } from '@angular/core';
import { BranchDTO } from '@generated/swagger/checkout-api';
import { BehaviorSubject } from 'rxjs';
@@ -9,9 +8,9 @@ import { BehaviorSubject } from 'rxjs';
selector: 'app-preview',
templateUrl: 'preview.component.html',
styleUrls: ['preview.component.css'],
imports: [CommonModule, BranchSelectorComponent, PlatformModule],
imports: [CommonModule, PlatformModule],
})
export class PreviewComponent implements OnInit {
export class PreviewComponent {
selectedBranch$ = new BehaviorSubject<BranchDTO>({});
get appVersion() {
@@ -24,7 +23,7 @@ export class PreviewComponent implements OnInit {
get navigator() {
const nav = {};
for (let i in window.navigator) nav[i] = navigator[i];
for (const i in window.navigator) nav[i] = navigator[i];
return nav;
}
@@ -51,8 +50,6 @@ export class PreviewComponent implements OnInit {
constructor(private readonly _platform: Platform) {}
ngOnInit() {}
setNewBranch(branch: BranchDTO) {
this.selectedBranch$.next(branch);
}

View File

@@ -1,5 +1,9 @@
import { Injectable } from '@angular/core';
import { ItemDTO, ListResponseArgsOfItemDTO, SearchService } from '@generated/swagger/cat-search-api';
import {
ItemDTO,
ListResponseArgsOfItemDTO,
SearchService,
} from '@generated/swagger/cat-search-api';
import {
RemiService,
StockService,
@@ -18,7 +22,11 @@ import { memorize } from '@utils/common';
import { Observable, of, throwError } from 'rxjs';
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
import { RemissionListItem } from './defs';
import { fromItemDto, mapFromReturnItemDTO, mapFromReturnSuggestionDTO } from './mappings';
import {
fromItemDto,
mapFromReturnItemDTO,
mapFromReturnSuggestionDTO,
} from './mappings';
import { Logger } from '@core/logger';
import { RemissionPlacementType } from '@domain/remission';
@@ -204,7 +212,10 @@ export class DomainRemissionService {
);
}
getStockInformation(items: RemissionListItem[], recalculate: boolean = false) {
getStockInformation(
items: RemissionListItem[],
recalculate: boolean = false,
) {
return this.getCurrentStock().pipe(
switchMap((stock) =>
this._stockService
@@ -218,7 +229,8 @@ export class DomainRemissionService {
map((res) => {
const o = items.map((item) => {
const stockInfo = res?.result?.find(
(stockInfo) => stockInfo.itemId === +item.dto.product.catalogProductNumber,
(stockInfo) =>
stockInfo.itemId === +item.dto.product.catalogProductNumber,
);
if (!stockInfo) {
@@ -231,7 +243,8 @@ export class DomainRemissionService {
return { ...item, ...defaultStockData };
}
const availableStock = stockInfo.inStock - stockInfo.removedFromStock;
const availableStock =
stockInfo.inStock - stockInfo.removedFromStock;
const inStock = availableStock < 0 ? 0 : availableStock;
let { remainingQuantity, remissionQuantity } = item;
@@ -249,7 +262,12 @@ export class DomainRemissionService {
}
}
return { ...item, remainingQuantity, remissionQuantity, inStock };
return {
...item,
remainingQuantity,
remissionQuantity,
inStock,
};
});
return o;
@@ -259,7 +277,10 @@ export class DomainRemissionService {
);
}
getRequiredCapacities(params: { departments?: string[]; supplierId: number }) {
getRequiredCapacities(params: {
departments?: string[];
supplierId: number;
}) {
return this.getCurrentStock().pipe(
switchMap((stock) =>
this._remiService
@@ -301,13 +322,18 @@ export class DomainRemissionService {
);
}
canAddReturnItem(item: ReturnItemDTO): Observable<BatchResponseArgsOfReturnItemDTOAndReturnItemDTO> {
canAddReturnItem(
item: ReturnItemDTO,
): Observable<BatchResponseArgsOfReturnItemDTOAndReturnItemDTO> {
return this._remiService.RemiCanAddReturnItem({
data: [item],
});
}
async createReturn(supplierId: number, returnGroup?: string): Promise<ReturnDTO> {
async createReturn(
supplierId: number,
returnGroup?: string,
): Promise<ReturnDTO> {
const response = await this._returnService
.ReturnCreateReturn({
data: {
@@ -343,7 +369,10 @@ export class DomainRemissionService {
.toPromise();
}
getReturns(params: { start?: Date; returncompleted: boolean }): Observable<ReturnDTO[]> {
getReturns(params: {
start?: Date;
returncompleted: boolean;
}): Observable<ReturnDTO[]> {
const queryToken: ReturnQueryTokenDTO = {
start: params.start?.toISOString(),
filter: {
@@ -360,13 +389,20 @@ export class DomainRemissionService {
});
return this.getCurrentStock().pipe(
switchMap((stock) => this._returnService.ReturnQueryReturns({ stockId: stock.id, queryToken })),
switchMap((stock) =>
this._returnService.ReturnQueryReturns({
stockId: stock.id,
queryToken,
}),
),
map((res) => res.result),
);
}
getReturn(returnId: number): Observable<ReturnDTO> {
return this._returnService.ReturnGetReturn({ returnId, eagerLoading: 3 }).pipe(map((res) => res.result));
return this._returnService
.ReturnGetReturn({ returnId, eagerLoading: 3 })
.pipe(map((res) => res.result));
}
async deleteReturn(returnId: number) {
@@ -393,7 +429,11 @@ export class DomainRemissionService {
inStock: number;
}) {
return this._returnService
.ReturnAddReturnItem({ returnId, receiptId, data: { returnItemId, quantity, placementType, inStock } })
.ReturnAddReturnItem({
returnId,
receiptId,
data: { returnItemId, quantity, placementType, inStock },
})
.pipe(map((r) => r.result));
}
@@ -420,7 +460,14 @@ export class DomainRemissionService {
.ReturnAddReturnSuggestion({
returnId,
receiptId,
data: { returnSuggestionId, quantity, placementType, inStock, impedimentComment, remainingQuantity },
data: {
returnSuggestionId,
quantity,
placementType,
inStock,
impedimentComment,
remainingQuantity,
},
})
.pipe(map((r) => r.result));
}
@@ -438,18 +485,28 @@ export class DomainRemissionService {
receiptId: number;
receiptItemId: number;
}) {
return this._returnService.ReturnRemoveReturnItem({ returnId, receiptItemId, receiptId });
return this._returnService.ReturnRemoveReturnItem({
returnId,
receiptItemId,
receiptId,
});
}
returnImpediment(itemId: number) {
return this._returnService
.ReturnReturnItemImpediment({ itemId, data: { comment: 'Produkt nicht gefunden' } })
.ReturnReturnItemImpediment({
itemId,
data: { comment: 'Produkt nicht gefunden' },
})
.pipe(map((r) => r.result));
}
returnSuggestion(itemId: number) {
return this._returnService
.ReturnReturnSuggestionImpediment({ itemId, data: { comment: 'Produkt nicht gefunden' } })
.ReturnReturnSuggestionImpediment({
itemId,
data: { comment: 'Produkt nicht gefunden' },
})
.pipe(map((r) => r.result));
}
@@ -459,7 +516,10 @@ export class DomainRemissionService {
* @param receiptNumber Receipt number
* @returns ReceiptDTO
*/
async createReceipt(returnDTO: ReturnDTO, receiptNumber?: string): Promise<ReceiptDTO> {
async createReceipt(
returnDTO: ReturnDTO,
receiptNumber?: string,
): Promise<ReceiptDTO> {
const stock = await this._getStock();
const response = await this._returnService
@@ -510,7 +570,10 @@ export class DomainRemissionService {
return receipt;
}
async completeReceipt(returnId: number, receiptId: number): Promise<ReceiptDTO> {
async completeReceipt(
returnId: number,
receiptId: number,
): Promise<ReceiptDTO> {
const res = await this._returnService
.ReturnFinalizeReceipt({
returnId,

View File

@@ -1,21 +1,22 @@
import { enableProdMode, isDevMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { CONFIG_DATA } from '@isa/core/config';
import { setDefaultOptions } from 'date-fns';
import { de } from 'date-fns/locale';
import * as moment from 'moment';
import { enableProdMode, isDevMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { CONFIG_DATA } from "@isa/core/config";
import { setDefaultOptions } from "date-fns";
import { de } from "date-fns/locale";
import * as moment from "moment";
import "moment/locale/de";
setDefaultOptions({ locale: de });
moment.locale('de');
moment.locale("de");
import { AppModule } from './app/app.module';
import { AppModule } from "./app/app.module";
if (!isDevMode()) {
enableProdMode();
}
async function bootstrap() {
const configRes = await fetch('/config/config.json');
const configRes = await fetch("/config/config.json");
const config = await configRes.json();

View File

@@ -7,7 +7,6 @@ import { UiSelectModule } from '@ui/select';
import { UiIconModule } from '@ui/icon';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { UiDateInputDirective } from '@ui/input';
import { validateCompanyOrPersonalInfoRequired } from '../../validators/gender-b2b-validator';
@Component({
@@ -23,7 +22,6 @@ import { validateCompanyOrPersonalInfoRequired } from '../../validators/gender-b
UiIconModule,
ReactiveFormsModule,
RouterLink,
UiDateInputDirective,
],
})
export class CustomerDataEditB2BComponent extends CustomerDataEditComponent {

View File

@@ -1,12 +1,11 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject } from '@angular/core';
import { CustomerSearchStore } from '../store';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { Subject, combineLatest, of } from 'rxjs';
import { catchError, map, share, switchMap } from 'rxjs/operators';
import { CrmCustomerService } from '@domain/crm';
import { KundenkarteComponent } from '../../components/kundenkarte';
import { AsyncPipe } from '@angular/common';
import { IconComponent } from '@shared/components/icon';
import { CustomerSearchNavigation } from '@shared/services/navigation';
import { BonusCardInfoDTO } from '@generated/swagger/crm-api';
import { CustomerMenuComponent } from '../../components/customer-menu';
@@ -17,7 +16,7 @@ import { CustomerMenuComponent } from '../../components/customer-menu';
styleUrls: ['kundenkarte-main-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-customer-kundenkarte-main-view' },
imports: [CustomerMenuComponent, KundenkarteComponent, AsyncPipe, IconComponent, RouterLink],
imports: [CustomerMenuComponent, KundenkarteComponent, AsyncPipe],
})
export class KundenkarteMainViewComponent implements OnInit, OnDestroy {
private _store = inject(CustomerSearchStore);

View File

@@ -19,7 +19,7 @@ import { AddressPipe } from '@shared/pipes/customer';
import { IconComponent } from '@shared/components/icon';
import { CustomerSearchNavigation } from '@shared/services/navigation';
import { CustomerOrderItemListItemComponent } from './order-item-list-item/order-item-list-item.component';
import { GroupByPipe, groupBy } from '@ui/common';
import { groupBy } from '@ui/common';
import { EnvironmentService } from '@core/environment';
@Component({
@@ -40,7 +40,6 @@ import { EnvironmentService } from '@core/environment';
RouterLink,
CustomerOrderItemListItemComponent,
NgFor,
GroupByPipe,
NgSwitch,
NgSwitchCase,
NgSwitchDefault,

View File

@@ -2,9 +2,7 @@ import { AsyncPipe, CurrencyPipe, DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, OnDestroy, OnInit, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ProductImagePipe } from '@cdn/product-image';
import { IconComponent } from '@shared/components/icon';
import { BranchNamePipe, ResolveBranchPipe } from '@shared/pipes/branch';
import { OrderItemProcessingStatusPipe, OrderProcessingStatusPipe } from '@shared/pipes/order';
import { OrderItemProcessingStatusPipe } from '@shared/pipes/order';
import { OrderItemDTO } from '@generated/swagger/oms-api';
import { BehaviorSubject, Subject, combineLatest } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
@@ -21,16 +19,12 @@ import { PaymentTypePipe } from '@shared/pipes/customer';
imports: [
AsyncPipe,
DatePipe,
OrderProcessingStatusPipe,
ProductImagePipe,
ResolveBranchPipe,
BranchNamePipe,
CurrencyPipe,
IconComponent,
RouterLink,
PaymentTypePipe,
OrderItemProcessingStatusPipe
],
],
})
export class CustomerOrderItemListItemComponent implements OnInit, OnDestroy {
private _activatedRoute = inject(ActivatedRoute);

View File

@@ -4,8 +4,6 @@ import { map } from 'rxjs/operators';
import { CustomerNamePipe } from '@shared/pipes/customer';
import { AsyncPipe } from '@angular/common';
import { ProductImagePipe } from '@cdn/product-image';
import { CustomerSearchNavigation } from '@shared/services/navigation';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { OrderItemProcessingStatusPipe } from '@shared/pipes/order';
@Component({
@@ -18,8 +16,6 @@ import { OrderItemProcessingStatusPipe } from '@shared/pipes/order';
CustomerNamePipe,
AsyncPipe,
ProductImagePipe,
RouterLink,
RouterLinkActive,
OrderItemProcessingStatusPipe
],
})

View File

@@ -1,6 +1,6 @@
import { AsyncPipe, CurrencyPipe, DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, inject } from '@angular/core';
import { OrderDestinationPipe, OrderProcessingStatusPipe } from '@shared/pipes/order';
import { OrderProcessingStatusPipe } from '@shared/pipes/order';
import { AddressPipe } from '@shared/pipes/customer';
import { OrderListItemDTO } from '@generated/swagger/oms-api';
@@ -19,7 +19,6 @@ import { RouterLink } from '@angular/router';
imports: [
DatePipe,
OrderProcessingStatusPipe,
OrderDestinationPipe,
CurrencyPipe,
AddressPipe,
AsyncPipe,

View File

@@ -4,8 +4,6 @@ import { Subject, combineLatest } from 'rxjs';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { AsyncPipe } from '@angular/common';
import { CustomerSearchNavigation } from '@shared/services/navigation';
import { RouterLink } from '@angular/router';
import { IconComponent } from '@shared/components/icon';
import { LoaderComponent } from '@shared/components/loader';
import { CustomerOrderListItemComponent } from './order-list-item/order-list-item.component';
import { CustomerMenuComponent } from '../../components/customer-menu';
@@ -19,8 +17,6 @@ import { CustomerMenuComponent } from '../../components/customer-menu';
imports: [
CustomerMenuComponent,
AsyncPipe,
RouterLink,
IconComponent,
LoaderComponent,
CustomerOrderListItemComponent
],

View File

@@ -1,4 +1,4 @@
import { CurrencyPipe, DatePipe } from '@angular/common';
import { DatePipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, Input, inject } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { NavigateOnClickDirective, ProductImageModule } from '@cdn/product-image';
@@ -25,7 +25,6 @@ import { MatomoModule } from 'ngx-matomo-client';
RouterLinkActive,
IconModule,
DatePipe,
CurrencyPipe,
ProductImageModule,
UiCommonModule,
PickupShelfProcessingStatusPipe,

View File

@@ -1 +1 @@
@import "./components/icon";
@use "./components/icon";

View File

@@ -1,303 +1,319 @@
<div class="side-menu-group">
<span class="side-menu-group-label">Kunden</span>
<nav class="side-menu-group-nav">
<a
class="side-menu-group-item"
(click)="closeSideMenu(); resetBranch(); focusSearchBox()"
[routerLink]="productRoutePath$ | async"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/product"
(isActiveChange)="focusSearchBox()"
>
<div class="side-menu-group-item-icon">
<shared-icon icon="import-contacts"></shared-icon>
</div>
<span class="side-menu-group-item-label">Artikelsuche</span>
</a>
<div class="side-menu-group-sub-item-wrapper">
@if (customerSearchRoute$ | async; as customerSearchRoute) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="customerSearchRoute.path"
[queryParams]="customerSearchRoute.queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer"
(isActiveChange)="customerActive($event); focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="person"></shared-icon>
</span>
<span class="side-menu-group-item-label">Kunden</span>
<button
class="side-menu-group-arrow"
[class.side-menu-item-rotate]="customerExpanded"
(click)="
$event.stopPropagation();
$event.preventDefault();
customerExpanded = !customerExpanded
"
>
<shared-icon icon="keyboard-arrow-down"></shared-icon>
</button>
</a>
}
<div class="side-menu-group-sub-items" [class.hidden]="!customerExpanded">
@if (customerSearchRoute$ | async; as customerSearchRoute) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="customerSearchRoute.path"
[queryParams]="customerSearchRoute.queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(search|search)"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Suchen</span>
</a>
}
@if (customerCreateRoute$ | async; as customerCreateRoute) {
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="customerCreateRoute.path"
[queryParams]="customerCreateRoute.queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(create|create)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Erfassen</span>
</a>
}
</div>
</div>
<a
*ifRole="'Store'"
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="pickUpShelfOutRoutePath$ | async"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/pickup-shelf"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="unarchive"></shared-icon>
</span>
<span class="side-menu-group-item-label">Warenausgabe</span>
</a>
<a
*ifRole="'Store'"
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
processService.activatedTab()?.id || processService.nextId(),
'return',
]"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
<ng-icon name="isaNavigationReturn"></ng-icon>
</span>
<span class="side-menu-group-item-label">Retoure</span>
</a>
<a
*ifRole="'CallCenter'"
class="side-menu-group-item"
(click)="closeSideMenu(); resetBranch(); focusSearchBox()"
[routerLink]="customerOrdersRoutePath$ | async"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/order"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="deployed-code"></shared-icon>
</span>
<span class="side-menu-group-item-label">Bestellungen</span>
</a>
</nav>
</div>
<div class="side-menu-group" *ifRole="'Store'">
<span class="side-menu-group-label">Filiale</span>
<nav class="side-menu-group-nav">
@if (taskCalenderNavigation$ | async; as taskCalenderNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="taskCalenderNavigation.path"
[queryParams]="taskCalenderNavigation.queryParams"
routerLinkActive="active"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="event-available"></shared-icon>
</span>
<span class="side-menu-group-item-label">Kalender</span>
</a>
}
@if (assortmentNavigation$ | async; as assortmentNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="assortmentNavigation.path"
[queryParams]="assortmentNavigation.queryParams"
routerLinkActive="active"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="shape-outline"></shared-icon>
</span>
<span class="side-menu-group-item-label">Sortiment</span>
</a>
}
<div class="side-menu-group-sub-item-wrapper">
@if (pickUpShelfInRoutePath$ | async; as pickUpShelfInNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="pickUpShelfInNavigation.path"
[queryParams]="pickUpShelfInNavigation.queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/filiale\/(pickup-shelf|goods\/in)"
(isActiveChange)="shelfActive($event); focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="isa-abholfach"></shared-icon>
</span>
<span class="side-menu-group-item-label">Abholfach</span>
<button
class="side-menu-group-arrow"
[class.side-menu-item-rotate]="shelfExpanded"
(click)="
$event.stopPropagation();
$event.preventDefault();
shelfExpanded = !shelfExpanded
"
>
<shared-icon icon="keyboard-arrow-down"></shared-icon>
</button>
</a>
}
<div class="side-menu-group-sub-items" [class.hidden]="!shelfExpanded">
@if (pickUpShelfInRoutePath$ | async; as pickUpShelfInListNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="pickUpShelfInListNavigation.path"
[queryParams]="pickUpShelfInListNavigation.queryParams"
[class.has-child-view]="currentShelfView$ | async"
sharedRegexRouterLinkActive="active"
[sharedRegexRouterLinkActiveTest]="'^\/filiale\/pickup-shelf'"
(isActiveChange)="shelfActive($event); focusSearchBox()"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Einbuchen</span>
</a>
}
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="['/filiale', 'goods', 'in', 'reservation']"
[queryParams]="{ view: 'reservation' }"
[class.active-child]="(currentShelfView$ | async) === 'reservation'"
routerLinkActive="active"
(isActiveChange)="shelfActive($event)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Reservierung</span>
</a>
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="['/filiale', 'goods', 'in', 'cleanup']"
[queryParams]="{ view: 'cleanup' }"
[class.active-child]="(currentShelfView$ | async) === 'cleanup'"
routerLinkActive="active"
(isActiveChange)="shelfActive($event)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Ausräumen</span>
</a>
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="['/filiale', 'goods', 'in', 'preview']"
[queryParams]="{ view: 'remission' }"
[class.active-child]="(currentShelfView$ | async) === 'remission'"
routerLinkActive="active"
(isActiveChange)="shelfActive($event)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Remi-Vorschau</span>
</a>
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="['/filiale', 'goods', 'in', 'list']"
[queryParams]="{ view: 'wareneingangsliste' }"
[class.active-child]="
(currentShelfView$ | async) === 'wareneingangsliste'
"
routerLinkActive="active"
(isActiveChange)="shelfActive($event)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Fehlende</span>
</a>
</div>
</div>
@if (remissionNavigation$ | async; as remissionNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="remissionNavigation.path"
[queryParams]="remissionNavigation.queryParams"
routerLinkActive="active"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="assignment-return"></shared-icon>
</span>
<span class="side-menu-group-item-label">Remission</span>
</a>
}
@if (packageInspectionNavigation$ | async; as packageInspectionNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); fetchAndOpenPackages()"
[routerLink]="packageInspectionNavigation.path"
[queryParams]="packageInspectionNavigation.queryParams"
routerLinkActive="active"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="clipboard-check-outline"></shared-icon>
</span>
<span class="side-menu-group-item-label">Wareneingang</span>
</a>
}
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
processService.activatedTab()?.id || processService.nextId(),
'remission',
]"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
<ng-icon name="isaNavigationRemission2"></ng-icon>
</span>
<span class="side-menu-group-item-label">Remission</span>
</a>
</nav>
</div>
<div class="side-menu-group">
<span class="side-menu-group-label">Kunden</span>
<nav class="side-menu-group-nav">
<a
class="side-menu-group-item"
(click)="closeSideMenu(); resetBranch(); focusSearchBox()"
[routerLink]="productRoutePath$ | async"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/product"
(isActiveChange)="focusSearchBox()"
>
<div class="side-menu-group-item-icon">
<shared-icon icon="import-contacts"></shared-icon>
</div>
<span class="side-menu-group-item-label">Artikelsuche</span>
</a>
<div class="side-menu-group-sub-item-wrapper">
@if (customerSearchRoute$ | async; as customerSearchRoute) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="customerSearchRoute.path"
[queryParams]="customerSearchRoute.queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer"
(isActiveChange)="customerActive($event); focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="person"></shared-icon>
</span>
<span class="side-menu-group-item-label">Kunden</span>
<button
class="side-menu-group-arrow"
[class.side-menu-item-rotate]="customerExpanded"
(click)="
$event.stopPropagation();
$event.preventDefault();
customerExpanded = !customerExpanded
"
>
<shared-icon icon="keyboard-arrow-down"></shared-icon>
</button>
</a>
}
<div class="side-menu-group-sub-items" [class.hidden]="!customerExpanded">
@if (customerSearchRoute$ | async; as customerSearchRoute) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="customerSearchRoute.path"
[queryParams]="customerSearchRoute.queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(search|search)"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Suchen</span>
</a>
}
@if (customerCreateRoute$ | async; as customerCreateRoute) {
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="customerCreateRoute.path"
[queryParams]="customerCreateRoute.queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(create|create)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Erfassen</span>
</a>
}
</div>
</div>
<a
*ifRole="'Store'"
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="pickUpShelfOutRoutePath$ | async"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/pickup-shelf"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="unarchive"></shared-icon>
</span>
<span class="side-menu-group-item-label">Warenausgabe</span>
</a>
<a
*ifRole="'Store'"
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
processService.activatedTab()?.id || processService.nextId(),
'return',
]"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
<ng-icon name="isaNavigationReturn"></ng-icon>
</span>
<span class="side-menu-group-item-label">Retoure</span>
</a>
<a
*ifRole="'CallCenter'"
class="side-menu-group-item"
(click)="closeSideMenu(); resetBranch(); focusSearchBox()"
[routerLink]="customerOrdersRoutePath$ | async"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/order"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="deployed-code"></shared-icon>
</span>
<span class="side-menu-group-item-label">Bestellungen</span>
</a>
</nav>
</div>
<div class="side-menu-group" *ifRole="'Store'">
<span class="side-menu-group-label">Filiale</span>
<nav class="side-menu-group-nav">
@if (taskCalenderNavigation$ | async; as taskCalenderNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="taskCalenderNavigation.path"
[queryParams]="taskCalenderNavigation.queryParams"
routerLinkActive="active"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="event-available"></shared-icon>
</span>
<span class="side-menu-group-item-label">Kalender</span>
</a>
}
@if (assortmentNavigation$ | async; as assortmentNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="assortmentNavigation.path"
[queryParams]="assortmentNavigation.queryParams"
routerLinkActive="active"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="shape-outline"></shared-icon>
</span>
<span class="side-menu-group-item-label">Sortiment</span>
</a>
}
<div class="side-menu-group-sub-item-wrapper">
@if (pickUpShelfInRoutePath$ | async; as pickUpShelfInNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="pickUpShelfInNavigation.path"
[queryParams]="pickUpShelfInNavigation.queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/filiale\/(pickup-shelf|goods\/in)"
(isActiveChange)="shelfActive($event); focusSearchBox()"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="isa-abholfach"></shared-icon>
</span>
<span class="side-menu-group-item-label">Abholfach</span>
<button
class="side-menu-group-arrow"
[class.side-menu-item-rotate]="shelfExpanded"
(click)="
$event.stopPropagation();
$event.preventDefault();
shelfExpanded = !shelfExpanded
"
>
<shared-icon icon="keyboard-arrow-down"></shared-icon>
</button>
</a>
}
<div class="side-menu-group-sub-items" [class.hidden]="!shelfExpanded">
@if (pickUpShelfInRoutePath$ | async; as pickUpShelfInListNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="pickUpShelfInListNavigation.path"
[queryParams]="pickUpShelfInListNavigation.queryParams"
[class.has-child-view]="currentShelfView$ | async"
sharedRegexRouterLinkActive="active"
[sharedRegexRouterLinkActiveTest]="'^\/filiale\/pickup-shelf'"
(isActiveChange)="shelfActive($event); focusSearchBox()"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Einbuchen</span>
</a>
}
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="['/filiale', 'goods', 'in', 'reservation']"
[queryParams]="{ view: 'reservation' }"
[class.active-child]="(currentShelfView$ | async) === 'reservation'"
routerLinkActive="active"
(isActiveChange)="shelfActive($event)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Reservierung</span>
</a>
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="['/filiale', 'goods', 'in', 'cleanup']"
[queryParams]="{ view: 'cleanup' }"
[class.active-child]="(currentShelfView$ | async) === 'cleanup'"
routerLinkActive="active"
(isActiveChange)="shelfActive($event)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Ausräumen</span>
</a>
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="['/filiale', 'goods', 'in', 'preview']"
[queryParams]="{ view: 'remission' }"
[class.active-child]="(currentShelfView$ | async) === 'remission'"
routerLinkActive="active"
(isActiveChange)="shelfActive($event)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Remi-Vorschau</span>
</a>
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="['/filiale', 'goods', 'in', 'list']"
[queryParams]="{ view: 'wareneingangsliste' }"
[class.active-child]="
(currentShelfView$ | async) === 'wareneingangsliste'
"
routerLinkActive="active"
(isActiveChange)="shelfActive($event)"
>
<span class="side-menu-group-item-icon"></span>
<span class="side-menu-group-item-label">Fehlende</span>
</a>
</div>
</div>
@if (remissionNavigation$ | async; as remissionNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="remissionNavigation.path"
[queryParams]="remissionNavigation.queryParams"
routerLinkActive="active"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="assignment-return"></shared-icon>
</span>
<span class="side-menu-group-item-label">Remission</span>
</a>
}
@if (packageInspectionNavigation$ | async; as packageInspectionNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); fetchAndOpenPackages()"
[routerLink]="packageInspectionNavigation.path"
[queryParams]="packageInspectionNavigation.queryParams"
routerLinkActive="active"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="clipboard-check-outline"></shared-icon>
</span>
<span class="side-menu-group-item-label">Wareneingang</span>
</a>
}
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
processService.activatedTab()?.id || processService.nextId(),
'remission',
]"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
<ng-icon name="isaNavigationRemission2"></ng-icon>
</span>
<span class="side-menu-group-item-label">Remission</span>
</a>
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
processService.activatedTab()?.id || processService.nextId(),
'remission',
'return-receipt',
]"
(isActiveChange)="focusSearchBox()"
>
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
<ng-icon name="isaNavigationRemission2"></ng-icon>
</span>
<span class="side-menu-group-item-label">Warenbegleitscheine</span>
</a>
</nav>
</div>

View File

@@ -1,97 +1,22 @@
/* You can add global styles to this file, and also import other style files */
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "./scss/components";
/* Scanner Fullscreen Styles */
.full-screen-scanner {
max-width: 100vw !important;
max-height: 100vh !important;
width: 100vw !important;
height: 100vh !important;
.scanner-component {
width: 100%;
height: 100%;
}
}
/* Override CDK overlay container styles for scanner */
.cdk-overlay-container {
.full-screen-scanner {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
}
}
@import "./scss/root";
@import "./scss/customer";
@import "./scss/branch";
@layer base {
body {
@apply bg-background;
}
::-webkit-scrollbar {
width: 0; // remove scrollbar space
height: 0;
background: transparent; // optional: just make scrollbar invisible */
}
.desktop .scroll-bar::-webkit-scrollbar {
@apply w-3;
background-color: transparent;
}
.desktop .scroll-bar::-webkit-scrollbar-track {
// @apply my-4;
-webkit-box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.1);
border-radius: 10px;
background-color: white;
}
.desktop .scroll-bar::-webkit-scrollbar-thumb {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1);
border-radius: 10px;
background-color: var(--scrollbar-color);
}
}
@keyframes load {
0% {
opacity: 0;
}
30% {
opacity: 0.5;
}
100% {
opacity: 0;
}
}
.skeleton {
@apply block bg-gray-300 h-6;
animation: load 1s ease-in-out infinite;
}
@layer components {
.input-control {
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;
}
// .input-control:focus,
// .input-control:not(:placeholder-shown) {
// @apply bg-white;
// }
.input-control.ng-touched.ng-invalid {
@apply border-brand;
}
}
.full-screen-scanner {
max-width: 100vw !important;
max-height: 100vh !important;
width: 100vw !important;
height: 100vh !important;
.scanner-component {
width: 100%;
height: 100%;
}
}
.cdk-overlay-container {
.full-screen-scanner {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
}
}

View File

@@ -0,0 +1,75 @@
@use "./scss/components";
@use "./scss/root";
@use "./scss/customer";
@use "./scss/branch";
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
@import "../../../libs/ui/buttons/src/buttons.scss";
@import "../../../libs/ui/bullet-list/src/bullet-list.scss";
@import "../../../libs/ui/datepicker/src/datepicker.scss";
@import "../../../libs/ui/dialog/src/dialog.scss";
@import "../../../libs/ui/input-controls/src/input-controls.scss";
@import "../../../libs/ui/menu/src/menu.scss";
@import "../../../libs/ui/progress-bar/src/lib/progress-bar.scss";
@import "../../../libs/ui/search-bar/src/search-bar.scss";
@import "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
@import "../../../libs/ui/tooltip/src/tooltip.scss";
.input-control {
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;
}
.input-control.ng-touched.ng-invalid {
@apply border-brand;
}
@keyframes load {
0% {
opacity: 0;
}
30% {
opacity: 0.5;
}
100% {
opacity: 0;
}
}
.skeleton {
@apply block bg-gray-300 h-6;
animation: load 1s ease-in-out infinite;
}
}
@layer base {
body {
@apply bg-background;
}
::-webkit-scrollbar {
width: 0;
height: 0;
background: transparent;
}
.desktop .scroll-bar::-webkit-scrollbar {
@apply w-3;
background-color: transparent;
}
.desktop .scroll-bar::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.1);
border-radius: 10px;
background-color: white;
}
.desktop .scroll-bar::-webkit-scrollbar-thumb {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1);
border-radius: 10px;
background-color: var(--scrollbar-color);
}
}

View File

@@ -1,9 +0,0 @@
@use "../../../libs/ui/buttons/src/buttons.scss";
@use "../../../libs/ui/datepicker/src/datepicker.scss";
@use "../../../libs/ui/dialog/src/dialog.scss";
@use "../../../libs/ui/input-controls/src/input-controls.scss";
@use "../../../libs/ui/menu/src/menu.scss";
@use "../../../libs/ui/progress-bar/src/lib/progress-bar.scss";
@use "../../../libs/ui/search-bar/src/search-bar.scss";
@use "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
@use "../../../libs/ui/tooltip/src/tooltip.scss";

View File

@@ -34,6 +34,7 @@
bottom: 30px !important;
right: 12px;
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
transition: all 100ms linear;
@screen desktop {
margin-left: 600px;
@@ -44,7 +45,6 @@
box-shadow: none;
}
transition: all 100ms linear;
&.up {
transform: rotate(-90deg);
}

View File

@@ -20,24 +20,42 @@ const meta: Meta<ProductStockInfoComponent> = {
}),
],
args: {
stock: 100,
stockToRemit: 20,
stock: 92,
removedFromStock: 0,
predefinedReturnQuantity: 4,
remainingQuantityInStock: 0,
zob: 0,
},
argTypes: {
stock: {
control: { type: 'number' },
description: 'The current stock of the product.',
defaultValue: undefined,
defaultValue: 0,
},
stockToRemit: {
removedFromStock: {
control: { type: 'number' },
description: 'The amount of stock to remit.',
defaultValue: undefined,
description: 'The amount of stock that has been removed.',
defaultValue: 0,
},
predefinedReturnQuantity: {
control: { type: 'number' },
description: 'The predefined return quantity for the product.',
defaultValue: 0,
},
remainingQuantityInStock: {
control: { type: 'number' },
description: 'The remaining quantity in stock after returns.',
defaultValue: 0,
},
zob: {
control: { type: 'number' },
description: 'Min Stock Category Management Information.',
defaultValue: 0,
},
},
render: (args) => ({
props: args,
template: `<remi-product-stock-info ${argsToTemplate({ info: args })}></remi-product-stock-info>`,
template: `<remi-product-stock-info ${argsToTemplate(args)}></remi-product-stock-info>`,
}),
};
@@ -46,5 +64,11 @@ export default meta;
type Story = StoryObj<ProductStockInfoComponent>;
export const Default: Story = {
args: {},
args: {
stock: 92,
removedFromStock: 0,
predefinedReturnQuantity: 4,
remainingQuantityInStock: 0,
zob: 0,
},
};

View File

@@ -31,7 +31,7 @@ const meta: Meta<ClientRowComponent> = {
</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-label>Beleg-Nr.</ui-item-row-data-label>
<ui-item-row-data-value>
<span class="isa-text-body-2-bold">
1234567890

View File

@@ -2,7 +2,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": []
"types": [],
"moduleResolution": "bundler"
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"],

View File

@@ -9,10 +9,10 @@ trigger:
variables:
# Major Version einstellen
- name: 'Major'
value: '3'
value: '4'
# Minor Version einstellen
- name: 'Minor'
value: '4'
value: '0'
- name: 'Patch'
value: "$[counter(format('{0}.{1}', variables['Major'], variables['Minor']),0)]"
- name: 'BuildUniqueID'

View File

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,11 @@ module.exports = [
...nx.configs['flat/typescript'],
...nx.configs['flat/javascript'],
{
ignores: ['**/dist'],
ignores: [
'**/dist',
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
],
},
// {
// files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],

View File

@@ -1,58 +0,0 @@
import nx from '@nx/eslint-plugin';
import eslintConfigPrettier from 'eslint-config-prettier/flat';
import jsoncEslintParser from 'jsonc-eslint-parser';
export default [
...nx.configs['flat/base'],
...nx.configs['flat/typescript'],
...nx.configs['flat/javascript'],
{
ignores: ['**/dist', '**/generated'],
},
// Bis Module Boundaries gelöst sind, wird das Plugin nicht verwendet
// {
// files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
// rules: {
// '@nx/enforce-module-boundaries': [
// 'error',
// {
// enforceBuildableLibDependency: true,
// allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'],
// depConstraints: [
// {
// sourceTag: '*',
// onlyDependOnLibsWithTags: ['*'],
// },
// ],
// },
// ],
// },
// },
{
files: [
'**/*.ts',
'**/*.tsx',
'**/*.js',
'**/*.jsx',
'**/*.cjs',
'**/*.mjs',
],
// Override or add rules here
rules: {},
},
{
files: ['**/*.json'],
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
},
],
},
languageOptions: {
parser: jsoncEslintParser,
},
},
eslintConfigPrettier,
];

View File

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -1,7 +1,7 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../../eslint.config.mjs';
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
export default [
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
@@ -31,4 +31,10 @@ export default [
// Override or add rules here
rules: {},
},
];
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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,34 +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: {},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'common',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'common',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -0,0 +1,277 @@
# Common Decorators Library
A collection of TypeScript decorators for common cross-cutting concerns in Angular applications.
## Installation
This library is already configured in the project's `tsconfig.base.json`. Import decorators using:
```typescript
import { InFlight, InFlightWithKey, InFlightWithCache } from '@isa/common/decorators';
```
## Available Decorators
### 🚀 InFlight Decorators
Prevent multiple simultaneous calls to the same async method. All concurrent calls receive the same Promise result.
#### Basic Usage
```typescript
import { InFlight } from '@isa/common/decorators';
@Injectable()
class DataService {
@InFlight()
async fetchData(): Promise<Data> {
// Even if called multiple times simultaneously,
// only one API call will be made
return await this.http.get<Data>('/api/data').toPromise();
}
}
```
**Benefits:**
- Prevents duplicate API calls
- Reduces server load
- Improves application performance
- All callers receive the same result
### 🔑 InFlightWithKey
Prevents duplicate calls while considering method arguments. Each unique set of arguments gets its own in-flight tracking.
```typescript
import { InFlightWithKey } from '@isa/common/decorators';
@Injectable()
class UserService {
@InFlightWithKey({
keyGenerator: (userId: string) => userId
})
async fetchUser(userId: string): Promise<User> {
// Multiple calls with same userId share the same request
// Different userIds can execute simultaneously
return await this.http.get<User>(`/api/users/${userId}`).toPromise();
}
@InFlightWithKey() // Uses JSON.stringify by default
async searchUsers(query: string, page: number): Promise<User[]> {
return await this.http.get<User[]>(`/api/users/search`, {
params: { query, page: page.toString() }
}).toPromise();
}
}
```
**Configuration Options:**
- `keyGenerator?: (...args) => string` - Custom key generation function
- If not provided, uses `JSON.stringify(args)` as the key
### 🗄️ InFlightWithCache
Combines in-flight request deduplication with result caching.
```typescript
import { InFlightWithCache } from '@isa/common/decorators';
@Injectable()
class ProductService {
@InFlightWithCache({
cacheTime: 5 * 60 * 1000, // Cache for 5 minutes
keyGenerator: (productId: string) => productId
})
async getProduct(productId: string): Promise<Product> {
// Results are cached for 5 minutes
// Multiple calls within cache time return cached result
return await this.http.get<Product>(`/api/products/${productId}`).toPromise();
}
}
```
**Configuration Options:**
- `cacheTime?: number` - Cache duration in milliseconds
- `keyGenerator?: (...args) => string` - Custom key generation function
## How It Works
### Memory Management
All decorators use `WeakMap` for memory efficiency:
- Automatic garbage collection when instances are destroyed
- No memory leaks
- Per-instance state isolation
### Error Handling
- Failed requests are not cached
- In-flight tracking is cleaned up on both success and error
- All concurrent callers receive the same error
### Thread Safety
- Decorators are instance-aware
- Each service instance has its own in-flight tracking
- No shared state between instances
## Real-World Examples
### Solving Your Original Problem
```typescript
// Before: Multiple simultaneous calls
@Injectable({ providedIn: 'root' })
export class RemissionProductGroupService {
async fetchProductGroups(): Promise<KeyValueStringAndString[]> {
// Multiple calls = multiple API requests
return await this.apiCall();
}
}
// After: Using InFlight decorator
@Injectable({ providedIn: 'root' })
export class RemissionProductGroupService {
@InFlight()
async fetchProductGroups(): Promise<KeyValueStringAndString[]> {
// Multiple simultaneous calls = single API request
return await this.apiCall();
}
}
```
### Advanced Scenarios
```typescript
@Injectable()
class OrderService {
// Different cache times for different data types
@InFlightWithCache({ cacheTime: 30 * 1000 }) // 30 seconds
async getOrderStatus(orderId: string): Promise<OrderStatus> {
return await this.http.get<OrderStatus>(`/api/orders/${orderId}/status`).toPromise();
}
@InFlightWithCache({ cacheTime: 10 * 60 * 1000 }) // 10 minutes
async getOrderHistory(customerId: string): Promise<Order[]> {
return await this.http.get<Order[]>(`/api/customers/${customerId}/orders`).toPromise();
}
// Custom key generation for complex parameters
@InFlightWithKey({
keyGenerator: (filter: OrderFilter) =>
`${filter.status}-${filter.dateFrom}-${filter.dateTo}`
})
async searchOrders(filter: OrderFilter): Promise<Order[]> {
return await this.http.post<Order[]>('/api/orders/search', filter).toPromise();
}
}
```
## Best Practices
### ✅ Do
- Use `@InFlight()` for simple methods without parameters
- Use `@InFlightWithKey()` for methods with parameters
- Use `@InFlightWithCache()` for expensive operations with stable results
- Provide custom `keyGenerator` for complex parameter objects
- Set appropriate cache times based on data volatility
### ❌ Don't
- Use on methods that return different results for the same input
- Use excessively long cache times for dynamic data
- Use on methods that have side effects (POST, PUT, DELETE)
- Rely on argument order for default key generation
## Performance Considerations
### Memory Usage
- `InFlight`: Minimal memory overhead (one Promise per instance)
- `InFlightWithKey`: Memory usage scales with unique parameter combinations
- `InFlightWithCache`: Additional memory for cached results
### Cleanup
- In-flight requests are automatically cleaned up on completion
- Cache entries are cleaned up on expiry
- WeakMap ensures instances can be garbage collected
## Testing
The decorators are fully tested with comprehensive unit tests. Key test scenarios include:
- Multiple simultaneous calls deduplication
- Error handling and cleanup
- Cache expiration
- Instance isolation
- Key generation
Run tests with:
```bash
npx nx test common-decorators
```
## Migration Guide
### From Manual Implementation
```typescript
// Before: Manual in-flight tracking
class MyService {
private inFlight: Promise<Data> | null = null;
async fetchData(): Promise<Data> {
if (this.inFlight) {
return this.inFlight;
}
this.inFlight = this.doFetch();
try {
return await this.inFlight;
} finally {
this.inFlight = null;
}
}
}
// After: Using decorator
class MyService {
@InFlight()
async fetchData(): Promise<Data> {
return await this.doFetch();
}
}
```
### From RxJS shareReplay
```typescript
// Before: RxJS approach
class MyService {
private data$ = this.http.get<Data>('/api/data').pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
getData(): Observable<Data> {
return this.data$;
}
}
// After: Promise-based with decorator
class MyService {
@InFlightWithCache({ cacheTime: 5 * 60 * 1000 })
async getData(): Promise<Data> {
return await this.http.get<Data>('/api/data').toPromise();
}
}
```
## Contributing
When adding new decorators:
1. Add implementation in `src/lib/`
2. Include comprehensive unit tests
3. Update this documentation
4. Export from `src/index.ts`

View File

@@ -1,7 +1,7 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../../eslint.config.mjs';
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
export default [
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],

View File

@@ -0,0 +1,20 @@
{
"name": "common-decorators",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/common/decorators/src",
"prefix": "common",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../../coverage/libs/common/decorators"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -0,0 +1 @@
export * from './lib/in-flight.decorator';

View File

@@ -0,0 +1,321 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { InFlight, InFlightWithKey, InFlightWithCache } from './in-flight.decorator';
describe('InFlight Decorators', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllTimers();
});
describe('InFlight', () => {
class TestService {
callCount = 0;
@InFlight()
async fetchData(delay = 100): Promise<string> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, delay));
return `result-${this.callCount}`;
}
@InFlight()
async fetchWithError(delay = 100): Promise<string> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, delay));
throw new Error('Test error');
}
}
it('should prevent multiple simultaneous calls', async () => {
const service = new TestService();
// Make three simultaneous calls
const promise1 = service.fetchData();
const promise2 = service.fetchData();
const promise3 = service.fetchData();
// Advance timers to complete the async operation
await vi.runAllTimersAsync();
// All promises should resolve to the same value
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
expect(result1).toBe('result-1');
expect(result2).toBe('result-1');
expect(result3).toBe('result-1');
expect(service.callCount).toBe(1);
});
it('should allow subsequent calls after completion', async () => {
const service = new TestService();
// First call
const promise1 = service.fetchData();
await vi.runAllTimersAsync();
const result1 = await promise1;
expect(result1).toBe('result-1');
// Second call after first completes
const promise2 = service.fetchData();
await vi.runAllTimersAsync();
const result2 = await promise2;
expect(result2).toBe('result-2');
expect(service.callCount).toBe(2);
});
it('should handle errors properly', async () => {
const service = new TestService();
// Make multiple calls that will error
const promise1 = service.fetchWithError();
const promise2 = service.fetchWithError();
await vi.runAllTimersAsync();
// Both should reject with the same error
await expect(promise1).rejects.toThrow('Test error');
await expect(promise2).rejects.toThrow('Test error');
expect(service.callCount).toBe(1);
// Should allow new call after error
const promise3 = service.fetchWithError();
await vi.runAllTimersAsync();
await expect(promise3).rejects.toThrow('Test error');
expect(service.callCount).toBe(2);
});
it('should maintain separate state per instance', async () => {
const service1 = new TestService();
const service2 = new TestService();
// Make simultaneous calls on different instances
const promise1 = service1.fetchData();
const promise2 = service2.fetchData();
await vi.runAllTimersAsync();
const [result1, result2] = await Promise.all([promise1, promise2]);
// Each instance should have made its own call
expect(result1).toBe('result-1');
expect(result2).toBe('result-1');
expect(service1.callCount).toBe(1);
expect(service2.callCount).toBe(1);
});
});
describe('InFlightWithKey', () => {
class UserService {
callCounts = new Map<string, number>();
@InFlightWithKey({
keyGenerator: (userId: string) => userId
})
async fetchUser(userId: string, delay = 100): Promise<{ id: string; name: string }> {
const count = (this.callCounts.get(userId) || 0) + 1;
this.callCounts.set(userId, count);
await new Promise(resolve => setTimeout(resolve, delay));
return { id: userId, name: `User ${userId} - Call ${count}` };
}
@InFlightWithKey()
async fetchWithDefaultKey(param1: string, param2: number): Promise<string> {
const key = `${param1}-${param2}`;
const count = (this.callCounts.get(key) || 0) + 1;
this.callCounts.set(key, count);
await new Promise(resolve => setTimeout(resolve, 100));
return `Result ${count}`;
}
}
it('should deduplicate calls with same key', async () => {
const service = new UserService();
// Multiple calls with same userId
const promise1 = service.fetchUser('user1');
const promise2 = service.fetchUser('user1');
const promise3 = service.fetchUser('user1');
await vi.runAllTimersAsync();
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
expect(result1).toEqual({ id: 'user1', name: 'User user1 - Call 1' });
expect(result2).toEqual(result1);
expect(result3).toEqual(result1);
expect(service.callCounts.get('user1')).toBe(1);
});
it('should allow simultaneous calls with different keys', async () => {
const service = new UserService();
// Calls with different userIds
const promise1 = service.fetchUser('user1');
const promise2 = service.fetchUser('user2');
const promise3 = service.fetchUser('user1'); // Duplicate of first
await vi.runAllTimersAsync();
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
expect(result1).toEqual({ id: 'user1', name: 'User user1 - Call 1' });
expect(result2).toEqual({ id: 'user2', name: 'User user2 - Call 1' });
expect(result3).toEqual(result1); // Same as first call
expect(service.callCounts.get('user1')).toBe(1);
expect(service.callCounts.get('user2')).toBe(1);
});
it('should use JSON.stringify as default key generator', async () => {
const service = new UserService();
// Multiple calls with same arguments
const promise1 = service.fetchWithDefaultKey('test', 123);
const promise2 = service.fetchWithDefaultKey('test', 123);
// Different arguments
const promise3 = service.fetchWithDefaultKey('test', 456);
await vi.runAllTimersAsync();
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
expect(result1).toBe('Result 1');
expect(result2).toBe('Result 1'); // Same as first
expect(result3).toBe('Result 1'); // Different key, separate call
expect(service.callCounts.get('test-123')).toBe(1);
expect(service.callCounts.get('test-456')).toBe(1);
});
});
describe('InFlightWithCache', () => {
class DataService {
callCount = 0;
@InFlightWithCache({
cacheTime: 1000, // 1 second cache
keyGenerator: (query: string) => query
})
async search(query: string): Promise<string[]> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, 100));
return [`result-${query}-${this.callCount}`];
}
@InFlightWithCache({
cacheTime: 500
})
async fetchWithExpiry(id: number): Promise<string> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, 50));
return `data-${id}-${this.callCount}`;
}
}
it('should cache results for specified time', async () => {
const service = new DataService();
// First call
const promise1 = service.search('test');
await vi.runAllTimersAsync();
const result1 = await promise1;
expect(result1).toEqual(['result-test-1']);
expect(service.callCount).toBe(1);
// Second call within cache time - should return cached result
const result2 = await service.search('test');
expect(result2).toEqual(['result-test-1']);
expect(service.callCount).toBe(1); // No new call
// Advance time past cache expiry
vi.advanceTimersByTime(1100);
// Third call after cache expiry - should make new call
const promise3 = service.search('test');
await vi.runAllTimersAsync();
const result3 = await promise3;
expect(result3).toEqual(['result-test-2']);
expect(service.callCount).toBe(2);
});
it('should handle in-flight deduplication with caching', async () => {
const service = new DataService();
// Multiple simultaneous calls
const promise1 = service.search('query1');
const promise2 = service.search('query1');
const promise3 = service.search('query1');
await vi.runAllTimersAsync();
const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]);
// All should get same result
expect(result1).toEqual(['result-query1-1']);
expect(result2).toEqual(result1);
expect(result3).toEqual(result1);
expect(service.callCount).toBe(1);
// Subsequent call should use cache
const result4 = await service.search('query1');
expect(result4).toEqual(['result-query1-1']);
expect(service.callCount).toBe(1);
});
it('should clean up expired cache entries', async () => {
const service = new DataService();
// Make a call
const promise1 = service.fetchWithExpiry(1);
await vi.runAllTimersAsync();
await promise1;
// Advance time past cache expiry
vi.advanceTimersByTime(600);
// Make another call - should not use expired cache
service.callCount = 0; // Reset for clarity
const promise2 = service.fetchWithExpiry(1);
await vi.runAllTimersAsync();
const result2 = await promise2;
expect(result2).toBe('data-1-1');
expect(service.callCount).toBe(1); // New call was made
});
it('should handle errors without caching them', async () => {
class ErrorService {
callCount = 0;
@InFlightWithCache({ cacheTime: 1000 })
async fetchWithError(): Promise<string> {
this.callCount++;
await new Promise(resolve => setTimeout(resolve, 50));
throw new Error('API Error');
}
}
const service = new ErrorService();
// First call that errors
const promise1 = service.fetchWithError();
await vi.runAllTimersAsync();
await expect(promise1).rejects.toThrow('API Error');
expect(service.callCount).toBe(1);
// Second call should not use cache (errors aren't cached)
const promise2 = service.fetchWithError();
await vi.runAllTimersAsync();
await expect(promise2).rejects.toThrow('API Error');
expect(service.callCount).toBe(2);
});
});
});

View File

@@ -0,0 +1,251 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Decorator that prevents multiple simultaneous calls to the same async method.
* All concurrent calls will receive the same Promise result.
*
* @example
* ```typescript
* class MyService {
* @InFlight()
* async fetchData(): Promise<Data> {
* // This method will only execute once even if called multiple times simultaneously
* return await api.getData();
* }
* }
* ```
*/
export function InFlight<
T extends (...args: any[]) => Promise<any>,
>(): MethodDecorator {
const inFlightMap = new WeakMap<object, Promise<any>>();
return function (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (
this: any,
...args: Parameters<T>
): Promise<ReturnType<T>> {
// Check if there's already an in-flight request for this instance
const existingRequest = inFlightMap.get(this);
if (existingRequest) {
return existingRequest;
}
// Create new request and store it
const promise = originalMethod
.apply(this, args)
.then((result: any) => {
// Clean up after successful completion
inFlightMap.delete(this);
return result;
})
.catch((error: any) => {
// Clean up after error
inFlightMap.delete(this);
throw error;
});
inFlightMap.set(this, promise);
return promise;
};
return descriptor;
};
}
/**
* Decorator that prevents multiple simultaneous calls to the same async method
* while considering method arguments. Each unique set of arguments gets its own
* in-flight tracking.
*
* @param options Configuration options for the decorator
* @example
* ```typescript
* class UserService {
* @InFlightWithKey({
* keyGenerator: (userId: string) => userId
* })
* async fetchUser(userId: string): Promise<User> {
* // Calls with different userIds can execute simultaneously
* // Calls with the same userId will share the same promise
* return await api.getUser(userId);
* }
* }
* ```
*/
export interface InFlightWithKeyOptions<T extends (...args: any[]) => any> {
/**
* Generate a cache key from the method arguments.
* If not provided, uses JSON.stringify on all arguments.
*/
keyGenerator?: (...args: Parameters<T>) => string;
}
export function InFlightWithKey<T extends (...args: any[]) => Promise<any>>(
options: InFlightWithKeyOptions<T> = {},
): MethodDecorator {
const inFlightMap = new WeakMap<object, Map<string, Promise<any>>>();
return function (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (
this: any,
...args: Parameters<T>
): Promise<ReturnType<T>> {
// Initialize map for this instance if needed
if (!inFlightMap.has(this)) {
inFlightMap.set(this, new Map());
}
const instanceMap = inFlightMap.get(this)!;
// Generate cache key
const key = options.keyGenerator
? options.keyGenerator(...args)
: JSON.stringify(args);
// Check if there's already an in-flight request for this key
const existingRequest = instanceMap.get(key);
if (existingRequest) {
return existingRequest;
}
// Create new request and store it
const promise = originalMethod
.apply(this, args)
.then((result: any) => {
// Clean up after successful completion
instanceMap.delete(key);
return result;
})
.catch((error: any) => {
// Clean up after error
instanceMap.delete(key);
throw error;
});
instanceMap.set(key, promise);
return promise;
};
return descriptor;
};
}
/**
* Decorator that prevents multiple simultaneous calls to the same async method
* with additional caching capabilities.
*
* @param options Configuration options for the decorator
* @example
* ```typescript
* class DataService {
* @InFlightWithCache({
* cacheTime: 5 * 60 * 1000, // Cache for 5 minutes
* keyGenerator: (params: QueryParams) => params.query
* })
* async searchData(params: QueryParams): Promise<SearchResult> {
* return await api.search(params);
* }
* }
* ```
*/
export interface InFlightWithCacheOptions<T extends (...args: any[]) => any> {
/**
* Generate a cache key from the method arguments.
* If not provided, uses JSON.stringify on all arguments.
*/
keyGenerator?: (...args: Parameters<T>) => string;
/**
* Time in milliseconds to keep the result cached after completion.
* If not provided, result is not cached after completion.
*/
cacheTime?: number;
}
export function InFlightWithCache<T extends (...args: any[]) => Promise<any>>(
options: InFlightWithCacheOptions<T> = {},
): MethodDecorator {
const inFlightMap = new WeakMap<object, Map<string, Promise<any>>>();
const cacheMap = new WeakMap<
object,
Map<string, { result: any; expiry: number }>
>();
return function (
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (
this: any,
...args: Parameters<T>
): Promise<ReturnType<T>> {
// Initialize maps for this instance if needed
if (!inFlightMap.has(this)) {
inFlightMap.set(this, new Map());
cacheMap.set(this, new Map());
}
const instanceInFlight = inFlightMap.get(this)!;
const instanceCache = cacheMap.get(this)!;
// Generate cache key
const key = options.keyGenerator
? options.keyGenerator(...args)
: JSON.stringify(args);
// Check cache first (if cacheTime is set)
if (options.cacheTime) {
const cached = instanceCache.get(key);
if (cached && cached.expiry > Date.now()) {
return Promise.resolve(cached.result);
}
// Clean up expired cache entry
if (cached) {
instanceCache.delete(key);
}
}
// Check if there's already an in-flight request
const existingRequest = instanceInFlight.get(key);
if (existingRequest) {
return existingRequest;
}
// Create new request
const promise = originalMethod
.apply(this, args)
.then((result: any) => {
// Cache result if cacheTime is set
if (options.cacheTime) {
instanceCache.set(key, {
result,
expiry: Date.now() + options.cacheTime,
});
}
return result;
})
.finally(() => {
// Always clean up in-flight request
instanceInFlight.delete(key);
});
instanceInFlight.set(key, promise);
return promise;
};
return descriptor;
};
}

View File

@@ -0,0 +1,13 @@
import '@angular/compiler';
import '@analogjs/vitest-angular/setup-zone';
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing';
import { getTestBed } from '@angular/core/testing';
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting(),
);

View File

@@ -1,12 +1,21 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "es2022",
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"moduleResolution": "bundler",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"include": [],
@@ -17,12 +26,5 @@
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
]
}

View File

@@ -0,0 +1,27 @@
{
"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",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,29 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"files": ["src/test-setup.ts"]
}

View File

@@ -0,0 +1,27 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-plugin-angular';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/common/decorators',
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
setupFiles: ['src/test-setup.ts'],
reporters: ['default'],
coverage: {
reportsDirectory: '../../../coverage/libs/common/decorators',
provider: 'v8' as const,
},
},
}));

View File

@@ -1,34 +1,40 @@
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: 'commonPrint',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'common-print',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'commonPrint',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'common-print',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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,34 +1,40 @@
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: 'core',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'core',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'core',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'core',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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,7 +1,7 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../../eslint.config.mjs';
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
export default [
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
@@ -31,4 +31,10 @@ export default [
// Override or add rules here
rules: {},
},
];
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -1,34 +0,0 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../../eslint.config.js';
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,81 +1,86 @@
import {
patchState,
signalStore,
withComputed,
withHooks,
withMethods,
withState,
} from '@ngrx/signals';
import {
addEntities,
addEntity,
removeEntity,
updateEntity,
withEntities,
} from '@ngrx/signals/entities';
import { Tab } from './tab';
import { z } from 'zod';
import { AddTabSchema, PatchTabSchema } from './tab.schemas';
import { computed, effect } from '@angular/core';
export const TabService = signalStore(
{ providedIn: 'root' },
withState<{ activatedTabId: number | null }>({
activatedTabId: null,
}),
withEntities<Tab>(),
withComputed((store) => ({
nextId: computed(
() => Math.max(0, ...store.entities().map((e) => e.id)) + 1,
),
activatedTab: computed<Tab | null>(() => {
const activeTabId = store.activatedTabId();
if (activeTabId === null) {
return null;
}
return store.entities().find((e) => e.id === activeTabId) ?? null;
}),
})),
withMethods((store) => ({
addTab(add: z.infer<typeof AddTabSchema>) {
const parsed = AddTabSchema.parse(add);
const tab: Tab = {
name: parsed.name,
id: store.nextId(),
createdAt: Date.now(),
tags: parsed.tags,
};
patchState(store, addEntity(tab));
return tab;
},
activateTab(id: number) {
patchState(store, {
...updateEntity({ id, changes: { activatedAt: Date.now() } }),
activatedTabId: id,
});
},
patchTab(id: number, changes: z.infer<typeof PatchTabSchema>) {
patchState(
store,
updateEntity({ id, changes: PatchTabSchema.parse(changes) }),
);
},
removeTab(id: number) {
patchState(store, removeEntity(id));
},
})),
withHooks((store) => ({
onInit() {
const entitiesStr = localStorage.getItem('TabEntities');
if (entitiesStr) {
const entities = JSON.parse(entitiesStr);
patchState(store, addEntities(entities));
}
effect(() => {
const state = store.entities();
localStorage.setItem('TabEntities', JSON.stringify(state));
});
},
})),
);
import {
patchState,
signalStore,
withComputed,
withHooks,
withMethods,
withState,
} from '@ngrx/signals';
import {
addEntities,
addEntity,
removeEntity,
updateEntity,
withEntities,
} from '@ngrx/signals/entities';
import { Tab } from './tab';
import { z } from 'zod';
import { AddTabSchema, PatchTabSchema } from './tab.schemas';
import { computed, effect } from '@angular/core';
export const TabService = signalStore(
{ providedIn: 'root' },
withState<{ activatedTabId: number | null }>({
activatedTabId: null,
}),
withEntities<Tab>(),
withComputed((store) => ({
nextId: computed(
() => Math.max(0, ...store.entities().map((e) => e.id)) + 1,
),
activatedTab: computed<Tab | null>(() => {
const activeTabId = store.activatedTabId();
if (activeTabId === null) {
return null;
}
return store.entities().find((e) => e.id === activeTabId) ?? null;
}),
})),
withMethods((store) => ({
addTab(add: z.infer<typeof AddTabSchema>) {
const parsed = AddTabSchema.parse(add);
const tab: Tab = {
name: parsed.name,
id: store.nextId(),
createdAt: Date.now(),
tags: parsed.tags,
metadata: {},
navigation: {
current: 0,
locations: [],
},
};
patchState(store, addEntity(tab));
return tab;
},
activateTab(id: number) {
patchState(store, {
...updateEntity({ id, changes: { activatedAt: Date.now() } }),
activatedTabId: id,
});
},
patchTab(id: number, changes: z.infer<typeof PatchTabSchema>) {
patchState(
store,
updateEntity({ id, changes: PatchTabSchema.parse(changes) }),
);
},
removeTab(id: number) {
patchState(store, removeEntity(id));
},
})),
withHooks((store) => ({
onInit() {
const entitiesStr = localStorage.getItem('TabEntities');
if (entitiesStr) {
const entities = JSON.parse(entitiesStr);
patchState(store, addEntities(entities));
}
effect(() => {
const state = store.entities();
localStorage.setItem('TabEntities', JSON.stringify(state));
});
},
})),
);

View File

@@ -1,7 +1,25 @@
export interface Tab {
id: number;
name: string;
tags: string[];
createdAt: number;
activatedAt?: number;
}
export interface Tab {
id: number;
name: string;
navigation: TabNavigation;
createdAt: number;
activatedAt?: number;
metadata: TabMetadata;
/** @deprecated */
tags: string[];
}
export interface TabNavigation {
current: number;
locations: TabLocation[];
}
export interface TabLocation {
timestamp: number;
title: string;
url: string;
}
export interface TabMetadata {
[key: string]: unknown;
}

View File

@@ -1,19 +1,19 @@
import baseConfig from '../../eslint.config.mjs'
export default [
...baseConfig,
{
files: ['**/*.json'],
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
},
],
},
languageOptions: {
parser: await import('jsonc-eslint-parser'),
},
},
]
const baseConfig = require('../../eslint.config.js');
module.exports = [
...baseConfig,
{
files: ['**/*.json'],
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
},
],
},
languageOptions: {
parser: require('jsonc-eslint-parser'),
},
},
];

View File

@@ -5,16 +5,6 @@
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/icons",
"main": "libs/icons/src/index.ts",
"tsConfig": "libs/icons/tsconfig.lib.json",
"assets": ["libs/icons/*.md"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],

View File

@@ -0,0 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...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: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

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,27 +1,51 @@
import { DataAccessError } from '@isa/common/data-access';
import { Receipt, ReceiptItem } from '../../models';
import { DataAccessError } from "@isa/common/data-access";
import { Receipt, ReceiptItem } from "../../models";
import {
CreateReturnProcessError,
CreateReturnProcessErrorReason,
CreateReturnProcessErrorMessages,
} from './create-return-process.error';
} from "./create-return-process.error";
import { ProductCategory } from "../../questions";
describe('CreateReturnProcessError', () => {
describe("CreateReturnProcessError", () => {
const params = {
processId: 123,
returns: [
{
receipt: { id: 321 } as Receipt,
items: [] as ReceiptItem[],
items: [
// Provide at least one valid item object, or an empty array if testing "no items"
// For NO_RETURNABLE_ITEMS, an empty array is valid, but must match the expected shape
// So, keep as [], but type is now correct
],
},
],
};
it('should create an error instance with NO_RETURNABLE_ITEMS reason', () => {
// For tests that require items, use the correct shape:
const validParams = {
processId: 123,
returns: [
{
receipt: { id: 321 } as Receipt,
items: [
{
receiptItem: { id: 111 } as ReceiptItem,
quantity: 1,
category: "A" as ProductCategory,
},
],
},
],
};
it("should create an error instance with NO_RETURNABLE_ITEMS reason", () => {
// Arrange, Act
const error = new CreateReturnProcessError(
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS,
params,
);
// Assert
expect(error).toBeInstanceOf(CreateReturnProcessError);
expect(error).toBeInstanceOf(DataAccessError);
expect(error.reason).toBe(
@@ -33,25 +57,103 @@ describe('CreateReturnProcessError', () => {
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS
],
);
expect(error.code).toBe('CREATE_RETURN_PROCESS');
expect(error.code).toBe("CREATE_RETURN_PROCESS");
});
it('should create an error instance with MISMATCH_RETURNABLE_ITEMS reason', () => {
it("should create an error instance with MISMATCH_RETURNABLE_ITEMS reason", () => {
// Arrange, Act
const error = new CreateReturnProcessError(
CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS,
params,
validParams,
);
// Assert
expect(error).toBeInstanceOf(CreateReturnProcessError);
expect(error).toBeInstanceOf(DataAccessError);
expect(error.reason).toBe(
CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS,
);
expect(error.params).toEqual(params);
expect(error.params).toEqual(validParams);
expect(error.message).toBe(
CreateReturnProcessErrorMessages[
CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS
],
);
expect(error.code).toBe('CREATE_RETURN_PROCESS');
expect(error.code).toBe("CREATE_RETURN_PROCESS");
});
it("should expose the correct params structure", () => {
const error = new CreateReturnProcessError(
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS,
params,
);
expect(error.params).toHaveProperty("processId", 123);
expect(error.params).toHaveProperty("returns");
expect(Array.isArray(error.params.returns)).toBe(true);
expect(error.params.returns[0]).toHaveProperty("receipt");
expect(error.params.returns[0]).toHaveProperty("items");
});
it("should throw and be catchable as CreateReturnProcessError", () => {
try {
throw new CreateReturnProcessError(
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS,
params,
);
} catch (err) {
expect(err).toBeInstanceOf(CreateReturnProcessError);
expect((err as CreateReturnProcessError).reason).toBe(
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS,
);
}
});
it("should use the correct message for each reason", () => {
Object.values(CreateReturnProcessErrorReason).forEach((reason) => {
const error = new CreateReturnProcessError(reason, params);
expect(error.message).toBe(CreateReturnProcessErrorMessages[reason]);
});
});
it('should have code "CREATE_RETURN_PROCESS" for all reasons', () => {
Object.values(CreateReturnProcessErrorReason).forEach((reason) => {
const error = new CreateReturnProcessError(reason, params);
expect(error.code).toBe("CREATE_RETURN_PROCESS");
});
});
it("should support params with multiple returns and items", () => {
const extendedParams = {
processId: 999,
returns: [
{
receipt: { id: 1 } as Receipt,
items: [
{
receiptItem: { id: 10 } as ReceiptItem,
quantity: 2,
category: "A" as ProductCategory,
},
],
},
{
receipt: { id: 2 } as Receipt,
items: [
{
receiptItem: { id: 20 } as ReceiptItem,
quantity: 1,
category: "B" as ProductCategory,
},
],
},
],
};
const error = new CreateReturnProcessError(
CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS,
extendedParams,
);
expect(error.params.processId).toBe(999);
expect(error.params.returns.length).toBe(2);
expect(error.params.returns[0].items[0].quantity).toBe(2);
expect(error.params.returns[1].items[0].category).toBe("B");
});
});

View File

@@ -1,13 +1,14 @@
import { DataAccessError } from '@isa/common/data-access';
import { Receipt, ReceiptItem } from '../../models';
import { DataAccessError } from "@isa/common/data-access";
import { Receipt, ReceiptItem } from "../../models";
import { ProductCategory } from "../../questions";
/**
* Enum-like object defining possible reasons for return process creation failures.
* Used to provide consistent and type-safe error categorization.
*/
export const CreateReturnProcessErrorReason = {
NO_RETURNABLE_ITEMS: 'NO_RETURNABLE_ITEMS',
MISMATCH_RETURNABLE_ITEMS: 'MISMATCH_RETURNABLE_ITEMS',
NO_RETURNABLE_ITEMS: "NO_RETURNABLE_ITEMS",
MISMATCH_RETURNABLE_ITEMS: "MISMATCH_RETURNABLE_ITEMS",
} as const;
/**
@@ -32,9 +33,9 @@ export const CreateReturnProcessErrorMessages: Record<
string
> = {
[CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS]:
'No returnable items found.',
"No returnable items found.",
[CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS]:
'Mismatch in the number of returnable items.',
"Mismatch in the number of returnable items.",
};
/**
@@ -73,14 +74,21 @@ export const CreateReturnProcessErrorMessages: Record<
* }
* ```
*/
export class CreateReturnProcessError extends DataAccessError<'CREATE_RETURN_PROCESS'> {
export class CreateReturnProcessError extends DataAccessError<"CREATE_RETURN_PROCESS"> {
constructor(
public readonly reason: CreateReturnProcessErrorReason,
public readonly params: {
processId: number;
returns: { receipt: Receipt; items: ReceiptItem[] }[];
returns: {
receipt: Receipt;
items: {
receiptItem: ReceiptItem;
quantity: number;
category: ProductCategory;
}[];
}[];
},
) {
super('CREATE_RETURN_PROCESS', CreateReturnProcessErrorMessages[reason]);
super("CREATE_RETURN_PROCESS", CreateReturnProcessErrorMessages[reason]);
}
}

View File

@@ -1,39 +1,39 @@
import { returnReceiptValuesMapping } from './return-receipt-values-mapping.helper';
import { PropertyNullOrUndefinedError } from '@isa/common/data-access';
import { getReturnProcessQuestions } from './get-return-process-questions.helper';
import { getReturnInfo } from './get-return-info.helper';
import { serializeReturnDetails } from './return-details-mapping.helper';
import { returnReceiptValuesMapping } from "./return-receipt-values-mapping.helper";
import { PropertyNullOrUndefinedError } from "@isa/common/data-access";
import { getReturnProcessQuestions } from "./get-return-process-questions.helper";
import { getReturnInfo } from "./get-return-info.helper";
import { serializeReturnDetails } from "./return-details-mapping.helper";
// Mock dependencies
jest.mock('./get-return-process-questions.helper', () => ({
jest.mock("./get-return-process-questions.helper", () => ({
getReturnProcessQuestions: jest.fn(),
}));
jest.mock('./get-return-info.helper', () => ({
jest.mock("./get-return-info.helper", () => ({
getReturnInfo: jest.fn(),
}));
jest.mock('./return-details-mapping.helper', () => ({
jest.mock("./return-details-mapping.helper", () => ({
serializeReturnDetails: jest.fn(),
}));
describe('returnReceiptValuesMapping', () => {
describe("returnReceiptValuesMapping", () => {
const processMock: any = {
receiptItem: {
id: 'item-1',
quantity: { quantity: 2 },
features: { category: 'shoes' },
id: "item-1",
},
answers: { foo: 'bar' },
quantity: 2, // <-- Add this
productCategory: "shoes", // <-- Add this
answers: { foo: "bar" },
};
const questionsMock = [{ id: 'q1' }];
const questionsMock = [{ id: "q1" }];
const returnInfoMock = {
comment: 'Test comment',
itemCondition: 'NEW',
otherProduct: 'Other',
returnDetails: { detail: 'details' },
returnReason: 'Damaged',
comment: "Test comment",
itemCondition: "NEW",
otherProduct: "Other",
returnDetails: { detail: "details" },
returnReason: "Damaged",
};
const serializedDetails = { detail: 'serialized' };
const serializedDetails = { detail: "serialized" };
beforeEach(() => {
jest.clearAllMocks();
@@ -42,32 +42,24 @@ describe('returnReceiptValuesMapping', () => {
(serializeReturnDetails as jest.Mock).mockReturnValue(serializedDetails);
});
it('should map values correctly when all dependencies return valid data', () => {
it("should map values correctly when all dependencies return valid data", () => {
// Act
const result = returnReceiptValuesMapping(processMock);
// Assert
expect(result).toEqual({
quantity: 2,
comment: 'Test comment',
itemCondition: 'NEW',
otherProduct: 'Other',
comment: "Test comment",
itemCondition: "NEW",
otherProduct: "Other",
returnDetails: serializedDetails,
returnReason: 'Damaged',
category: 'shoes',
receiptItem: { id: 'item-1' },
returnReason: "Damaged",
category: "shoes",
receiptItem: { id: "item-1" },
});
expect(getReturnProcessQuestions).toHaveBeenCalledWith(processMock);
expect(getReturnInfo).toHaveBeenCalledWith({
questions: questionsMock,
answers: processMock.answers,
});
expect(serializeReturnDetails).toHaveBeenCalledWith(
returnInfoMock.returnDetails,
);
});
it('should throw PropertyNullOrUndefinedError if questions is undefined', () => {
it("should throw PropertyNullOrUndefinedError if questions is undefined", () => {
// Arrange
(getReturnProcessQuestions as jest.Mock).mockReturnValue(undefined);
@@ -75,10 +67,10 @@ describe('returnReceiptValuesMapping', () => {
expect(() => returnReceiptValuesMapping(processMock)).toThrow(
PropertyNullOrUndefinedError,
);
expect(() => returnReceiptValuesMapping(processMock)).toThrow('questions');
expect(() => returnReceiptValuesMapping(processMock)).toThrow("questions");
});
it('should throw PropertyNullOrUndefinedError if returnInfo is undefined', () => {
it("should throw PropertyNullOrUndefinedError if returnInfo is undefined", () => {
// Arrange
(getReturnInfo as jest.Mock).mockReturnValue(undefined);
@@ -86,28 +78,55 @@ describe('returnReceiptValuesMapping', () => {
expect(() => returnReceiptValuesMapping(processMock)).toThrow(
PropertyNullOrUndefinedError,
);
expect(() => returnReceiptValuesMapping(processMock)).toThrow('returnInfo');
expect(() => returnReceiptValuesMapping(processMock)).toThrow("returnInfo");
});
it('should handle missing category gracefully', () => {
// Arrange
const processNoCategory = {
...processMock,
receiptItem: { ...processMock.receiptItem, features: {} },
};
// Act
const result = returnReceiptValuesMapping(processNoCategory);
// Assert
expect(result?.category).toBeUndefined();
});
it('should handle missing receiptItem gracefully (may throw)', () => {
it("should handle missing receiptItem gracefully (may throw)", () => {
// Arrange
const processNoReceiptItem = { ...processMock, receiptItem: undefined };
// Act & Assert
expect(() => returnReceiptValuesMapping(processNoReceiptItem)).toThrow();
});
// Additional tests for edge cases and error scenarios
it("should return correct quantity when process.quantity is 0", () => {
const processZeroQuantity = { ...processMock, quantity: 0 };
const result = returnReceiptValuesMapping(processZeroQuantity);
expect(result?.quantity).toBe(0);
});
it("should propagate the correct receiptItem id", () => {
const result = returnReceiptValuesMapping(processMock);
expect(result?.receiptItem).toEqual({ id: "item-1" });
});
it("should throw if process is null", () => {
expect(() => returnReceiptValuesMapping(null as any)).toThrow();
});
it("should throw if process is undefined", () => {
expect(() => returnReceiptValuesMapping(undefined as any)).toThrow();
});
it("should call serializeReturnDetails with undefined if returnDetails is missing", () => {
// Arrange
const returnInfoNoDetails = { ...returnInfoMock, returnDetails: undefined };
(getReturnInfo as jest.Mock).mockReturnValue(returnInfoNoDetails);
// Act
returnReceiptValuesMapping(processMock);
// Assert
expect(serializeReturnDetails).toHaveBeenCalledWith(undefined);
});
it("should return undefined if process.quantity is undefined", () => {
const processNoQuantity = { ...processMock };
delete processNoQuantity.quantity;
// Should not throw, but quantity will be undefined in result
const result = returnReceiptValuesMapping(processNoQuantity);
expect(result?.quantity).toBeUndefined();
});
});

View File

@@ -1,16 +1,16 @@
import { ReturnProcess } from '../../models';
import { ReturnReceiptValues } from '../../schemas';
import { getReturnProcessQuestions } from './get-return-process-questions.helper';
import { getReturnInfo } from './get-return-info.helper';
import { PropertyNullOrUndefinedError } from '@isa/common/data-access';
import { serializeReturnDetails } from './return-details-mapping.helper';
import { ReturnProcess } from "../../models";
import { ReturnReceiptValues } from "../../schemas";
import { getReturnProcessQuestions } from "./get-return-process-questions.helper";
import { getReturnInfo } from "./get-return-info.helper";
import { PropertyNullOrUndefinedError } from "@isa/common/data-access";
import { serializeReturnDetails } from "./return-details-mapping.helper";
export const returnReceiptValuesMapping = (
process: ReturnProcess,
): ReturnReceiptValues | undefined => {
const questions = getReturnProcessQuestions(process);
if (!questions) {
throw new PropertyNullOrUndefinedError('questions');
throw new PropertyNullOrUndefinedError("questions");
}
const returnInfo = getReturnInfo({
@@ -19,17 +19,17 @@ export const returnReceiptValuesMapping = (
});
if (!returnInfo) {
throw new PropertyNullOrUndefinedError('returnInfo');
throw new PropertyNullOrUndefinedError("returnInfo");
}
return {
quantity: process.receiptItem.quantity.quantity,
quantity: process.quantity,
comment: returnInfo.comment,
itemCondition: returnInfo.itemCondition,
otherProduct: returnInfo.otherProduct,
returnDetails: serializeReturnDetails(returnInfo.returnDetails),
returnReason: returnInfo.returnReason,
category: process?.receiptItem?.features?.['category'],
category: process.productCategory,
receiptItem: {
id: process.receiptItem.id,
},

View File

@@ -1,5 +1,5 @@
import { Receipt } from './receipt';
import { ReceiptItem } from './receipt-item';
import { Receipt } from "./receipt";
import { ReceiptItem } from "./receipt-item";
/**
* Interface representing a return process within the OMS system.
@@ -21,6 +21,7 @@ export interface ReturnProcess {
receiptItem: ReceiptItem;
receiptDate: string | undefined;
answers: Record<string, unknown>;
productCategory?: string;
productCategory: string;
quantity: number;
returnReceipt?: Receipt;
}

View File

@@ -1,17 +1,17 @@
import { inject, Injectable } from '@angular/core';
import { inject, Injectable } from "@angular/core";
import {
FetchReturnDetails,
FetchReturnDetailsSchema,
ReturnReceiptValues,
} from '../schemas';
import { firstValueFrom } from 'rxjs';
import { ReceiptService } from '@generated/swagger/oms-api';
import { CanReturn, Receipt, ReceiptItem, ReceiptListItem } from '../models';
import { CategoryQuestions, ProductCategory } from '../questions';
import { KeyValue } from '@angular/common';
import { ReturnCanReturnService } from './return-can-return.service';
import { takeUntilAborted } from '@isa/common/data-access';
import { z } from 'zod';
} from "../schemas";
import { firstValueFrom } from "rxjs";
import { ReceiptService } from "@generated/swagger/oms-api";
import { CanReturn, Receipt, ReceiptItem, ReceiptListItem } from "../models";
import { CategoryQuestions, ProductCategory } from "../questions";
import { KeyValue } from "@angular/common";
import { ReturnCanReturnService } from "./return-can-return.service";
import { takeUntilAborted } from "@isa/common/data-access";
import { z } from "zod";
/**
* Service responsible for managing receipt return details and operations.
@@ -22,7 +22,7 @@ import { z } from 'zod';
* - Query receipts by customer email
* - Get available product categories for returns
*/
@Injectable({ providedIn: 'root' })
@Injectable({ providedIn: "root" })
export class ReturnDetailsService {
#receiptService = inject(ReceiptService);
#returnCanReturnService = inject(ReturnCanReturnService);
@@ -38,13 +38,17 @@ export class ReturnDetailsService {
* @throws Will throw an error if the return check fails or is aborted.
*/
async canReturn(
{ item, category }: { item: ReceiptItem; category: ProductCategory },
{
receiptItemId,
quantity,
category,
}: { receiptItemId: number; quantity: number; category: ProductCategory },
abortSignal?: AbortSignal,
): Promise<CanReturn> {
const returnReceiptValues: ReturnReceiptValues = {
quantity: item.quantity.quantity,
quantity,
receiptItem: {
id: item.id,
id: receiptItemId,
},
category,
};
@@ -102,7 +106,7 @@ export class ReturnDetailsService {
const res = await firstValueFrom(req$);
if (res.error || !res.result) {
throw new Error(res.message || 'Failed to fetch return details');
throw new Error(res.message || "Failed to fetch return details");
}
return res.result as Receipt;
@@ -137,7 +141,7 @@ export class ReturnDetailsService {
let req$ = this.#receiptService.ReceiptQueryReceipt({
queryToken: {
input: { qs: email },
filter: { receipt_type: '1;128;1024' },
filter: { receipt_type: "1;128;1024" },
},
});
@@ -147,7 +151,7 @@ export class ReturnDetailsService {
const res = await firstValueFrom(req$);
if (res.error || !res.result) {
throw new Error(res.message || 'Failed to fetch return items by email');
throw new Error(res.message || "Failed to fetch return items by email");
}
return res.result as ReceiptListItem[];

View File

@@ -1,4 +1,4 @@
import { computed, inject, resource, untracked } from '@angular/core';
import { computed, inject, resource } from '@angular/core';
import {
CanReturn,
ProductCategory,
@@ -7,7 +7,6 @@ import {
ReturnDetailsService,
} from '@isa/oms/data-access';
import {
getState,
patchState,
signalStore,
type,
@@ -22,13 +21,11 @@ import {
getReceiptItemQuantity,
getReceiptItemProductCategory,
receiptItemHasCategory,
getReceiptItemReturnedQuantity,
} from '../helpers/return-process';
import { SessionStorageProvider } from '@isa/core/storage';
import { logger } from '@isa/core/logging';
import { clone } from 'lodash';
interface ReturnDetailsState {
_storageId: number | undefined;
_selectedItemIds: number[];
selectedProductCategory: Record<number, ProductCategory>;
selectedQuantity: Record<number, number>;
@@ -36,7 +33,6 @@ interface ReturnDetailsState {
}
const initialState: ReturnDetailsState = {
_storageId: undefined,
_selectedItemIds: [],
selectedProductCategory: {},
selectedQuantity: {},
@@ -49,36 +45,11 @@ export const receiptConfig = entityConfig({
});
export const ReturnDetailsStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withEntities(receiptConfig),
withProps(() => ({
_logger: logger(() => ({ store: 'ReturnDetailsStore' })),
_returnDetailsService: inject(ReturnDetailsService),
_storage: inject(SessionStorageProvider),
})),
withMethods((store) => ({
_storageKey: () => `ReturnDetailsStore:${store._storageId}`,
})),
withMethods((store) => ({
_storeState: () => {
const state = getState(store);
if (!store._storageId) {
return;
}
store._storage.set(store._storageKey(), state);
store._logger.debug('State stored:', () => state);
},
_restoreState: async () => {
const data = await store._storage.get(store._storageKey());
if (data) {
patchState(store, data);
store._logger.debug('State restored:', () => ({ data }));
} else {
patchState(store, { ...initialState, _storageId: store._storageId() });
store._logger.debug('No state found, initialized with default state');
}
},
})),
withComputed((store) => ({
items: computed<Array<ReceiptItem>>(() =>
@@ -86,43 +57,56 @@ export const ReturnDetailsStore = signalStore(
.receiptsEntities()
.map((receipt) => receipt.items)
.flat()
.map((container) => {
const item = container.data;
if (!item) {
const err = new Error('Item data is undefined');
store._logger.error('Item data is undefined', err, () => ({
item: container,
}));
throw err;
}
const itemData = clone(item);
const quantityMap = store.selectedQuantity();
if (quantityMap[itemData.id]) {
itemData.quantity = { quantity: quantityMap[itemData.id] };
} else {
const quantity = getReceiptItemQuantity(itemData);
if (!itemData.quantity) {
itemData.quantity = { quantity };
} else {
itemData.quantity.quantity = quantity;
}
}
if (!itemData.features) {
itemData.features = {};
}
itemData.features['category'] =
store.selectedProductCategory()[itemData.id] ||
getReceiptItemProductCategory(itemData);
return itemData;
}),
.map((container) => container.data!),
),
})),
withComputed((store) => ({
availableQuantityMap: computed(() => {
const items = store.items();
const availableQuantity: Record<number, number> = {};
items.forEach((item) => {
const itemId = item.id;
const quantity = getReceiptItemQuantity(item);
const returnedQuantity = getReceiptItemReturnedQuantity(item);
availableQuantity[itemId] = quantity - returnedQuantity;
});
return availableQuantity;
}),
itemCategoryMap: computed(() => {
const items = store.items();
const categoryMap: Record<number, ProductCategory> = {};
items.forEach((item) => {
const itemId = item.id;
const selectedCategory = store.selectedProductCategory()[itemId];
const category = getReceiptItemProductCategory(item);
categoryMap[itemId] = selectedCategory ?? category;
});
return categoryMap;
}),
})),
withComputed((store) => ({
selectedQuantityMap: computed(() => {
const items = store.items();
const selectedQuantity: Record<number, number> = {};
items.forEach((item) => {
const itemId = item.id;
const quantity =
store.selectedQuantity()[itemId] ||
store.availableQuantityMap()[itemId];
selectedQuantity[itemId] = quantity;
});
return selectedQuantity;
}),
})),
withComputed((store) => ({
selectedItemIds: computed(() => {
const selectedIds = store._selectedItemIds();
@@ -167,8 +151,8 @@ export const ReturnDetailsStore = signalStore(
{ receiptId: params },
abortSignal,
);
patchState(store, setEntity(receipt, receiptConfig));
store._storeState();
return receipt;
},
}),
@@ -182,18 +166,21 @@ export const ReturnDetailsStore = signalStore(
return undefined;
}
const receiptItemId = item.id;
const quantity = store.selectedQuantityMap()[receiptItemId];
const category = store.itemCategoryMap()[receiptItemId];
return {
item: item,
category:
store.selectedProductCategory()[item.id] ||
getReceiptItemProductCategory(item),
receiptItemId,
quantity,
category,
};
},
loader: async ({ params, abortSignal }) => {
if (params === undefined) {
return undefined;
}
const key = `${params.item.id}:${params.category}`;
const key = `${params.receiptItemId}:${params.category}`;
if (store.canReturn()[key]) {
return store.canReturn()[key];
@@ -207,7 +194,6 @@ export const ReturnDetailsStore = signalStore(
canReturn: { ...store.canReturn(), [key]: res },
});
store._storeState();
return res;
},
}),
@@ -248,37 +234,25 @@ export const ReturnDetailsStore = signalStore(
})),
withMethods((store) => ({
selectStorage: (id: number) => {
untracked(() => {
patchState(store, { _storageId: id });
store._restoreState();
store._storeState();
store._logger.debug('Storage ID set:', () => ({ id }));
});
},
addSelectedItems(itemIds: number[]) {
const currentIds = store.selectedItemIds();
const newIds = Array.from(new Set([...currentIds, ...itemIds]));
patchState(store, { _selectedItemIds: newIds });
store._storeState();
},
removeSelectedItems(itemIds: number[]) {
const currentIds = store.selectedItemIds();
const newIds = currentIds.filter((id) => !itemIds.includes(id));
patchState(store, { _selectedItemIds: newIds });
store._storeState();
},
async setProductCategory(itemId: number, category: ProductCategory) {
const currentCategory = store.selectedProductCategory();
const newCategory = { ...currentCategory, [itemId]: category };
patchState(store, { selectedProductCategory: newCategory });
store._storeState();
},
setQuantity(itemId: number, quantity: number) {
const currentQuantity = store.selectedQuantity();
const newQuantity = { ...currentQuantity, [itemId]: quantity };
patchState(store, { selectedQuantity: newQuantity });
store._storeState();
},
})),
);

View File

@@ -7,6 +7,7 @@ import { setAllEntities, setEntity } from '@ngrx/signals/entities';
import { unprotected } from '@ngrx/signals/testing';
import { Product, ReturnProcess } from '../models';
import { CreateReturnProcessError } from '../errors/return-process';
import { ProductCategory } from '../questions';
const TEST_ITEMS: Record<number, ReturnProcess['receiptItem']> = {
1: {
@@ -77,7 +78,8 @@ describe('ReturnProcessStore', () => {
receiptItem: TEST_ITEMS[1],
receiptDate: '',
answers: {},
productCategory: undefined,
productCategory: ProductCategory.BookCalendar,
quantity: 1,
returnReceipt: undefined,
},
{
@@ -87,7 +89,8 @@ describe('ReturnProcessStore', () => {
receiptItem: TEST_ITEMS[2],
receiptDate: '',
answers: {},
productCategory: undefined,
productCategory: ProductCategory.BookCalendar,
quantity: 1,
returnReceipt: undefined,
},
{
@@ -97,7 +100,8 @@ describe('ReturnProcessStore', () => {
receiptItem: TEST_ITEMS[3],
receiptDate: '',
answers: {},
productCategory: undefined,
productCategory: ProductCategory.BookCalendar,
quantity: 1,
returnReceipt: undefined,
},
] as ReturnProcess[]),
@@ -122,7 +126,8 @@ describe('ReturnProcessStore', () => {
receiptItem: TEST_ITEMS[1],
receiptDate: '',
answers: {},
productCategory: undefined,
productCategory: ProductCategory.BookCalendar,
quantity: 1,
returnReceipt: undefined,
},
] as ReturnProcess[]),
@@ -148,7 +153,8 @@ describe('ReturnProcessStore', () => {
receiptDate: new Date().toJSON(),
receiptItem: TEST_ITEMS[1],
receiptId: 123,
productCategory: undefined,
productCategory: ProductCategory.BookCalendar,
quantity: 1,
returnReceipt: undefined,
} as ReturnProcess),
);
@@ -173,7 +179,13 @@ describe('ReturnProcessStore', () => {
items: [],
buyer: { buyerNumber: '' },
},
items: [TEST_ITEMS[1]],
items: [
{
receiptItem: TEST_ITEMS[1],
quantity: 1,
category: ProductCategory.BookCalendar,
},
],
},
{
receipt: {
@@ -182,12 +194,22 @@ describe('ReturnProcessStore', () => {
items: [],
buyer: { buyerNumber: '' },
},
items: [TEST_ITEMS[3]],
items: [
{
receiptItem: TEST_ITEMS[3],
quantity: 1,
category: ProductCategory.BookCalendar,
},
],
},
],
});
expect(store.entities()).toHaveLength(2);
expect(store.entities()[0].productCategory).toBe(
ProductCategory.BookCalendar,
);
expect(store.entities()[0].quantity).toBe(1);
});
it('should throw an error if no returnable items are found', () => {
@@ -205,7 +227,13 @@ describe('ReturnProcessStore', () => {
items: [],
buyer: { buyerNumber: '' },
},
items: [TEST_ITEMS[2]], // Non-returnable item
items: [
{
receiptItem: TEST_ITEMS[2], // Non-returnable item
quantity: 1,
category: ProductCategory.BookCalendar,
},
],
},
],
});
@@ -227,7 +255,23 @@ describe('ReturnProcessStore', () => {
items: [],
buyer: { buyerNumber: '' },
},
items: [TEST_ITEMS[1], TEST_ITEMS[2], TEST_ITEMS[3]],
items: [
{
receiptItem: TEST_ITEMS[1],
quantity: 1,
category: ProductCategory.BookCalendar,
},
{
receiptItem: TEST_ITEMS[2],
quantity: 1,
category: ProductCategory.BookCalendar,
},
{
receiptItem: TEST_ITEMS[3],
quantity: 1,
category: ProductCategory.BookCalendar,
},
],
},
],
});

View File

@@ -21,13 +21,21 @@ import {
} from '../errors/return-process';
import { logger } from '@isa/core/logging';
import { canReturnReceiptItem } from '../helpers/return-process';
import { ProductCategory } from '../questions';
/**
* Interface representing the parameters required to start a return process.
*/
export type StartProcess = {
processId: number;
returns: { receipt: Receipt; items: ReceiptItem[] }[];
returns: {
receipt: Receipt;
items: {
receiptItem: ReceiptItem;
quantity: number;
category: ProductCategory;
}[];
}[];
};
/**
@@ -142,6 +150,7 @@ export const ReturnProcessStore = signalStore(
const returnableItems = params.returns
.flatMap((r) => r.items)
.map((item) => item.receiptItem)
.filter(canReturnReceiptItem);
if (returnableItems.length === 0) {
@@ -170,9 +179,10 @@ export const ReturnProcessStore = signalStore(
id: nextId + entities.length,
processId: params.processId,
receiptId: receipt.id,
productCategory: item.features?.['category'],
productCategory: item.category,
quantity: item.quantity,
receiptDate: receipt.printedDate,
receiptItem: item,
receiptItem: item.receiptItem,
answers: {},
});
}

View File

@@ -1,34 +1,40 @@
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: 'omsFeature',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'oms-feature',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'omsFeature',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'oms-feature',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -1,9 +1,11 @@
<div class="flex flex-row w-full">
<div
class="flex flex-row justify-end -mb-4 desktop:mb-0 w-[13.4375rem] desktop:w-full"
>
@if (quantityDropdownValues().length > 1) {
<ui-dropdown
class="quantity-dropdown"
[disabled]="!canReturnReceiptItem()"
[value]="availableQuantity()"
[value]="selectedQuantity()"
(valueChange)="setQuantity($event)"
>
@for (quantity of quantityDropdownValues(); track quantity) {

View File

@@ -1,9 +1,13 @@
:host {
@apply flex flex-col-reverse items-end desktop:flex-row desktop:justify-center desktop:items-center gap-4;
@apply flex flex-col-reverse items-end desktop:flex-row desktop:justify-center desktop:items-center gap-4;
.product-dropdown.ui-dropdown {
@apply max-w-[13.4375rem] desktop:max-w-full;
}
:has(.product-dropdown):has(.quantity-dropdown) {
.quantity-dropdown.ui-dropdown {
@apply border-r-0 pr-4;
@apply border-r-0 pr-4 pl-5 max-w-20 desktop:max-w-full;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
@@ -15,7 +19,7 @@
}
.product-dropdown.ui-dropdown {
@apply border-l-0 pl-4;
@apply border-l-0 max-w-[8.75rem] desktop:max-w-full pr-5 pl-4;
border-top-left-radius: 0;
border-bottom-left-radius: 0;

View File

@@ -1,50 +1,52 @@
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { MockDirective } from 'ng-mocks';
import { createComponentFactory, Spectator } from "@ngneat/spectator/jest";
import { MockDirective } from "ng-mocks";
import {
ReceiptItem,
ReturnDetailsService,
ReturnDetailsStore,
} from '@isa/oms/data-access';
} from "@isa/oms/data-access";
import { ProductImageDirective } from '@isa/shared/product-image';
import { ReturnDetailsOrderGroupItemControlsComponent } from '../return-details-order-group-item-controls/return-details-order-group-item-controls.component';
import { CheckboxComponent } from '@isa/ui/input-controls';
import { signal } from '@angular/core';
import { ProductImageDirective } from "@isa/shared/product-image";
import { ReturnDetailsOrderGroupItemControlsComponent } from "../return-details-order-group-item-controls/return-details-order-group-item-controls.component";
import { CheckboxComponent } from "@isa/ui/input-controls";
import { signal } from "@angular/core";
// Helper function to create mock ReceiptItem data
const createMockItem = (
ean: string,
canReturn: boolean,
name = 'Test Product',
category = 'BOOK', // Add default category that's not 'unknown'
name = "Test Product",
category = "BOOK", // Add default category that's not 'unknown'
availableQuantity = 2,
selectedQuantity = 1,
): ReceiptItem =>
({
id: 123,
receiptNumber: 'R-123456', // Add the required receiptNumber property
quantity: { quantity: 1 },
receiptNumber: "R-123456",
quantity: { quantity: availableQuantity },
price: {
value: { value: 19.99, currency: 'EUR' },
value: { value: 19.99, currency: "EUR" },
vat: { inPercent: 19 },
},
product: {
ean: ean,
name: name,
contributors: 'Test Author',
format: 'HC',
formatDetail: 'Hardcover',
manufacturer: 'Test Publisher',
publicationDate: '2024-01-01T00:00:00Z',
catalogProductNumber: '1234567890',
volume: '1',
contributors: "Test Author",
format: "HC",
formatDetail: "Hardcover",
manufacturer: "Test Publisher",
publicationDate: "2024-01-01T00:00:00Z",
catalogProductNumber: "1234567890",
volume: "1",
},
actions: [{ key: 'canReturn', value: String(canReturn) }],
features: { category: category }, // Add the features property with category
actions: [{ key: "canReturn", value: String(canReturn) }],
features: { category: category },
}) as ReceiptItem;
describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
describe("ReturnDetailsOrderGroupItemControlsComponent", () => {
let spectator: Spectator<ReturnDetailsOrderGroupItemControlsComponent>;
const mockItemSelectable = createMockItem('1234567890123', true);
const mockItemSelectable = createMockItem("1234567890123", true);
const mockIsSelectable = signal<boolean>(true);
const mockGetItemSelectted = signal<boolean>(false);
@@ -52,6 +54,11 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
isLoading: signal<boolean>(true),
};
// Mocks for availableQuantityMap and selectedQuantityMap
const mockAvailableQuantityMap = { [mockItemSelectable.id]: 2 };
const mockSelectedQuantityMap = { [mockItemSelectable.id]: 1 };
const mockItemCategoryMap = { [mockItemSelectable.id]: "BOOK" };
function resetMocks() {
mockIsSelectable.set(true);
mockGetItemSelectted.set(false);
@@ -68,12 +75,16 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
isSelectable: jest.fn(() => mockIsSelectable),
getItemSelected: jest.fn(() => mockGetItemSelectted),
canReturnResource: jest.fn(() => mockCanReturnResource),
availableQuantityMap: jest.fn(() => mockAvailableQuantityMap),
selectedQuantityMap: jest.fn(() => mockSelectedQuantityMap),
itemCategoryMap: jest.fn(() => mockItemCategoryMap),
setProductCategory: jest.fn(),
setQuantity: jest.fn(),
addSelectedItems: jest.fn(),
removeSelectedItems: jest.fn(),
},
},
],
// Spectator automatically stubs standalone dependencies like ItemRowComponent, CheckboxComponent etc.
// We don't need deep interaction, just verify the host component renders correctly.
// If specific interactions were needed, we could provide mocks or use overrideComponents.
overrideComponents: [
[
ReturnDetailsOrderGroupItemControlsComponent,
@@ -85,50 +96,41 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
},
],
],
detectChanges: false, // Control initial detection manually
detectChanges: false,
});
beforeEach(() => {
// Default setup with a selectable item
spectator = createComponent({
props: {
item: mockItemSelectable, // Use signal for input
item: mockItemSelectable,
},
});
});
afterEach(() => {
resetMocks(); // Reset mocks after each test
resetMocks();
});
it('should create', () => {
// Arrange
spectator.detectChanges(); // Trigger initial render
// Assert
it("should create", () => {
spectator.detectChanges();
expect(spectator.component).toBeTruthy();
});
it('should display the checkbox when item is selectable', () => {
// Arrange
mockCanReturnResource.isLoading.set(false); // Simulate the resource being ready
mockIsSelectable.set(true); // Simulate the item being selectable
it("should display the checkbox when item is selectable and not loading", () => {
mockCanReturnResource.isLoading.set(false);
mockIsSelectable.set(true);
spectator.detectChanges();
// Assert
expect(spectator.component.selectable()).toBe(true);
const checkbox = spectator.query(CheckboxComponent);
expect(checkbox).toBeTruthy();
expect(spectator.query(CheckboxComponent)).toBeTruthy();
expect(
spectator.query(`input[data-what="return-item-checkbox"]`),
).toExist();
});
it('should NOT display the checkbox when item is not selectable', () => {
// Arrange
mockIsSelectable.set(false); // Simulate the item not being selectable
spectator.detectChanges();
spectator.detectComponentChanges();
// Assert
it("should NOT display the checkbox when item is not selectable", () => {
mockIsSelectable.set(false);
mockCanReturnResource.isLoading.set(false);
spectator.detectChanges();
expect(spectator.component.selectable()).toBe(false);
expect(
spectator.query(`input[data-what="return-item-checkbox"]`),
@@ -136,27 +138,73 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
expect(spectator.query(CheckboxComponent)).toBeFalsy();
});
it('should be false when no canReturn action is present', () => {
// Arrange
const item = { ...createMockItem('0001', true), actions: [] };
spectator.setInput('item', item as any);
// Act
it("should show spinner when canReturnResource is loading", () => {
mockCanReturnResource.isLoading.set(true);
spectator.detectChanges();
expect(
spectator.query('ui-icon-button[data-what="load-spinner"]'),
).toExist();
});
// Assert
it("should render correct quantity dropdown values", () => {
spectator.detectChanges();
expect(spectator.component.quantityDropdownValues()).toEqual([1, 2]);
});
it("should call setQuantity when dropdown value changes", () => {
const store = spectator.inject(ReturnDetailsStore);
const spy = jest.spyOn(store, "setQuantity");
spectator.detectChanges();
// Simulate dropdown value change
spectator.component.setQuantity(2);
expect(spy).toHaveBeenCalledWith(mockItemSelectable.id, 2);
});
it("should call setProductCategory when product category changes", () => {
const store = spectator.inject(ReturnDetailsStore);
const spy = jest.spyOn(store, "setProductCategory");
spectator.detectChanges();
spectator.component.setProductCategory("Buch/Kalender");
expect(spy).toHaveBeenCalledWith(mockItemSelectable.id, "Buch/Kalender");
});
it("should call addSelectedItems when setSelected(true) is called", () => {
const store = spectator.inject(ReturnDetailsStore);
const spy = jest.spyOn(store, "addSelectedItems");
spectator.detectChanges();
spectator.component.setSelected(true);
expect(spy).toHaveBeenCalledWith([mockItemSelectable.id]);
});
it("should call removeSelectedItems when setSelected(false) is called", () => {
const store = spectator.inject(ReturnDetailsStore);
const spy = jest.spyOn(store, "removeSelectedItems");
spectator.detectChanges();
spectator.component.setSelected(false);
expect(spy).toHaveBeenCalledWith([mockItemSelectable.id]);
});
it("should be false when no canReturn action is present", () => {
const item = { ...createMockItem("0001", true), actions: [] };
spectator.setInput("item", item as any);
spectator.detectChanges();
expect(spectator.component.canReturnReceiptItem()).toBe(false);
});
it('should be false when canReturn action has falsy value', () => {
// Arrange
const item = createMockItem('0001', false);
spectator.setInput('item', item);
// Act
it("should be false when canReturn action has falsy value", () => {
const item = createMockItem("0001", false);
spectator.setInput("item", item);
spectator.detectChanges();
// Assert
expect(spectator.component.canReturnReceiptItem()).toBe(false);
});
it("should display correct selected quantity", () => {
spectator.detectChanges();
expect(spectator.component.selectedQuantity()).toBe(1);
});
it("should display correct product category", () => {
spectator.detectChanges();
expect(spectator.component.productCategory()).toBe("BOOK");
});
});

View File

@@ -5,30 +5,27 @@ import {
inject,
input,
signal,
} from '@angular/core';
import { provideLoggerContext } from '@isa/core/logging';
} from "@angular/core";
import { provideLoggerContext } from "@isa/core/logging";
import {
canReturnReceiptItem,
getReceiptItemReturnedQuantity,
getReceiptItemProductCategory,
getReceiptItemQuantity,
ProductCategory,
ReceiptItem,
ReturnDetailsService,
ReturnDetailsStore,
} from '@isa/oms/data-access';
import { IconButtonComponent } from '@isa/ui/buttons';
} from "@isa/oms/data-access";
import { IconButtonComponent } from "@isa/ui/buttons";
import {
CheckboxComponent,
DropdownButtonComponent,
DropdownOptionComponent,
} from '@isa/ui/input-controls';
import { FormsModule } from '@angular/forms';
} from "@isa/ui/input-controls";
import { FormsModule } from "@angular/forms";
@Component({
selector: 'oms-feature-return-details-order-group-item-controls',
templateUrl: './return-details-order-group-item-controls.component.html',
styleUrls: ['./return-details-order-group-item-controls.component.scss'],
selector: "oms-feature-return-details-order-group-item-controls",
templateUrl: "./return-details-order-group-item-controls.component.html",
styleUrls: ["./return-details-order-group-item-controls.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
@@ -40,7 +37,7 @@ import { FormsModule } from '@angular/forms';
],
providers: [
provideLoggerContext({
component: 'ReturnDetailsOrderGroupItemControlsComponent',
component: "ReturnDetailsOrderGroupItemControlsComponent",
}),
],
})
@@ -66,38 +63,11 @@ export class ReturnDetailsOrderGroupItemControlsComponent {
availableCategories = this.#returnDetailsService.availableCategories();
/**
* Computes the quantity of the current receipt item that has already been returned.
*
* This value is derived from the item's return history and is used to indicate
* how many units have already been processed for return.
*
* @returns The number of units already returned for this receipt item.
*/
returnedQuantity = computed(() => {
selectedQuantity = computed(() => {
const item = this.item();
return getReceiptItemReturnedQuantity(item);
return this.#store.selectedQuantityMap()[item.id];
});
/**
* Computes the total quantity for the current receipt item.
* Represents the original quantity as recorded in the receipt.
*
* @returns The total quantity for the item.
*/
quantity = computed(() => {
const item = this.item();
return getReceiptItemQuantity(item);
});
/**
* Computes the quantity of the item that is still available for return.
* Calculated as the difference between the total quantity and the returned quantity.
*
* @returns The number of units available to be returned.
*/
availableQuantity = computed(() => this.quantity() - this.returnedQuantity());
/**
* Generates the list of selectable quantities for the dropdown.
* The values range from 1 up to the available quantity.
@@ -105,13 +75,14 @@ export class ReturnDetailsOrderGroupItemControlsComponent {
* @returns An array of selectable quantity values.
*/
quantityDropdownValues = computed(() => {
const itemQuantity = this.availableQuantity();
const item = this.item();
const itemQuantity = this.#store.availableQuantityMap()[item.id];
return Array.from({ length: itemQuantity }, (_, i) => i + 1);
});
productCategory = computed(() => {
const item = this.item();
return getReceiptItemProductCategory(item);
return this.#store.itemCategoryMap()[item.id];
});
selectable = this.#store.isSelectable(this.item);
@@ -127,8 +98,9 @@ export class ReturnDetailsOrderGroupItemControlsComponent {
}
setQuantity(quantity: number | undefined) {
const item = this.item();
if (quantity === undefined) {
quantity = this.item().quantity.quantity;
quantity = this.#store.availableQuantityMap()[item.id];
}
this.#store.setQuantity(this.item().id, quantity);
}

View File

@@ -57,7 +57,7 @@
{{ i.product.manufacturer }} | {{ i.product.ean }}
</div>
<div class="text-isa-neutral-600 isa-text-body-2-regular">
{{ i.product.publicationDate | date: 'dd. MMM yyyy' }}
{{ i.product.publicationDate | date: "dd. MMM yyyy" }}
</div>
</div>
<oms-feature-return-details-order-group-item-controls [item]="i">
@@ -73,11 +73,11 @@
</div>
}
@if (returnedQuantity() > 0 && itemQuantity() !== returnedQuantity()) {
@if (availableQuantity() !== quantity()) {
<div
class="flex items-center self-start text-isa-neutral-600 isa-text-body-2-bold pb-6"
>
Es wurden bereits {{ returnedQuantity() }} von {{ itemQuantity() }} Artikel
zurückgegeben.
Es wurden bereits {{ quantity() - availableQuantity() }} von
{{ quantity() }} Artikel zurückgegeben.
</div>
}

View File

@@ -1,29 +1,28 @@
import { CurrencyPipe, DatePipe, LowerCasePipe } from '@angular/common';
import { CurrencyPipe, DatePipe, LowerCasePipe } from "@angular/common";
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
input,
} from '@angular/core';
import { isaActionClose, ProductFormatIconGroup } from '@isa/icons';
} from "@angular/core";
import { isaActionClose, ProductFormatIconGroup } from "@isa/icons";
import {
getReceiptItemAction,
getReceiptItemReturnedQuantity,
getReceiptItemQuantity,
ReceiptItem,
ReturnDetailsStore,
} from '@isa/oms/data-access';
import { ProductImageDirective } from '@isa/shared/product-image';
import { ItemRowComponent } from '@isa/ui/item-rows';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { ReturnDetailsOrderGroupItemControlsComponent } from '../return-details-order-group-item-controls/return-details-order-group-item-controls.component';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
} from "@isa/oms/data-access";
import { ProductImageDirective } from "@isa/shared/product-image";
import { ItemRowComponent } from "@isa/ui/item-rows";
import { NgIconComponent, provideIcons } from "@ng-icons/core";
import { ReturnDetailsOrderGroupItemControlsComponent } from "../return-details-order-group-item-controls/return-details-order-group-item-controls.component";
import { ProductRouterLinkDirective } from "@isa/shared/product-router-link";
@Component({
selector: 'oms-feature-return-details-order-group-item',
templateUrl: './return-details-order-group-item.component.html',
styleUrls: ['./return-details-order-group-item.component.scss'],
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,
standalone: true,
imports: [
@@ -82,7 +81,7 @@ export class ReturnDetailsOrderGroupItemComponent {
*/
canReturnMessage = computed(() => {
const item = this.item();
const canReturnAction = getReceiptItemAction(item, 'canReturn');
const canReturnAction = getReceiptItemAction(item, "canReturn");
if (canReturnAction?.description) {
return canReturnAction.description;
@@ -90,30 +89,32 @@ export class ReturnDetailsOrderGroupItemComponent {
const canReturnMessage = this.canReturn()?.message;
return canReturnMessage ?? '';
return canReturnMessage ?? "";
});
/**
* Computes the quantity of the current receipt item that has already been returned.
* The original quantity of the item as recorded in the order.
* This value is retrieved from the store and represents the total number of units
* initially purchased for this receipt item.
*
* This value is derived using the item's return history and is used to display
* how many units of this item have been processed for return so far.
*
* @returns The number of units already returned for this receipt item.
* @readonly
* @returns {number} The original quantity of the item in the order.
*/
returnedQuantity = computed(() => {
const item = this.item();
return getReceiptItemReturnedQuantity(item);
});
/**
* Computes the total quantity for the current receipt item.
* Represents the original quantity of the item as recorded in the receipt.
*
* @returns The total quantity for the item.
*/
itemQuantity = computed(() => {
quantity = computed(() => {
const item = this.item();
return getReceiptItemQuantity(item);
});
/**
* The currently available quantity of the item for return.
* This value is computed based on the item's current state and may be less than
* the original quantity if some units have already been returned or are otherwise unavailable.
*
* @readonly
* @returns {number} The number of units available for return.
*/
availableQuantity = computed(() => {
const item = this.item();
return this.#store.availableQuantityMap()[item.id];
});
}

View File

@@ -2,7 +2,6 @@ import {
ChangeDetectionStrategy,
Component,
computed,
effect,
inject,
resource,
} from '@angular/core';
@@ -40,11 +39,11 @@ import { groupBy } from 'lodash';
ExpandableDirectives,
ProgressBarComponent,
],
providers: [provideIcons({ isaActionChevronLeft })],
providers: [provideIcons({ isaActionChevronLeft }), ReturnDetailsStore],
})
export class ReturnDetailsComponent {
#logger = logger(() => ({
component: ReturnDetailsComponent.name,
component: 'ReturnDetailsComponent',
itemId: this.receiptId(),
processId: this.processId(),
params: this.params(),
@@ -71,10 +70,6 @@ export class ReturnDetailsComponent {
throw new Error('No receiptId found in route params');
});
// Effect resets the Store's state when the receiptId changes
// This ensures that the store is always in sync with the current receiptId
receiptIdEffect = effect(() => this.#store.selectStorage(this.receiptId()));
receiptResource = this.#store.receiptResource(this.receiptId);
customerReceiptsResource = resource({
@@ -107,6 +102,8 @@ export class ReturnDetailsComponent {
const processId = this.processId();
const selectedItems = this.#store.selectedItems();
const selectedQuantites = this.#store.selectedQuantityMap();
const selectedProductCategories = this.#store.itemCategoryMap();
this.#logger.info('Starting return process', () => ({
processId: processId,
@@ -126,7 +123,14 @@ export class ReturnDetailsComponent {
const returns = Object.entries(itemsGrouptByReceiptId).map(
([receiptId, items]) => ({
receipt: receipts[Number(receiptId)],
items,
items: items.map((item) => {
const receiptItem = item;
return {
receiptItem,
quantity: selectedQuantites[receiptItem.id],
category: selectedProductCategories[receiptItem.id],
};
}),
}),
);

View File

@@ -1,34 +1,40 @@
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: 'oms-feature',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'oms-feature',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'oms-feature',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'oms-feature',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -159,7 +159,7 @@ export class ReturnProcessItemComponent {
} catch (error) {
this.#logger.error(
'Failed to validate return process',
error,
error as Error,
() => ({
returnProcessId: returnProcess.id,
}),

View File

@@ -16,8 +16,6 @@ import {
ReturnProcessStore,
} from '@isa/oms/data-access';
import {
ChipOptionComponent,
ChipsComponent,
CheckboxComponent,
ChecklistComponent,
CheckboxLabelDirective,
@@ -25,7 +23,6 @@ import {
TextareaComponent,
} from '@isa/ui/input-controls';
import { isEqual } from 'lodash';
import { z } from 'zod';
@Component({
selector: 'oms-feature-return-process-checklist-question',
@@ -35,8 +32,6 @@ import { z } from 'zod';
standalone: true,
imports: [
FormsModule,
ChipsComponent,
ChipOptionComponent,
CheckboxComponent,
ChecklistComponent,
ChecklistValueDirective,

View File

@@ -162,7 +162,7 @@ export class ReturnProcessComponent {
} catch (error) {
this.#logger.error(
'Failed to check canReturn for process',
error,
error as Error,
() => ({
processId: returnProcess.processId,
}),

View File

@@ -1,34 +1,40 @@
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: 'omsFeature',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'oms-feature',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'omsFeature',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'oms-feature',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -1,34 +1,40 @@
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: 'omsFeature',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'oms-feature',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'omsFeature',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'oms-feature',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -77,10 +77,10 @@ export class ReturnSearchMainComponent {
}: CallbackResult<ListResponseArgs<ReceiptListItem>>) => {
if (data) {
if (data.result.length === 1) {
this.navigate(['receipt', data.result[0].id]);
} else if (data.result.length > 1) {
this.navigate(['receipts']);
return this.navigate(['receipt', data.result[0].id]);
}
return this.navigate(['receipts']);
}
};

View File

@@ -1,4 +1,7 @@
<ui-client-row data-what="search-result-item" [attr.data-which]="receiptNumber()">
<ui-client-row
data-what="search-result-item"
[attr.data-which]="receiptNumber()"
>
<ui-client-row-content>
<h3 class="isa-text-subtitle-1-regular">{{ name() }}</h3>
</ui-client-row-content>
@@ -7,12 +10,12 @@
<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' }}
{{ 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-label>Beleg-Nr.</ui-item-row-data-label>
<ui-item-row-data-value>
<span class="isa-text-body-2-bold"> {{ receiptNumber() }} </span>
</ui-item-row-data-value>

Some files were not shown because too many files have changed in this diff Show More