mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Compare commits
38 Commits
nx-build-e
...
feature/51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9623a8ede5 | ||
|
|
741e1e6d15 | ||
|
|
a36d746fb8 | ||
|
|
f6b2b554bb | ||
|
|
465df27858 | ||
|
|
7c907645dc | ||
|
|
b7e7155577 | ||
|
|
b28c204f23 | ||
|
|
e7a807cfbd | ||
|
|
344dc61a90 | ||
|
|
8d063428fc | ||
|
|
06b0c6264a | ||
|
|
4fe633e973 | ||
|
|
2463a803ea | ||
|
|
1663dcec73 | ||
|
|
827aa565c5 | ||
|
|
39fc4ce1ce | ||
|
|
4f4b072e25 | ||
|
|
9af4a72a76 | ||
|
|
7a44101e90 | ||
|
|
6fee35c756 | ||
|
|
c15077aa86 | ||
|
|
f051a97e53 | ||
|
|
1b26a44a37 | ||
|
|
80b2508708 | ||
|
|
e9affd2359 | ||
|
|
8f8b9153b0 | ||
|
|
9a4121e2bf | ||
|
|
50b7f21394 | ||
|
|
a67375557d | ||
|
|
6e7c56fcb9 | ||
|
|
05e257b922 | ||
|
|
d7d61915fa | ||
|
|
d0220b6246 | ||
|
|
32336ba5b4 | ||
|
|
1f26d5285b | ||
|
|
be0bff0535 | ||
|
|
cb7391e66f |
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__context7__resolve-library-id",
|
||||
"mcp__context7__get-library-docs"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
9
.github/instructions/nx.instructions.md
vendored
9
.github/instructions/nx.instructions.md
vendored
@@ -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
145
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import "./components/icon";
|
||||
@use "./components/icon";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
75
apps/isa-app/src/tailwind.scss
Normal file
75
apps/isa-app/src/tailwind.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": []
|
||||
"types": [],
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
|
||||
@@ -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'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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'],
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
40
generated/swagger/availability-api/eslint.config.js
Normal file
40
generated/swagger/availability-api/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
40
generated/swagger/cat-search-api/eslint.config.js
Normal file
40
generated/swagger/cat-search-api/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
40
generated/swagger/crm-api/eslint.config.js
Normal file
40
generated/swagger/crm-api/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
40
generated/swagger/eis-api/eslint.config.js
Normal file
40
generated/swagger/eis-api/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
40
generated/swagger/inventory-api/eslint.config.js
Normal file
40
generated/swagger/inventory-api/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
40
generated/swagger/isa-api/eslint.config.js
Normal file
40
generated/swagger/isa-api/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
40
generated/swagger/oms-api/eslint.config.js
Normal file
40
generated/swagger/oms-api/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
40
generated/swagger/print-api/eslint.config.js
Normal file
40
generated/swagger/print-api/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
40
generated/swagger/wws-api/eslint.config.js
Normal file
40
generated/swagger/wws-api/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
40
libs/catalogue/data-access/eslint.config.js
Normal file
40
libs/catalogue/data-access/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
277
libs/common/decorators/README.md
Normal file
277
libs/common/decorators/README.md
Normal 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`
|
||||
@@ -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'],
|
||||
20
libs/common/decorators/project.json
Normal file
20
libs/common/decorators/project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
libs/common/decorators/src/index.ts
Normal file
1
libs/common/decorators/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/in-flight.decorator';
|
||||
321
libs/common/decorators/src/lib/in-flight.decorator.spec.ts
Normal file
321
libs/common/decorators/src/lib/in-flight.decorator.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
251
libs/common/decorators/src/lib/in-flight.decorator.ts
Normal file
251
libs/common/decorators/src/lib/in-flight.decorator.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
13
libs/common/decorators/src/test-setup.ts
Normal file
13
libs/common/decorators/src/test-setup.ts
Normal 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(),
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/common/decorators/tsconfig.lib.json
Normal file
27
libs/common/decorators/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
29
libs/common/decorators/tsconfig.spec.json
Normal file
29
libs/common/decorators/tsconfig.spec.json
Normal 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"]
|
||||
}
|
||||
27
libs/common/decorators/vite.config.mts
Normal file
27
libs/common/decorators/vite.config.mts
Normal 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,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
40
libs/core/config/eslint.config.js
Normal file
40
libs/core/config/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
40
libs/core/storage/eslint.config.js
Normal file
40
libs/core/storage/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
@@ -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));
|
||||
});
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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}"],
|
||||
|
||||
40
libs/oms/data-access/eslint.config.js
Normal file
40
libs/oms/data-access/eslint.config.js
Normal 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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -159,7 +159,7 @@ export class ReturnProcessItemComponent {
|
||||
} catch (error) {
|
||||
this.#logger.error(
|
||||
'Failed to validate return process',
|
||||
error,
|
||||
error as Error,
|
||||
() => ({
|
||||
returnProcessId: returnProcess.id,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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']);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user