mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Compare commits
73 Commits
nx-build-e
...
feature/51
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61a09bfacc | ||
|
|
d7d535c10d | ||
|
|
ad00899b6e | ||
|
|
1e84223076 | ||
|
|
244984b6cf | ||
|
|
b39abe630d | ||
|
|
239ab52890 | ||
|
|
4732656a0f | ||
|
|
0da9800ca0 | ||
|
|
baf4a0dfbc | ||
|
|
da5a42280a | ||
|
|
4d29189c8d | ||
|
|
32bd3e26d2 | ||
|
|
6d26f7f6c0 | ||
|
|
72bcacefb6 | ||
|
|
71e9a6da0e | ||
|
|
b339a6d79f | ||
|
|
0b4aef5f6c | ||
|
|
c5182809ac | ||
|
|
f4b541c7c0 | ||
|
|
afe6c6abcc | ||
|
|
3f233f9580 | ||
|
|
6f9d4d9218 | ||
|
|
4111663d8c | ||
|
|
2beeba5c92 | ||
|
|
edab1322c8 | ||
|
|
59ce736faa | ||
|
|
3cd6f4bd58 | ||
|
|
594acaa5f5 | ||
|
|
76ff54dd3a | ||
|
|
598df7d5ed | ||
|
|
442670bdd0 | ||
|
|
b015e97e1f | ||
|
|
65ab3bfc0a | ||
|
|
e674378080 | ||
|
|
40c9d51dfc | ||
|
|
5f74c6ddf8 | ||
|
|
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": []
|
||||
}
|
||||
}
|
||||
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -4,6 +4,8 @@
|
||||
|
||||
You are Mentor, an AI assistant focused on ensuring code quality, strict adherence to best practices, and development efficiency. **Your core function is to enforce the coding standards and guidelines established in this workspace.** Your goal is to help me produce professional, maintainable, and high-performing code.
|
||||
|
||||
**Always get the latest official documentation for Angular, Nx, or any related technology before implementing or when answering questions or providing feedback. Use Context7:**
|
||||
|
||||
## Tone and Personality
|
||||
|
||||
Maintain a professional, objective, and direct tone consistently:
|
||||
|
||||
40
.github/instructions/nx.instructions.md
vendored
40
.github/instructions/nx.instructions.md
vendored
@@ -1,40 +0,0 @@
|
||||
---
|
||||
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 have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
|
||||
|
||||
# General Guidelines
|
||||
- When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture
|
||||
- For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration
|
||||
- If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors
|
||||
- To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool
|
||||
|
||||
# Generation Guidelines
|
||||
If the user wants to generate something, use the following flow:
|
||||
|
||||
- learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable
|
||||
- get the available generators using the 'nx_generators' tool
|
||||
- decide which generator to use. If no generators seem relevant, check the 'nx_available_plugins' tool to see if the user could install a plugin to help them
|
||||
- get generator details using the 'nx_generator_schema' tool
|
||||
- you may use the 'nx_docs' tool to learn more about a specific generator or technology if you're unsure
|
||||
- decide which options to provide in order to best complete the user's request. Don't make any assumptions and keep the options minimalistic
|
||||
- open the generator UI using the 'nx_open_generate_ui' tool
|
||||
- wait for the user to finish the generator
|
||||
- read the generator log file using the 'nx_read_generator_log' tool
|
||||
- use the information provided in the log file to answer the user's question or continue with what they were doing
|
||||
|
||||
# Running Tasks Guidelines
|
||||
If the user wants help with tasks or commands (which include keywords like "test", "build", "lint", or other similar actions), use the following flow:
|
||||
- Use the 'nx_current_running_tasks_details' tool to get the list of tasks (this can include tasks that were completed, stopped or failed).
|
||||
- If there are any tasks, ask the user if they would like help with a specific task then use the 'nx_current_running_task_output' tool to get the terminal output for that task/command
|
||||
- Use the terminal output from 'nx_current_running_task_output' to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
|
||||
- If the user would like to rerun the task or command, always use `nx run <taskId>` to rerun in the terminal. This will ensure that the task will run in the nx context and will be run the same way it originally executed
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
39
.github/testing-instructions.md
vendored
39
.github/testing-instructions.md
vendored
@@ -2,8 +2,10 @@
|
||||
|
||||
## Framework and Tools
|
||||
|
||||
- Use **Jest** as the testing framework.
|
||||
- For unit tests, utilize **Spectator** to simplify Angular component testing.
|
||||
- **Vitest** is the recommended testing framework.
|
||||
[Vitest Documentation (latest)](https://context7.com/vitest-dev/vitest/llms.txt?topic=getting+started)
|
||||
- **Jest** and **Spectator** are **deprecated**.
|
||||
Do not use them for new tests. Existing tests should be migrated to Vitest where possible.
|
||||
|
||||
## Guidelines
|
||||
|
||||
@@ -23,28 +25,31 @@
|
||||
## Example Test Structure
|
||||
|
||||
```typescript
|
||||
// Example using Jest and Spectator
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator';
|
||||
// Example using Vitest (Jest and Spectator are deprecated)
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { render } from '@testing-library/angular';
|
||||
import { MyComponent } from './my-component.component';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
let spectator: Spectator<MyComponent>;
|
||||
const createComponent = createComponentFactory(MyComponent);
|
||||
let component: MyComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
beforeEach(async () => {
|
||||
const { fixture } = await render(MyComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should display the correct title', () => {
|
||||
it('should display the correct title', async () => {
|
||||
// Arrange
|
||||
const expectedTitle = 'Hello World';
|
||||
|
||||
// Act
|
||||
spectator.component.title = expectedTitle;
|
||||
spectator.detectChanges();
|
||||
component.title = expectedTitle;
|
||||
// If using Angular, trigger change detection:
|
||||
// fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(spectator.query('h1')).toHaveText(expectedTitle);
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toBe(expectedTitle);
|
||||
});
|
||||
|
||||
it('should handle error cases gracefully', () => {
|
||||
@@ -52,15 +57,17 @@ describe('MyComponent', () => {
|
||||
const invalidInput = null;
|
||||
|
||||
// Act
|
||||
spectator.component.input = invalidInput;
|
||||
component.input = invalidInput;
|
||||
|
||||
// Assert
|
||||
expect(() => spectator.component.processInput()).toThrowError('Invalid input');
|
||||
expect(() => component.processInput()).toThrowError('Invalid input');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Jest Documentation](https://jestjs.io/docs/getting-started)
|
||||
- [Spectator Documentation](https://ngneat.github.io/spectator/)
|
||||
- [Vitest Documentation (latest)](https://context7.com/vitest-dev/vitest/llms.txt?topic=getting+started)
|
||||
- [Vitest Official Guide](https://vitest.dev/guide/)
|
||||
- [Testing Library for Angular](https://testing-library.com/docs/angular-testing-library/intro/)
|
||||
- **Jest** and **Spectator** documentation are deprecated
|
||||
|
||||
147
.gitignore
vendored
147
.gitignore
vendored
@@ -1,70 +1,77 @@
|
||||
# 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
|
||||
|
||||
nx.instructions.md
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ WORKDIR /app
|
||||
COPY . .
|
||||
RUN umask 0022
|
||||
RUN npm version ${SEMVERSION}
|
||||
RUN npm install --foreground-scripts --legacy-peer-deps
|
||||
RUN npm install --foreground-scripts
|
||||
RUN if [ "${IS_PRODUCTION}" = "true" ] ; then npm run-script build-prod ; else npm run-script build ; fi
|
||||
|
||||
# stage final
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -38,7 +38,7 @@
|
||||
],
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/ui.scss",
|
||||
"apps/isa-app/src/tailwind.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
@@ -128,7 +128,7 @@
|
||||
],
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/ui.scss",
|
||||
"apps/isa-app/src/tailwind.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
]
|
||||
},
|
||||
@@ -148,7 +148,7 @@
|
||||
"compodoc": false,
|
||||
"styles": [
|
||||
"@angular/cdk/overlay-prebuilt.css",
|
||||
"apps/isa-app/src/ui.scss",
|
||||
"apps/isa-app/src/tailwind.scss",
|
||||
"apps/isa-app/src/styles.scss"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
withInterceptorsFromDi,
|
||||
} from '@angular/common/http';
|
||||
import {
|
||||
DEFAULT_CURRENCY_CODE,
|
||||
ErrorHandler,
|
||||
Injector,
|
||||
LOCALE_ID,
|
||||
@@ -228,6 +229,10 @@ export function _notificationsHubOptionsFactory(
|
||||
withRouteData(),
|
||||
),
|
||||
provideLogging(withLogLevel(LogLevel.Debug), withSink(ConsoleLogSink)),
|
||||
{
|
||||
provide: DEFAULT_CURRENCY_CODE,
|
||||
useValue: 'EUR',
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -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,11 +1,16 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { ErrorHandler, Injectable } from '@angular/core';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { DialogModel, UiDialogModalComponent, UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { IsaLogProvider } from './isa.log-provider';
|
||||
import { LogLevel } from '@core/logger';
|
||||
import { HttpErrorResponse } from "@angular/common/http";
|
||||
import { ErrorHandler, Injectable } from "@angular/core";
|
||||
import { AuthService } from "@core/auth";
|
||||
import {
|
||||
DialogModel,
|
||||
UiDialogModalComponent,
|
||||
UiErrorModalComponent,
|
||||
UiModalService,
|
||||
} from "@ui/modal";
|
||||
import { IsaLogProvider } from "./isa.log-provider";
|
||||
import { LogLevel } from "@core/logger";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class IsaErrorHandler implements ErrorHandler {
|
||||
constructor(
|
||||
private _modal: UiModalService,
|
||||
@@ -17,7 +22,7 @@ export class IsaErrorHandler implements ErrorHandler {
|
||||
console.error(error);
|
||||
|
||||
// Bei Klick auf Abbrechen auf der Login Seite erneut zur Login Seite weiterleiten
|
||||
if (error?.type === 'token_error') {
|
||||
if (error?.type === "token_error") {
|
||||
this._authService.login();
|
||||
return;
|
||||
}
|
||||
@@ -26,11 +31,14 @@ export class IsaErrorHandler implements ErrorHandler {
|
||||
await this._modal
|
||||
.open({
|
||||
content: UiDialogModalComponent,
|
||||
title: 'Sitzung abgelaufen',
|
||||
title: "Sitzung abgelaufen",
|
||||
data: {
|
||||
handleCommand: false,
|
||||
content: 'Sie waren zu lange nicht in der ISA aktiv. Bitte melden Sie sich erneut an',
|
||||
actions: [{ command: 'CLOSE', selected: true, label: 'Erneut anmelden' }],
|
||||
content:
|
||||
"Sie waren zu lange nicht in der ISA aktiv. Bitte melden Sie sich erneut an",
|
||||
actions: [
|
||||
{ command: "CLOSE", selected: true, label: "Erneut anmelden" },
|
||||
],
|
||||
} as DialogModel,
|
||||
})
|
||||
.afterClosed$.toPromise();
|
||||
@@ -39,7 +47,11 @@ export class IsaErrorHandler implements ErrorHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isaLogProvider.log(LogLevel.ERROR, 'Client Error', error);
|
||||
try {
|
||||
this._isaLogProvider.log(LogLevel.ERROR, "Client Error", error);
|
||||
} catch (logError) {
|
||||
console.error("Error logging to IsaLogProvider:", logError);
|
||||
}
|
||||
|
||||
// this._modal.open({
|
||||
// content: UiErrorModalComponent,
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { LogLevel, LogProvider } from '@core/logger';
|
||||
import { UserStateService } from '@generated/swagger/isa-api';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { LogLevel, LogProvider } from "@core/logger";
|
||||
import { UserStateService } from "@generated/swagger/isa-api";
|
||||
import { environment } from "../../environments/environment";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class IsaLogProvider implements LogProvider {
|
||||
static InfoService: UserStateService | undefined;
|
||||
|
||||
constructor() {}
|
||||
|
||||
log(logLevel: LogLevel, message: string, error: Error, ...optionalParams: any[]): void {
|
||||
if (!environment.production && (logLevel === LogLevel.WARN || logLevel === LogLevel.ERROR)) {
|
||||
IsaLogProvider.InfoService?.UserStateSaveLog({
|
||||
logType: logLevel,
|
||||
message: message,
|
||||
content: JSON.stringify({
|
||||
error: error?.name,
|
||||
message: error?.message,
|
||||
stack: error?.stack,
|
||||
data: optionalParams,
|
||||
}),
|
||||
})
|
||||
.toPromise()
|
||||
.catch(() => {});
|
||||
log(
|
||||
logLevel: LogLevel,
|
||||
message: string,
|
||||
error: Error,
|
||||
...optionalParams: any[]
|
||||
): void {
|
||||
try {
|
||||
if (
|
||||
!environment.production &&
|
||||
(logLevel === LogLevel.WARN || logLevel === LogLevel.ERROR)
|
||||
) {
|
||||
IsaLogProvider.InfoService?.UserStateSaveLog({
|
||||
logType: logLevel,
|
||||
message: message,
|
||||
content: JSON.stringify({
|
||||
error: error?.name,
|
||||
message: error?.message,
|
||||
stack: error?.stack,
|
||||
data: optionalParams,
|
||||
}),
|
||||
}).toPromise();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error logging to InfoService:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { LogLevel } from './log-level';
|
||||
import { LogLevel } from "./log-level";
|
||||
|
||||
export interface LogProvider {
|
||||
log(logLevel: LogLevel, message: string, ...optionalParams: any[]): void;
|
||||
|
||||
@@ -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,350 @@
|
||||
<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>
|
||||
}
|
||||
<div class="side-menu-group-sub-item-wrapper">
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="[
|
||||
'/',
|
||||
processService.activatedTab()?.id || processService.nextId(),
|
||||
'remission',
|
||||
]"
|
||||
(isActiveChange)="focusSearchBox(); remissionExpanded.set($event)"
|
||||
routerLinkActive="active"
|
||||
#rlActive="routerLinkActive"
|
||||
>
|
||||
<span class="side-menu-group-item-icon w-[2.375rem] h-12">
|
||||
<ng-icon name="isaNavigationRemission2" size="1.5rem"></ng-icon>
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Remission</span>
|
||||
<button
|
||||
class="side-menu-group-arrow"
|
||||
[class.side-menu-item-rotate]="remissionExpanded()"
|
||||
(click)="
|
||||
$event.stopPropagation();
|
||||
$event.preventDefault();
|
||||
remissionExpanded.set(!remissionExpanded())
|
||||
"
|
||||
>
|
||||
<shared-icon icon="keyboard-arrow-down"></shared-icon>
|
||||
</button>
|
||||
</a>
|
||||
@if (remissionExpanded()) {
|
||||
<div class="side-menu-group-sub-items">
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
[routerLink]="[
|
||||
'/',
|
||||
processService.activatedTab()?.id || processService.nextId(),
|
||||
'remission',
|
||||
]"
|
||||
(isActiveChange)="focusSearchBox()"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-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()"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"> </span>
|
||||
<span class="side-menu-group-item-label">Warenbegleitscheine</span>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,435 +1,434 @@
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Inject,
|
||||
ChangeDetectorRef,
|
||||
inject,
|
||||
DOCUMENT,
|
||||
} from '@angular/core';
|
||||
import { AuthModule, AuthService } from '@core/auth';
|
||||
import { StockService } from '@generated/swagger/wws-api';
|
||||
import { first, map, retry, switchMap, take } from 'rxjs/operators';
|
||||
import { ShellService } from '../shell.service';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Config } from '@core/config';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { RegexRouterLinkActiveDirective } from '@shared/directives/router-link-active';
|
||||
import { WrongDestinationModalService } from '@modal/wrong-destination';
|
||||
import {
|
||||
CustomerCreateNavigation,
|
||||
CustomerOrdersNavigationService,
|
||||
CustomerSearchNavigation,
|
||||
PickupShelfInNavigationService,
|
||||
PickUpShelfOutNavigationService,
|
||||
ProductCatalogNavigationService,
|
||||
} from '@shared/services/navigation';
|
||||
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationRemission2, isaNavigationReturn } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-side-menu',
|
||||
templateUrl: 'side-menu.component.html',
|
||||
styleUrls: ['side-menu.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIconComponent,
|
||||
IconComponent,
|
||||
RouterModule,
|
||||
AuthModule,
|
||||
RegexRouterLinkActiveDirective,
|
||||
],
|
||||
providers: [provideIcons({ isaNavigationReturn, isaNavigationRemission2 })],
|
||||
})
|
||||
export class ShellSideMenuComponent {
|
||||
processService = inject(TabService);
|
||||
|
||||
branchKey$ = this._stockService.StockCurrentBranch().pipe(
|
||||
retry(3),
|
||||
map((x) => x.result.key),
|
||||
);
|
||||
|
||||
section$ = this._app.getSection$();
|
||||
|
||||
processes$ = this.section$.pipe(
|
||||
switchMap((section) => this._app.getProcesses$(section)),
|
||||
);
|
||||
|
||||
processesCount$ = this.processes$.pipe(
|
||||
map((processes) => processes?.length ?? 0),
|
||||
);
|
||||
|
||||
activeProcess$ = this._app.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this._app.getProcessById$(processId)),
|
||||
);
|
||||
|
||||
get isTablet() {
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
customerBasePath$ = this.activeProcess$.pipe(
|
||||
map((process) => {
|
||||
if (
|
||||
!!process &&
|
||||
process.section === 'customer' &&
|
||||
process.type !== 'cart-checkout'
|
||||
) {
|
||||
// Übernehme aktiven Prozess
|
||||
return `/kunde/${process.id}`;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return '/kunde';
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
customerSearchRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
return this._customerSearchNavigation.defaultRoute({ processId });
|
||||
}),
|
||||
);
|
||||
|
||||
customerCreateRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
return this._customerCreateNavigation.defaultRoute({ processId });
|
||||
}),
|
||||
);
|
||||
|
||||
pickUpShelfOutRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this._pickUpShelfOutNavigation.defaultRoute({ processId }).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this._pickUpShelfOutNavigation.defaultRoute({}).path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
productRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this._catalogNavigationService.getArticleSearchBasePath(
|
||||
processId,
|
||||
).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this._catalogNavigationService.getArticleSearchBasePath().path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
customerOrdersRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this._customerOrdersNavigationService.getCustomerOrdersBasePath(
|
||||
processId,
|
||||
).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this._customerOrdersNavigationService.getCustomerOrdersBasePath()
|
||||
.path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
taskCalenderNavigation$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.taskCalendar'),
|
||||
{
|
||||
path: ['/filiale', 'task-calendar'],
|
||||
queryParams: {},
|
||||
},
|
||||
'/filiale/task-calendar',
|
||||
);
|
||||
|
||||
assortmentNavigation$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.assortment'),
|
||||
{
|
||||
path: ['/filiale', 'assortment'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
pickUpShelfInRoutePath$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.pickupShelf'),
|
||||
this._pickUpShelfInNavigation.defaultRoute(),
|
||||
'/filiale/pickup-shelf',
|
||||
);
|
||||
|
||||
// #4478 - RD // Abholfach - Routing löst Suche aus
|
||||
// pickUpShelfInListRoutePath$ = this.getLastNavigationByProcessId(
|
||||
// this._config.get('process.ids.pickupShelf'),
|
||||
// this._pickUpShelfInNavigation.listRoute()
|
||||
// );
|
||||
|
||||
remissionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.remission'),
|
||||
{
|
||||
path: ['/filiale', 'remission'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
packageInspectionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this._config.get('process.ids.packageInspection'),
|
||||
{
|
||||
path: ['/filiale', 'package-inspection'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
get currentShelfView$() {
|
||||
return this._route.queryParams.pipe(map((params) => params.view));
|
||||
}
|
||||
|
||||
shelfExpanded = false;
|
||||
customerExpanded = false;
|
||||
|
||||
constructor(
|
||||
private _shellService: ShellService,
|
||||
private _authService: AuthService,
|
||||
private _stockService: StockService,
|
||||
private _app: ApplicationService,
|
||||
private _router: Router,
|
||||
private _route: ActivatedRoute,
|
||||
private readonly _wrongDestinationModalService: WrongDestinationModalService,
|
||||
private _environment: EnvironmentService,
|
||||
private _catalogNavigationService: ProductCatalogNavigationService,
|
||||
private _customerOrdersNavigationService: CustomerOrdersNavigationService,
|
||||
private _config: Config,
|
||||
private _breadcrumbService: BreadcrumbService,
|
||||
private _customerSearchNavigation: CustomerSearchNavigation,
|
||||
private _customerCreateNavigation: CustomerCreateNavigation,
|
||||
private _pickUpShelfOutNavigation: PickUpShelfOutNavigationService,
|
||||
private _pickUpShelfInNavigation: PickupShelfInNavigationService,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
@Inject(DOCUMENT) private readonly _document: Document,
|
||||
) {}
|
||||
|
||||
customerActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
this.expandCustomer();
|
||||
}
|
||||
}
|
||||
|
||||
shelfActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
this.expandShelf();
|
||||
}
|
||||
}
|
||||
|
||||
expandCustomer() {
|
||||
this.customerExpanded = true;
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
expandShelf() {
|
||||
this.shelfExpanded = true;
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
getLastNavigationByProcessId(
|
||||
id: number,
|
||||
fallback?: { path: string[]; queryParams: unknown },
|
||||
pathContainsString?: string,
|
||||
) {
|
||||
return this._breadcrumbService.getBreadcrumbByKey$(id)?.pipe(
|
||||
map((breadcrumbs) => {
|
||||
const lastCrumb = breadcrumbs
|
||||
.filter((breadcrumb) => {
|
||||
/**
|
||||
* #4532 - Der optionale Filter wurde hinzugefügt Breadcrumbs mit fehlerhaften Pfad auszuschließen.
|
||||
* Dieser Filter kann entfernt werden, sobald die Breadcrumbs korrekt gesetzt werden. Jedoch konnte man bisher nicht feststellen,
|
||||
* woher die fehlerhaften Breadcrumbs kommen.
|
||||
*/
|
||||
if (!pathContainsString) {
|
||||
// Wenn kein Filter gesetzt ist, dann wird der letzte Breadcrumb zurückgegeben
|
||||
return true;
|
||||
}
|
||||
|
||||
const pathStr = Array.isArray(breadcrumb.path)
|
||||
? breadcrumb.path.join('/')
|
||||
: breadcrumb.path;
|
||||
return pathStr.includes(pathContainsString);
|
||||
})
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
.filter((breadcrumb) => !breadcrumb?.params?.hasOwnProperty('view'))
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('reservation'))
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('cleanup'))
|
||||
.filter(
|
||||
(breadcrumb) => !breadcrumb?.tags?.includes('wareneingangsliste'),
|
||||
)
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('preview'))
|
||||
.reduce((last, current) => {
|
||||
if (!last) return current;
|
||||
|
||||
if (last.changed > current.changed) {
|
||||
return last;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
}, undefined);
|
||||
|
||||
if (!lastCrumb) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// #4692 Return Fallback if Values contain undefined or null values, regardless if path is from type string or array
|
||||
if (typeof lastCrumb?.path === 'string') {
|
||||
if (
|
||||
lastCrumb?.path?.includes('undefined') ||
|
||||
lastCrumb?.path?.includes('null')
|
||||
) {
|
||||
return fallback;
|
||||
}
|
||||
} else {
|
||||
const valuesToCheck = [];
|
||||
|
||||
// eslint-disable-next-line no-unsafe-optional-chaining
|
||||
for (const value of lastCrumb?.path) {
|
||||
if (
|
||||
value?.outlets &&
|
||||
value?.outlets?.primary &&
|
||||
value?.outlets?.side
|
||||
) {
|
||||
valuesToCheck.push(
|
||||
...Object.values(value?.outlets?.primary),
|
||||
...Object.values(value?.outlets?.side),
|
||||
);
|
||||
} else {
|
||||
valuesToCheck.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.checkIfArrayContainsUndefinedOrNull(valuesToCheck)) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return { path: lastCrumb.path, queryParams: lastCrumb.params };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
checkIfArrayContainsUndefinedOrNull(array: unknown[]) {
|
||||
return (
|
||||
array?.includes(undefined) ||
|
||||
array?.includes('undefined') ||
|
||||
array?.includes(null) ||
|
||||
array?.includes('null')
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedCustomerProcessId$() {
|
||||
return this._app.getProcesses$('customer').pipe(
|
||||
map((processes) => {
|
||||
const lastCustomerProcess = processes
|
||||
.filter((process) => process.type === 'cart')
|
||||
.reduce((last, current) => {
|
||||
if (!last) return current;
|
||||
|
||||
if (last.activated > current.activated) {
|
||||
return last;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
}, undefined);
|
||||
|
||||
return lastCustomerProcess?.id ?? Date.now();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
closeSideMenu() {
|
||||
this._shellService.closeSideMenu();
|
||||
}
|
||||
|
||||
logout() {
|
||||
this._authService.logout();
|
||||
}
|
||||
|
||||
async resetBranch() {
|
||||
const process = await this.activeProcess$.pipe(first()).toPromise();
|
||||
if (process?.id) {
|
||||
this._app.patchProcessData(process.id, { selectedBranch: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
focusSearchBox() {
|
||||
setTimeout(() => this._document.getElementById('searchbox')?.focus(), 0);
|
||||
}
|
||||
|
||||
async createProcess() {
|
||||
const process = await this.createCartProcess();
|
||||
this.navigateToCatalog(process);
|
||||
}
|
||||
|
||||
async createCartProcess() {
|
||||
const nextProcessName = await this.getNextProcessName();
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: this.getNextProcessId(),
|
||||
type: 'cart',
|
||||
name: nextProcessName,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
this._app.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async getNextProcessName() {
|
||||
let processes = await this._app
|
||||
.getProcesses$('customer')
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
processes = processes.filter(
|
||||
(x) => x.type === 'cart' && x.name.startsWith('Vorgang '),
|
||||
);
|
||||
|
||||
const maxProcessNumber = processes.reduce((max, process) => {
|
||||
const number = parseInt(process.name.replace('Vorgang ', ''), 10);
|
||||
return number > max ? number : max;
|
||||
}, 0);
|
||||
|
||||
return `Vorgang ${maxProcessNumber + 1}`;
|
||||
}
|
||||
|
||||
getNextProcessId() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
async navigateToCatalog(process: ApplicationProcess) {
|
||||
await this._catalogNavigationService
|
||||
.getArticleSearchBasePath(process.id)
|
||||
.navigate();
|
||||
}
|
||||
|
||||
navigateToDashboard() {
|
||||
this._router.navigate(['/kunde', 'dashboard']);
|
||||
}
|
||||
|
||||
async closeAllProcesses() {
|
||||
const processes = await this.processes$.pipe(take(1)).toPromise();
|
||||
|
||||
processes.forEach((process) => this._app.removeProcess(process.id));
|
||||
|
||||
this.navigateToDashboard();
|
||||
}
|
||||
|
||||
fetchAndOpenPackages = () =>
|
||||
this._wrongDestinationModalService.fetchAndOpen();
|
||||
}
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Inject,
|
||||
ChangeDetectorRef,
|
||||
inject,
|
||||
DOCUMENT,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { AuthModule, AuthService } from '@core/auth';
|
||||
import { StockService } from '@generated/swagger/wws-api';
|
||||
import { first, map, retry, switchMap, take } from 'rxjs/operators';
|
||||
import { ShellService } from '../shell.service';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Config } from '@core/config';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { RegexRouterLinkActiveDirective } from '@shared/directives/router-link-active';
|
||||
import { WrongDestinationModalService } from '@modal/wrong-destination';
|
||||
import {
|
||||
CustomerCreateNavigation,
|
||||
CustomerOrdersNavigationService,
|
||||
CustomerSearchNavigation,
|
||||
PickupShelfInNavigationService,
|
||||
PickUpShelfOutNavigationService,
|
||||
ProductCatalogNavigationService,
|
||||
} from '@shared/services/navigation';
|
||||
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationRemission2, isaNavigationReturn } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-side-menu',
|
||||
templateUrl: 'side-menu.component.html',
|
||||
styleUrls: ['side-menu.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgIconComponent,
|
||||
IconComponent,
|
||||
RouterModule,
|
||||
AuthModule,
|
||||
RegexRouterLinkActiveDirective,
|
||||
],
|
||||
providers: [provideIcons({ isaNavigationReturn, isaNavigationRemission2 })],
|
||||
})
|
||||
export class ShellSideMenuComponent {
|
||||
#shellService = inject(ShellService);
|
||||
#authService = inject(AuthService);
|
||||
#stockService = inject(StockService);
|
||||
#app = inject(ApplicationService);
|
||||
#router = inject(Router);
|
||||
#route = inject(ActivatedRoute);
|
||||
#wrongDestinationModalService = inject(WrongDestinationModalService);
|
||||
#environment = inject(EnvironmentService);
|
||||
#catalogNavigationService = inject(ProductCatalogNavigationService);
|
||||
#customerOrdersNavigationService = inject(CustomerOrdersNavigationService);
|
||||
#config = inject(Config);
|
||||
#breadcrumbService = inject(BreadcrumbService);
|
||||
#customerSearchNavigation = inject(CustomerSearchNavigation);
|
||||
#customerCreateNavigation = inject(CustomerCreateNavigation);
|
||||
#pickUpShelfOutNavigation = inject(PickUpShelfOutNavigationService);
|
||||
#pickUpShelfInNavigation = inject(PickupShelfInNavigationService);
|
||||
#cdr = inject(ChangeDetectorRef);
|
||||
#document = inject(DOCUMENT);
|
||||
processService = inject(TabService);
|
||||
|
||||
branchKey$ = this.#stockService.StockCurrentBranch().pipe(
|
||||
retry(3),
|
||||
map((x) => x.result.key),
|
||||
);
|
||||
|
||||
section$ = this.#app.getSection$();
|
||||
|
||||
processes$ = this.section$.pipe(
|
||||
switchMap((section) => this.#app.getProcesses$(section)),
|
||||
);
|
||||
|
||||
processesCount$ = this.processes$.pipe(
|
||||
map((processes) => processes?.length ?? 0),
|
||||
);
|
||||
|
||||
activeProcess$ = this.#app.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.#app.getProcessById$(processId)),
|
||||
);
|
||||
|
||||
get isTablet() {
|
||||
return this.#environment.matchTablet();
|
||||
}
|
||||
|
||||
customerBasePath$ = this.activeProcess$.pipe(
|
||||
map((process) => {
|
||||
if (
|
||||
!!process &&
|
||||
process.section === 'customer' &&
|
||||
process.type !== 'cart-checkout'
|
||||
) {
|
||||
// Übernehme aktiven Prozess
|
||||
return `/kunde/${process.id}`;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return '/kunde';
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
customerSearchRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
return this.#customerSearchNavigation.defaultRoute({ processId });
|
||||
}),
|
||||
);
|
||||
|
||||
customerCreateRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
return this.#customerCreateNavigation.defaultRoute({ processId });
|
||||
}),
|
||||
);
|
||||
|
||||
pickUpShelfOutRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this.#pickUpShelfOutNavigation.defaultRoute({ processId }).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this.#pickUpShelfOutNavigation.defaultRoute({}).path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
productRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this.#catalogNavigationService.getArticleSearchBasePath(
|
||||
processId,
|
||||
).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this.#catalogNavigationService.getArticleSearchBasePath().path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
customerOrdersRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
// Übernehme aktiven Prozess
|
||||
return this.#customerOrdersNavigationService.getCustomerOrdersBasePath(
|
||||
processId,
|
||||
).path;
|
||||
} else {
|
||||
// Über Guards wird ein neuer Prozess erstellt
|
||||
return this.#customerOrdersNavigationService.getCustomerOrdersBasePath()
|
||||
.path;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
taskCalenderNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.taskCalendar'),
|
||||
{
|
||||
path: ['/filiale', 'task-calendar'],
|
||||
queryParams: {},
|
||||
},
|
||||
'/filiale/task-calendar',
|
||||
);
|
||||
|
||||
assortmentNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.assortment'),
|
||||
{
|
||||
path: ['/filiale', 'assortment'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
pickUpShelfInRoutePath$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.pickupShelf'),
|
||||
this.#pickUpShelfInNavigation.defaultRoute(),
|
||||
'/filiale/pickup-shelf',
|
||||
);
|
||||
|
||||
// #4478 - RD // Abholfach - Routing löst Suche aus
|
||||
// pickUpShelfInListRoutePath$ = this.getLastNavigationByProcessId(
|
||||
// this._config.get('process.ids.pickupShelf'),
|
||||
// this._pickUpShelfInNavigation.listRoute()
|
||||
// );
|
||||
|
||||
remissionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.remission'),
|
||||
{
|
||||
path: ['/filiale', 'remission'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
packageInspectionNavigation$ = this.getLastNavigationByProcessId(
|
||||
this.#config.get('process.ids.packageInspection'),
|
||||
{
|
||||
path: ['/filiale', 'package-inspection'],
|
||||
queryParams: {},
|
||||
},
|
||||
);
|
||||
|
||||
get currentShelfView$() {
|
||||
return this.#route.queryParams.pipe(map((params) => params.view));
|
||||
}
|
||||
|
||||
shelfExpanded = false;
|
||||
customerExpanded = false;
|
||||
remissionExpanded = signal(false);
|
||||
|
||||
customerActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
this.expandCustomer();
|
||||
}
|
||||
}
|
||||
|
||||
shelfActive(isActive: boolean) {
|
||||
if (isActive) {
|
||||
this.expandShelf();
|
||||
}
|
||||
}
|
||||
|
||||
expandCustomer() {
|
||||
this.customerExpanded = true;
|
||||
this.#cdr.markForCheck();
|
||||
}
|
||||
|
||||
expandShelf() {
|
||||
this.shelfExpanded = true;
|
||||
this.#cdr.markForCheck();
|
||||
}
|
||||
|
||||
getLastNavigationByProcessId(
|
||||
id: number,
|
||||
fallback?: { path: string[]; queryParams: unknown },
|
||||
pathContainsString?: string,
|
||||
) {
|
||||
return this.#breadcrumbService.getBreadcrumbByKey$(id)?.pipe(
|
||||
map((breadcrumbs) => {
|
||||
const lastCrumb = breadcrumbs
|
||||
.filter((breadcrumb) => {
|
||||
/**
|
||||
* #4532 - Der optionale Filter wurde hinzugefügt Breadcrumbs mit fehlerhaften Pfad auszuschließen.
|
||||
* Dieser Filter kann entfernt werden, sobald die Breadcrumbs korrekt gesetzt werden. Jedoch konnte man bisher nicht feststellen,
|
||||
* woher die fehlerhaften Breadcrumbs kommen.
|
||||
*/
|
||||
if (!pathContainsString) {
|
||||
// Wenn kein Filter gesetzt ist, dann wird der letzte Breadcrumb zurückgegeben
|
||||
return true;
|
||||
}
|
||||
|
||||
const pathStr = Array.isArray(breadcrumb.path)
|
||||
? breadcrumb.path.join('/')
|
||||
: breadcrumb.path;
|
||||
return pathStr.includes(pathContainsString);
|
||||
})
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
.filter((breadcrumb) => !breadcrumb?.params?.hasOwnProperty('view'))
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('reservation'))
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('cleanup'))
|
||||
.filter(
|
||||
(breadcrumb) => !breadcrumb?.tags?.includes('wareneingangsliste'),
|
||||
)
|
||||
.filter((breadcrumb) => !breadcrumb?.tags?.includes('preview'))
|
||||
.reduce((last, current) => {
|
||||
if (!last) return current;
|
||||
|
||||
if (last.changed > current.changed) {
|
||||
return last;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
}, undefined);
|
||||
|
||||
if (!lastCrumb) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// #4692 Return Fallback if Values contain undefined or null values, regardless if path is from type string or array
|
||||
if (typeof lastCrumb?.path === 'string') {
|
||||
if (
|
||||
lastCrumb?.path?.includes('undefined') ||
|
||||
lastCrumb?.path?.includes('null')
|
||||
) {
|
||||
return fallback;
|
||||
}
|
||||
} else {
|
||||
const valuesToCheck = [];
|
||||
|
||||
// eslint-disable-next-line no-unsafe-optional-chaining
|
||||
for (const value of lastCrumb?.path) {
|
||||
if (
|
||||
value?.outlets &&
|
||||
value?.outlets?.primary &&
|
||||
value?.outlets?.side
|
||||
) {
|
||||
valuesToCheck.push(
|
||||
...Object.values(value?.outlets?.primary),
|
||||
...Object.values(value?.outlets?.side),
|
||||
);
|
||||
} else {
|
||||
valuesToCheck.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.checkIfArrayContainsUndefinedOrNull(valuesToCheck)) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return { path: lastCrumb.path, queryParams: lastCrumb.params };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
checkIfArrayContainsUndefinedOrNull(array: unknown[]) {
|
||||
return (
|
||||
array?.includes(undefined) ||
|
||||
array?.includes('undefined') ||
|
||||
array?.includes(null) ||
|
||||
array?.includes('null')
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedCustomerProcessId$() {
|
||||
return this.#app.getProcesses$('customer').pipe(
|
||||
map((processes) => {
|
||||
const lastCustomerProcess = processes
|
||||
.filter((process) => process.type === 'cart')
|
||||
.reduce((last, current) => {
|
||||
if (!last) return current;
|
||||
|
||||
if (last.activated > current.activated) {
|
||||
return last;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
}, undefined);
|
||||
|
||||
return lastCustomerProcess?.id ?? Date.now();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
closeSideMenu() {
|
||||
this.#shellService.closeSideMenu();
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.#authService.logout();
|
||||
}
|
||||
|
||||
async resetBranch() {
|
||||
const process = await this.activeProcess$.pipe(first()).toPromise();
|
||||
if (process?.id) {
|
||||
this.#app.patchProcessData(process.id, { selectedBranch: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
focusSearchBox() {
|
||||
setTimeout(() => this.#document.getElementById('searchbox')?.focus(), 0);
|
||||
}
|
||||
|
||||
async createProcess() {
|
||||
const process = await this.createCartProcess();
|
||||
this.navigateToCatalog(process);
|
||||
}
|
||||
|
||||
async createCartProcess() {
|
||||
const nextProcessName = await this.getNextProcessName();
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: this.getNextProcessId(),
|
||||
type: 'cart',
|
||||
name: nextProcessName,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
this.#app.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async getNextProcessName() {
|
||||
let processes = await this.#app
|
||||
.getProcesses$('customer')
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
processes = processes.filter(
|
||||
(x) => x.type === 'cart' && x.name.startsWith('Vorgang '),
|
||||
);
|
||||
|
||||
const maxProcessNumber = processes.reduce((max, process) => {
|
||||
const number = parseInt(process.name.replace('Vorgang ', ''), 10);
|
||||
return number > max ? number : max;
|
||||
}, 0);
|
||||
|
||||
return `Vorgang ${maxProcessNumber + 1}`;
|
||||
}
|
||||
|
||||
getNextProcessId() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
async navigateToCatalog(process: ApplicationProcess) {
|
||||
await this.#catalogNavigationService
|
||||
.getArticleSearchBasePath(process.id)
|
||||
.navigate();
|
||||
}
|
||||
|
||||
navigateToDashboard() {
|
||||
this.#router.navigate(['/kunde', 'dashboard']);
|
||||
}
|
||||
|
||||
async closeAllProcesses() {
|
||||
const processes = await this.processes$.pipe(take(1)).toPromise();
|
||||
|
||||
processes.forEach((process) => this.#app.removeProcess(process.id));
|
||||
|
||||
this.navigateToDashboard();
|
||||
}
|
||||
|
||||
fetchAndOpenPackages = () =>
|
||||
this.#wrongDestinationModalService.fetchAndOpen();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { type Meta, type StoryObj, argsToTemplate } from '@storybook/angular';
|
||||
import { ProductShelfMetaInfoComponent } from '@isa/remission/shared/product';
|
||||
|
||||
const meta: Meta<ProductShelfMetaInfoComponent> = {
|
||||
component: ProductShelfMetaInfoComponent,
|
||||
title: 'remission/shared/product/ProductShelfMetaInfoComponent',
|
||||
args: {
|
||||
department: 'Reise',
|
||||
shelfLabel: 'Europa',
|
||||
productGroupKey: '311',
|
||||
productGroupValue: 'Romane TB',
|
||||
assortment: 'Basissortiment|BPrämienartikel|n',
|
||||
returnReason: 'Beschädigt',
|
||||
},
|
||||
argTypes: {
|
||||
department: {
|
||||
control: { type: 'text' },
|
||||
description: 'The department of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
shelfLabel: {
|
||||
control: { type: 'text' },
|
||||
description: 'The shelf label of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
productGroupKey: {
|
||||
control: { type: 'text' },
|
||||
description: 'The key of the product group.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
productGroupValue: {
|
||||
control: { type: 'text' },
|
||||
description: 'The value of the product group.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
assortment: {
|
||||
control: { type: 'text' },
|
||||
description: 'The assortment of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
returnReason: {
|
||||
control: { type: 'text' },
|
||||
description: 'The reason for the return of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<remi-product-shelf-meta-info ${argsToTemplate(args)}></remi-product-shelf-meta-info>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductShelfMetaInfoComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
department: 'Reise',
|
||||
shelfLabel: 'Europa',
|
||||
productGroupKey: '311',
|
||||
productGroupValue: 'Romane TB',
|
||||
assortment: 'Basissortiment|BPrämienartikel|n',
|
||||
returnReason: 'Beschädigt',
|
||||
},
|
||||
};
|
||||
@@ -1,50 +1,64 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { ProductStockInfoComponent } from '@isa/remission/shared/product';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
|
||||
const meta: Meta<ProductStockInfoComponent> = {
|
||||
component: ProductStockInfoComponent,
|
||||
title: 'remission/shared/product/ProductStockInfoComponent',
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
provideProductRouterLinkBuilder((ean: string) => ean),
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
stock: 100,
|
||||
stockToRemit: 20,
|
||||
},
|
||||
argTypes: {
|
||||
stock: {
|
||||
control: { type: 'number' },
|
||||
description: 'The current stock of the product.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
stockToRemit: {
|
||||
control: { type: 'number' },
|
||||
description: 'The amount of stock to remit.',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<remi-product-stock-info ${argsToTemplate({ info: args })}></remi-product-stock-info>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductStockInfoComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { ProductStockInfoComponent } from '@isa/remission/shared/product';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
|
||||
const meta: Meta<ProductStockInfoComponent> = {
|
||||
component: ProductStockInfoComponent,
|
||||
title: 'remission/shared/product/ProductStockInfoComponent',
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
provideProductRouterLinkBuilder((ean: string) => ean),
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
availableStock: 92,
|
||||
stockToRemit: 91,
|
||||
targetStock: 1,
|
||||
zob: 0,
|
||||
},
|
||||
argTypes: {
|
||||
availableStock: {
|
||||
control: { type: 'number' },
|
||||
description: 'Total available stock for the product.',
|
||||
},
|
||||
stockToRemit: {
|
||||
control: { type: 'number' },
|
||||
description: 'Stock quantity to remit.',
|
||||
},
|
||||
targetStock: {
|
||||
control: { type: 'number' },
|
||||
description: 'Target stock level after remittance.',
|
||||
},
|
||||
zob: {
|
||||
control: { type: 'number' },
|
||||
description: 'Min Stock Category Management Information.',
|
||||
defaultValue: 0,
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<remi-product-stock-info ${argsToTemplate(args)}></remi-product-stock-info>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductStockInfoComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
availableStock: 92,
|
||||
stockToRemit: 91,
|
||||
targetStock: 1,
|
||||
zob: 0,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { argsToTemplate, type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';
|
||||
import { EmptyStateComponent } from '@isa/ui/empty-state';
|
||||
import {
|
||||
argsToTemplate,
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { EmptyStateAppearance, EmptyStateComponent } from '@isa/ui/empty-state';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
|
||||
type EmptyStateComponentInputs = {
|
||||
title: string;
|
||||
description: string;
|
||||
appearance: EmptyStateAppearance;
|
||||
};
|
||||
|
||||
const meta: Meta<EmptyStateComponentInputs> = {
|
||||
@@ -22,6 +28,10 @@ const meta: Meta<EmptyStateComponentInputs> = {
|
||||
description: {
|
||||
control: 'text',
|
||||
},
|
||||
appearance: {
|
||||
control: 'select',
|
||||
options: Object.values(EmptyStateAppearance),
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
@@ -40,5 +50,6 @@ export const Default: Story = {
|
||||
args: {
|
||||
title: 'Keine Suchergebnisse',
|
||||
description: 'Suchen Sie nach einer Rechnungsnummer oder Kundennamen.',
|
||||
appearance: EmptyStateAppearance.NoResults,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
condition: and(ne(variables['Build.SourceBranch'], 'refs/heads/integration'), ne(variables['Build.SourceBranch'], 'refs/heads/master'), not(startsWith(variables['Build.SourceBranch'], 'refs/heads/hotfix/')), not(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')))
|
||||
variables:
|
||||
- name: DockerTagSourceBranch
|
||||
value: $[replace(variables['Build.SourceBranch'], '/', '_')]
|
||||
value: $[replace(variables['Build.SourceBranch'], '/', '-')]
|
||||
- name: 'DockerTag'
|
||||
value: |
|
||||
$(Build.BuildNumber)-$(Build.SourceVersion)
|
||||
|
||||
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: {},
|
||||
},
|
||||
];
|
||||
3
libs/catalogue/data-access/src/lib/index.ts
Normal file
3
libs/catalogue/data-access/src/lib/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './models';
|
||||
export * from './schemas';
|
||||
export * from './services';
|
||||
@@ -0,0 +1,222 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { CatalougeSearchService } from './catalouge-search.service';
|
||||
import { SearchService } from '@generated/swagger/cat-search-api';
|
||||
import { Item } from '../models';
|
||||
import { SearchByTermInput } from '../schemas/catalouge-search.schemas';
|
||||
|
||||
describe('CatalougeSearchService', () => {
|
||||
let service: CatalougeSearchService;
|
||||
let searchServiceSpy: jest.Mocked<SearchService>;
|
||||
|
||||
beforeEach(() => {
|
||||
const searchServiceMock = {
|
||||
SearchByEAN: jest.fn(),
|
||||
SearchSearch: jest.fn(),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CatalougeSearchService,
|
||||
{ provide: SearchService, useValue: searchServiceMock },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(CatalougeSearchService);
|
||||
searchServiceSpy = TestBed.inject(SearchService) as jest.Mocked<SearchService>;
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('searchByEans', () => {
|
||||
it('should return items when search is successful', (done) => {
|
||||
// Arrange
|
||||
const mockItems: Item[] = [
|
||||
{ id: 1, product: { name: 'Item 1' }, catalogAvailability: { available: true } } as unknown as Item,
|
||||
{ id: 2, product: { name: 'Item 2' }, catalogAvailability: { available: true } } as unknown as Item,
|
||||
];
|
||||
const mockResponse = {
|
||||
error: false,
|
||||
result: mockItems,
|
||||
};
|
||||
searchServiceSpy.SearchByEAN.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act
|
||||
service.searchByEans('123456789', '987654321').subscribe({
|
||||
next: (result) => {
|
||||
// Assert
|
||||
expect(result).toEqual(mockItems);
|
||||
expect(searchServiceSpy.SearchByEAN).toHaveBeenCalledWith(['123456789', '987654321']);
|
||||
done();
|
||||
},
|
||||
error: done.fail,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when response has error', (done) => {
|
||||
// Arrange
|
||||
const mockResponse = {
|
||||
error: true,
|
||||
message: 'Search failed',
|
||||
};
|
||||
searchServiceSpy.SearchByEAN.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act
|
||||
service.searchByEans('123456789').subscribe({
|
||||
next: () => done.fail('Should have thrown error'),
|
||||
error: (error) => {
|
||||
// Assert
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toBe('Search failed');
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle single EAN', (done) => {
|
||||
// Arrange
|
||||
const mockItems: Item[] = [{ id: 1, product: { name: 'Item 1' }, catalogAvailability: { available: true } } as unknown as Item];
|
||||
const mockResponse = {
|
||||
error: false,
|
||||
result: mockItems,
|
||||
};
|
||||
searchServiceSpy.SearchByEAN.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act
|
||||
service.searchByEans('123456789').subscribe({
|
||||
next: (result) => {
|
||||
// Assert
|
||||
expect(result).toEqual(mockItems);
|
||||
expect(searchServiceSpy.SearchByEAN).toHaveBeenCalledWith(['123456789']);
|
||||
done();
|
||||
},
|
||||
error: done.fail,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty EAN array', (done) => {
|
||||
// Arrange
|
||||
const mockResponse = {
|
||||
error: false,
|
||||
result: [],
|
||||
};
|
||||
searchServiceSpy.SearchByEAN.mockReturnValue(of(mockResponse));
|
||||
|
||||
// Act
|
||||
service.searchByEans().subscribe({
|
||||
next: (result) => {
|
||||
// Assert
|
||||
expect(result).toEqual([]);
|
||||
expect(searchServiceSpy.SearchByEAN).toHaveBeenCalledWith([]);
|
||||
done();
|
||||
},
|
||||
error: done.fail,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchByTerm', () => {
|
||||
it('should return search results when successful', async () => {
|
||||
// Arrange
|
||||
const mockItems: Item[] = [
|
||||
{ id: 1, product: { name: 'Test Item' }, catalogAvailability: { available: true } } as unknown as Item,
|
||||
];
|
||||
const mockResponse = {
|
||||
error: false,
|
||||
result: mockItems,
|
||||
total: 1,
|
||||
};
|
||||
searchServiceSpy.SearchSearch.mockReturnValue(of(mockResponse));
|
||||
|
||||
const params: SearchByTermInput = {
|
||||
searchTerm: 'test',
|
||||
skip: 0,
|
||||
take: 10,
|
||||
};
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Act
|
||||
const result = await service.searchByTerm(params, abortController.signal);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(searchServiceSpy.SearchSearch).toHaveBeenCalledWith({
|
||||
input: { qs: 'test' },
|
||||
skip: 0,
|
||||
take: 10,
|
||||
doNotTrack: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when response has error', async () => {
|
||||
// Arrange
|
||||
const mockResponse = {
|
||||
error: true,
|
||||
message: 'Search failed',
|
||||
};
|
||||
searchServiceSpy.SearchSearch.mockReturnValue(of(mockResponse));
|
||||
|
||||
const params: SearchByTermInput = {
|
||||
searchTerm: 'test',
|
||||
skip: 0,
|
||||
take: 10,
|
||||
};
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Act & Assert
|
||||
await expect(service.searchByTerm(params, abortController.signal))
|
||||
.rejects
|
||||
.toThrow('Search failed');
|
||||
});
|
||||
|
||||
it('should handle abort signal', async () => {
|
||||
// Arrange
|
||||
const abortController = new AbortController();
|
||||
const mockResponse = {
|
||||
error: false,
|
||||
result: [],
|
||||
};
|
||||
searchServiceSpy.SearchSearch.mockReturnValue(of(mockResponse));
|
||||
|
||||
const params: SearchByTermInput = {
|
||||
searchTerm: 'test',
|
||||
skip: 0,
|
||||
take: 10,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await service.searchByTerm(params, abortController.signal);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(searchServiceSpy.SearchSearch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use default values when not provided', async () => {
|
||||
// Arrange
|
||||
const mockResponse = {
|
||||
error: false,
|
||||
result: [],
|
||||
};
|
||||
searchServiceSpy.SearchSearch.mockReturnValue(of(mockResponse));
|
||||
|
||||
const params: SearchByTermInput = {
|
||||
searchTerm: 'test',
|
||||
};
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Act
|
||||
await service.searchByTerm(params, abortController.signal);
|
||||
|
||||
// Assert
|
||||
expect(searchServiceSpy.SearchSearch).toHaveBeenCalledWith({
|
||||
input: { qs: 'test' },
|
||||
skip: 0,
|
||||
take: 20,
|
||||
doNotTrack: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,52 +1,53 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { SearchService } from '@generated/swagger/cat-search-api';
|
||||
import { firstValueFrom, map, Observable } from 'rxjs';
|
||||
import { takeUntilAborted } from '@isa/common/data-access';
|
||||
import { Item } from '../models';
|
||||
import {
|
||||
SearchByTermInput,
|
||||
SearchByTermSchema,
|
||||
} from '../schemas/catalouge-search.schemas';
|
||||
import { ListResponseArgs } from '@isa/common/data-access';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CatalougeSearchService {
|
||||
#searchService = inject(SearchService);
|
||||
|
||||
searchByEans(...ean: string[]): Observable<Item[]> {
|
||||
return this.#searchService.SearchByEAN(ean).pipe(
|
||||
map((res) => {
|
||||
if (res.error) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res.result as Item[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async searchByTerm(
|
||||
params: SearchByTermInput,
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<ListResponseArgs<Item>> {
|
||||
const { searchTerm, skip, take } = SearchByTermSchema.parse(params);
|
||||
|
||||
const req$ = this.#searchService
|
||||
.SearchSearch({
|
||||
filter: {
|
||||
qs: searchTerm,
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
})
|
||||
.pipe(takeUntilAborted(abortSignal));
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res as ListResponseArgs<Item>;
|
||||
}
|
||||
}
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { SearchService } from '@generated/swagger/cat-search-api';
|
||||
import { firstValueFrom, map, Observable } from 'rxjs';
|
||||
import { takeUntilAborted } from '@isa/common/data-access';
|
||||
import { Item } from '../models';
|
||||
import {
|
||||
SearchByTermInput,
|
||||
SearchByTermSchema,
|
||||
} from '../schemas/catalouge-search.schemas';
|
||||
import { ListResponseArgs } from '@isa/common/data-access';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CatalougeSearchService {
|
||||
#searchService = inject(SearchService);
|
||||
|
||||
searchByEans(...ean: string[]): Observable<Item[]> {
|
||||
return this.#searchService.SearchByEAN(ean).pipe(
|
||||
map((res) => {
|
||||
if (res.error) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res.result as Item[];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async searchByTerm(
|
||||
params: SearchByTermInput,
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<ListResponseArgs<Item>> {
|
||||
const { searchTerm, skip, take } = SearchByTermSchema.parse(params);
|
||||
|
||||
const req$ = this.#searchService
|
||||
.SearchSearch({
|
||||
input: {
|
||||
qs: searchTerm,
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
doNotTrack: true,
|
||||
})
|
||||
.pipe(takeUntilAborted(abortSignal));
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res as ListResponseArgs<Item>;
|
||||
}
|
||||
}
|
||||
|
||||
34
libs/common/data-access/eslint.config.js
Normal file
34
libs/common/data-access/eslint.config.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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: {},
|
||||
},
|
||||
];
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './errors';
|
||||
export * from './models';
|
||||
export * from './errors';
|
||||
export * from './helpers';
|
||||
export * from './models';
|
||||
export * from './operators';
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { BatchResponseArgs } from './batch-response-args';
|
||||
import { ReturnValue } from './return-value';
|
||||
|
||||
describe('BatchResponseArgs', () => {
|
||||
describe('interface structure', () => {
|
||||
it('should support all properties', () => {
|
||||
// Arrange
|
||||
const testData: BatchResponseArgs<string> = {
|
||||
alreadyProcessed: [
|
||||
{ error: false, result: 'processed1' },
|
||||
{ error: false, result: 'processed2' },
|
||||
],
|
||||
ambiguous: ['ambiguous1', 'ambiguous2'],
|
||||
completed: true,
|
||||
duplicates: [
|
||||
{ key: 'key1', value: 1 },
|
||||
{ key: 'key2', value: 2 },
|
||||
],
|
||||
error: false,
|
||||
failed: [
|
||||
{ error: true, message: 'Failed', result: 'failed1' },
|
||||
],
|
||||
invalidProperties: { field1: 'Invalid value' },
|
||||
message: 'Success',
|
||||
requestId: 12345,
|
||||
successful: [
|
||||
{ key: 'key1', value: 'value1' },
|
||||
{ key: 'key2', value: 'value2' },
|
||||
],
|
||||
total: 10,
|
||||
unknown: [
|
||||
{ error: false, result: 'unknown1' },
|
||||
],
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.alreadyProcessed).toHaveLength(2);
|
||||
expect(testData.ambiguous).toHaveLength(2);
|
||||
expect(testData.completed).toBe(true);
|
||||
expect(testData.duplicates).toHaveLength(2);
|
||||
expect(testData.error).toBe(false);
|
||||
expect(testData.failed).toHaveLength(1);
|
||||
expect(testData.invalidProperties).toEqual({ field1: 'Invalid value' });
|
||||
expect(testData.message).toBe('Success');
|
||||
expect(testData.requestId).toBe(12345);
|
||||
expect(testData.successful).toHaveLength(2);
|
||||
expect(testData.total).toBe(10);
|
||||
expect(testData.unknown).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should support required properties only', () => {
|
||||
// Arrange
|
||||
const testData: BatchResponseArgs<number> = {
|
||||
completed: false,
|
||||
error: true,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.completed).toBe(false);
|
||||
expect(testData.error).toBe(true);
|
||||
expect(testData.total).toBe(0);
|
||||
expect(testData.alreadyProcessed).toBeUndefined();
|
||||
expect(testData.ambiguous).toBeUndefined();
|
||||
expect(testData.duplicates).toBeUndefined();
|
||||
expect(testData.failed).toBeUndefined();
|
||||
expect(testData.invalidProperties).toBeUndefined();
|
||||
expect(testData.message).toBeUndefined();
|
||||
expect(testData.requestId).toBeUndefined();
|
||||
expect(testData.successful).toBeUndefined();
|
||||
expect(testData.unknown).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should support generic type parameter', () => {
|
||||
// Arrange
|
||||
interface TestObject {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const testData: BatchResponseArgs<TestObject> = {
|
||||
completed: true,
|
||||
error: false,
|
||||
total: 1,
|
||||
successful: [
|
||||
{ key: { id: 1, name: 'test' }, value: { id: 1, name: 'test' } },
|
||||
],
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.successful?.[0].key.id).toBe(1);
|
||||
expect(testData.successful?.[0].key.name).toBe('test');
|
||||
expect(testData.successful?.[0].value.id).toBe(1);
|
||||
expect(testData.successful?.[0].value.name).toBe('test');
|
||||
});
|
||||
|
||||
it('should support ReturnValue arrays', () => {
|
||||
// Arrange
|
||||
const returnValue: ReturnValue<string> = {
|
||||
error: false,
|
||||
result: 'test result',
|
||||
message: 'Success',
|
||||
};
|
||||
|
||||
const testData: BatchResponseArgs<string> = {
|
||||
completed: true,
|
||||
error: false,
|
||||
total: 1,
|
||||
alreadyProcessed: [returnValue],
|
||||
failed: [returnValue],
|
||||
unknown: [returnValue],
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.alreadyProcessed?.[0]).toEqual(returnValue);
|
||||
expect(testData.failed?.[0]).toEqual(returnValue);
|
||||
expect(testData.unknown?.[0]).toEqual(returnValue);
|
||||
});
|
||||
|
||||
it('should support empty arrays', () => {
|
||||
// Arrange
|
||||
const testData: BatchResponseArgs<string> = {
|
||||
completed: true,
|
||||
error: false,
|
||||
total: 0,
|
||||
alreadyProcessed: [],
|
||||
ambiguous: [],
|
||||
duplicates: [],
|
||||
failed: [],
|
||||
successful: [],
|
||||
unknown: [],
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.alreadyProcessed).toHaveLength(0);
|
||||
expect(testData.ambiguous).toHaveLength(0);
|
||||
expect(testData.duplicates).toHaveLength(0);
|
||||
expect(testData.failed).toHaveLength(0);
|
||||
expect(testData.successful).toHaveLength(0);
|
||||
expect(testData.unknown).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ReturnValue } from './return-value';
|
||||
|
||||
export interface BatchResponseArgs<T> {
|
||||
alreadyProcessed?: Array<ReturnValue<T>>;
|
||||
ambiguous?: Array<T>;
|
||||
completed: boolean;
|
||||
duplicates?: Array<{ key: T; value: number }>;
|
||||
error: boolean;
|
||||
failed?: Array<ReturnValue<T>>;
|
||||
invalidProperties?: { [key: string]: string };
|
||||
message?: string;
|
||||
requestId?: number;
|
||||
successful?: Array<{ key: T; value: T }>;
|
||||
total: number;
|
||||
unknown?: Array<ReturnValue<T>>;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from './async-result';
|
||||
export * from './callback-result';
|
||||
export * from './entity-cotnainer';
|
||||
export * from './list-response-args';
|
||||
export * from './response-args';
|
||||
export * from './async-result';
|
||||
export * from './batch-response-args';
|
||||
export * from './callback-result';
|
||||
export * from './entity-cotnainer';
|
||||
export * from './list-response-args';
|
||||
export * from './response-args';
|
||||
export * from './return-value';
|
||||
|
||||
128
libs/common/data-access/src/lib/models/return-value.spec.ts
Normal file
128
libs/common/data-access/src/lib/models/return-value.spec.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { ReturnValue } from './return-value';
|
||||
|
||||
describe('ReturnValue', () => {
|
||||
describe('interface structure', () => {
|
||||
it('should support all properties', () => {
|
||||
// Arrange
|
||||
const testData: ReturnValue<string> = {
|
||||
error: false,
|
||||
invalidProperties: { field1: 'Invalid value', field2: 'Another error' },
|
||||
message: 'Operation successful',
|
||||
result: 'test result',
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.error).toBe(false);
|
||||
expect(testData.invalidProperties).toEqual({
|
||||
field1: 'Invalid value',
|
||||
field2: 'Another error'
|
||||
});
|
||||
expect(testData.message).toBe('Operation successful');
|
||||
expect(testData.result).toBe('test result');
|
||||
});
|
||||
|
||||
it('should support required properties only', () => {
|
||||
// Arrange
|
||||
const testData: ReturnValue<number> = {
|
||||
error: true,
|
||||
result: 42,
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.error).toBe(true);
|
||||
expect(testData.result).toBe(42);
|
||||
expect(testData.invalidProperties).toBeUndefined();
|
||||
expect(testData.message).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should support generic type parameter', () => {
|
||||
// Arrange
|
||||
interface TestObject {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const testObject: TestObject = { id: 1, name: 'test' };
|
||||
const testData: ReturnValue<TestObject> = {
|
||||
error: false,
|
||||
result: testObject,
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.result.id).toBe(1);
|
||||
expect(testData.result.name).toBe('test');
|
||||
});
|
||||
|
||||
it('should support arrays as generic type', () => {
|
||||
// Arrange
|
||||
const testData: ReturnValue<string[]> = {
|
||||
error: false,
|
||||
result: ['item1', 'item2', 'item3'],
|
||||
message: 'Array operation successful',
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.result).toHaveLength(3);
|
||||
expect(testData.result[0]).toBe('item1');
|
||||
expect(testData.result[1]).toBe('item2');
|
||||
expect(testData.result[2]).toBe('item3');
|
||||
});
|
||||
|
||||
it('should support null result', () => {
|
||||
// Arrange
|
||||
const testData: ReturnValue<string | null> = {
|
||||
error: false,
|
||||
result: null,
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.result).toBeNull();
|
||||
});
|
||||
|
||||
it('should support error state with message', () => {
|
||||
// Arrange
|
||||
const testData: ReturnValue<string> = {
|
||||
error: true,
|
||||
message: 'Operation failed',
|
||||
result: '',
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.error).toBe(true);
|
||||
expect(testData.message).toBe('Operation failed');
|
||||
expect(testData.result).toBe('');
|
||||
});
|
||||
|
||||
it('should support complex invalidProperties', () => {
|
||||
// Arrange
|
||||
const testData: ReturnValue<any> = {
|
||||
error: true,
|
||||
invalidProperties: {
|
||||
'user.email': 'Invalid email format',
|
||||
'user.age': 'Age must be a positive number',
|
||||
'nested.field.value': 'Required field missing',
|
||||
},
|
||||
result: null,
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.invalidProperties).toEqual({
|
||||
'user.email': 'Invalid email format',
|
||||
'user.age': 'Age must be a positive number',
|
||||
'nested.field.value': 'Required field missing',
|
||||
});
|
||||
});
|
||||
|
||||
it('should support empty invalidProperties', () => {
|
||||
// Arrange
|
||||
const testData: ReturnValue<string> = {
|
||||
error: false,
|
||||
invalidProperties: {},
|
||||
result: 'success',
|
||||
};
|
||||
|
||||
// Assert
|
||||
expect(testData.invalidProperties).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
6
libs/common/data-access/src/lib/models/return-value.ts
Normal file
6
libs/common/data-access/src/lib/models/return-value.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface ReturnValue<T> {
|
||||
error: boolean;
|
||||
invalidProperties?: { [key: string]: string };
|
||||
message?: string;
|
||||
result: T;
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
2
libs/common/decorators/src/index.ts
Normal file
2
libs/common/decorators/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lib/in-flight.decorator';
|
||||
export * from './lib/cache.decorator';
|
||||
383
libs/common/decorators/src/lib/cache.decorator.spec.ts
Normal file
383
libs/common/decorators/src/lib/cache.decorator.spec.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { Cache } from './cache.decorator';
|
||||
|
||||
describe('Cache Decorator', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllTimers();
|
||||
});
|
||||
|
||||
describe('Cache', () => {
|
||||
class DataService {
|
||||
callCount = 0;
|
||||
|
||||
@Cache({
|
||||
ttl: 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}`];
|
||||
}
|
||||
|
||||
@Cache({
|
||||
ttl: 500
|
||||
})
|
||||
async fetchWithExpiry(id: number): Promise<string> {
|
||||
this.callCount++;
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
return `data-${id}-${this.callCount}`;
|
||||
}
|
||||
|
||||
@Cache()
|
||||
async fetchWithNoExpiry(value: string): Promise<string> {
|
||||
this.callCount++;
|
||||
return `permanent-${value}-${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 cache without expiry when no ttl specified', async () => {
|
||||
const service = new DataService();
|
||||
|
||||
// First call
|
||||
const result1 = await service.fetchWithNoExpiry('test');
|
||||
expect(result1).toBe('permanent-test-1');
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Advance time significantly
|
||||
vi.advanceTimersByTime(10000);
|
||||
|
||||
// Second call should still use cache
|
||||
const result2 = await service.fetchWithNoExpiry('test');
|
||||
expect(result2).toBe('permanent-test-1');
|
||||
expect(service.callCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should cache different keys separately', async () => {
|
||||
const service = new DataService();
|
||||
|
||||
// First call
|
||||
const promise1 = service.search('query1');
|
||||
await vi.runAllTimersAsync();
|
||||
const result1 = await promise1;
|
||||
expect(result1).toEqual(['result-query1-1']);
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Second call with different key
|
||||
const promise2 = service.search('query2');
|
||||
await vi.runAllTimersAsync();
|
||||
const result2 = await promise2;
|
||||
expect(result2).toEqual(['result-query2-2']);
|
||||
expect(service.callCount).toBe(2);
|
||||
|
||||
// Subsequent calls should use cache
|
||||
const result3 = await service.search('query1');
|
||||
const result4 = await service.search('query2');
|
||||
expect(result3).toEqual(['result-query1-1']);
|
||||
expect(result4).toEqual(['result-query2-2']);
|
||||
expect(service.callCount).toBe(2); // No new calls
|
||||
});
|
||||
|
||||
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 not cache errors', async () => {
|
||||
class ErrorService {
|
||||
callCount = 0;
|
||||
|
||||
@Cache({ ttl: 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
|
||||
try {
|
||||
const promise1 = service.fetchWithError();
|
||||
await vi.runAllTimersAsync();
|
||||
await promise1;
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toBe('API Error');
|
||||
}
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Second call should not use cache (errors aren't cached)
|
||||
try {
|
||||
const promise2 = service.fetchWithError();
|
||||
await vi.runAllTimersAsync();
|
||||
await promise2;
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toBe('API Error');
|
||||
}
|
||||
expect(service.callCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should use JSON.stringify as default key generator', async () => {
|
||||
class DefaultKeyService {
|
||||
callCount = 0;
|
||||
|
||||
@Cache({ ttl: 1000 })
|
||||
async fetch(_param1: string, _param2: number): Promise<string> {
|
||||
this.callCount++;
|
||||
return `result-${this.callCount}`;
|
||||
}
|
||||
}
|
||||
|
||||
const service = new DefaultKeyService();
|
||||
|
||||
// First call with specific args
|
||||
const result1 = await service.fetch('test', 123);
|
||||
expect(result1).toBe('result-1');
|
||||
|
||||
// Same args should use cache
|
||||
const result2 = await service.fetch('test', 123);
|
||||
expect(result2).toBe('result-1');
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Different args should make new call
|
||||
const result3 = await service.fetch('test', 456);
|
||||
expect(result3).toBe('result-2');
|
||||
expect(service.callCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sync function caching', () => {
|
||||
class MathService {
|
||||
callCount = 0;
|
||||
|
||||
@Cache({
|
||||
ttl: 1000,
|
||||
keyGenerator: (n: number) => n.toString()
|
||||
})
|
||||
fibonacci(n: number): number {
|
||||
this.callCount++;
|
||||
if (n <= 1) return n;
|
||||
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
|
||||
}
|
||||
|
||||
@Cache({
|
||||
ttl: 500
|
||||
})
|
||||
expensiveCalculation(a: number, b: number): number {
|
||||
this.callCount++;
|
||||
// Simulate expensive calculation
|
||||
return a * b + Math.sqrt(a + b);
|
||||
}
|
||||
|
||||
@Cache()
|
||||
permanentCalculation(value: number): number {
|
||||
this.callCount++;
|
||||
return value * 2;
|
||||
}
|
||||
}
|
||||
|
||||
it('should cache sync function results', () => {
|
||||
const service = new MathService();
|
||||
|
||||
// First call
|
||||
const result1 = service.fibonacci(5);
|
||||
expect(result1).toBe(5);
|
||||
expect(service.callCount).toBe(6); // fibonacci(5) calls fibonacci(4), fibonacci(3), etc.
|
||||
|
||||
// Second call should use cache
|
||||
const result2 = service.fibonacci(5);
|
||||
expect(result2).toBe(5);
|
||||
expect(service.callCount).toBe(6); // No new calls
|
||||
});
|
||||
|
||||
it('should cache sync functions with multiple arguments', () => {
|
||||
const service = new MathService();
|
||||
|
||||
// First call
|
||||
const result1 = service.expensiveCalculation(10, 20);
|
||||
expect(result1).toBe(10 * 20 + Math.sqrt(10 + 20));
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Second call with same arguments should use cache
|
||||
const result2 = service.expensiveCalculation(10, 20);
|
||||
expect(result2).toBe(result1);
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Call with different arguments should execute
|
||||
const result3 = service.expensiveCalculation(5, 10);
|
||||
expect(result3).toBe(5 * 10 + Math.sqrt(5 + 10));
|
||||
expect(service.callCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle sync function cache expiry', () => {
|
||||
const service = new MathService();
|
||||
|
||||
// First call
|
||||
const result1 = service.expensiveCalculation(3, 4);
|
||||
expect(result1).toBe(3 * 4 + Math.sqrt(3 + 4));
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Second call within TTL should use cache
|
||||
const result2 = service.expensiveCalculation(3, 4);
|
||||
expect(result2).toBe(result1);
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Advance time past TTL
|
||||
vi.advanceTimersByTime(600);
|
||||
|
||||
// Call after expiry should execute again
|
||||
const result3 = service.expensiveCalculation(3, 4);
|
||||
expect(result3).toBe(3 * 4 + Math.sqrt(3 + 4));
|
||||
expect(service.callCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should cache sync functions without expiry', () => {
|
||||
const service = new MathService();
|
||||
|
||||
// First call
|
||||
const result1 = service.permanentCalculation(42);
|
||||
expect(result1).toBe(84);
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Advance time significantly
|
||||
vi.advanceTimersByTime(10000);
|
||||
|
||||
// Second call should still use cache
|
||||
const result2 = service.permanentCalculation(42);
|
||||
expect(result2).toBe(84);
|
||||
expect(service.callCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should not cache sync function errors', () => {
|
||||
class ErrorService {
|
||||
callCount = 0;
|
||||
|
||||
@Cache({ ttl: 1000 })
|
||||
errorFunction(shouldError: boolean): string {
|
||||
this.callCount++;
|
||||
if (shouldError) {
|
||||
throw new Error('Sync Error');
|
||||
}
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
|
||||
const service = new ErrorService();
|
||||
|
||||
// First call that errors
|
||||
expect(() => service.errorFunction(true)).toThrow('Sync Error');
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Second call should not use cache (errors aren't cached)
|
||||
expect(() => service.errorFunction(true)).toThrow('Sync Error');
|
||||
expect(service.callCount).toBe(2);
|
||||
|
||||
// Call with success should work
|
||||
const result = service.errorFunction(false);
|
||||
expect(result).toBe('success');
|
||||
expect(service.callCount).toBe(3);
|
||||
|
||||
// Subsequent success call should use cache
|
||||
const result2 = service.errorFunction(false);
|
||||
expect(result2).toBe('success');
|
||||
expect(service.callCount).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mixed sync/async usage', () => {
|
||||
class MixedService {
|
||||
callCount = 0;
|
||||
|
||||
@Cache({ ttl: 1000 })
|
||||
async asyncMethod(value: string): Promise<string> {
|
||||
this.callCount++;
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
return `async-${value}`;
|
||||
}
|
||||
|
||||
@Cache({ ttl: 1000 })
|
||||
syncMethod(value: string): string {
|
||||
this.callCount++;
|
||||
return `sync-${value}`;
|
||||
}
|
||||
}
|
||||
|
||||
it('should handle mixed sync and async methods in same class', async () => {
|
||||
const service = new MixedService();
|
||||
|
||||
// Test async method
|
||||
const promise1 = service.asyncMethod('test');
|
||||
await vi.runAllTimersAsync();
|
||||
const result1 = await promise1;
|
||||
expect(result1).toBe('async-test');
|
||||
expect(service.callCount).toBe(1);
|
||||
|
||||
// Test sync method
|
||||
const result2 = service.syncMethod('test');
|
||||
expect(result2).toBe('sync-test');
|
||||
expect(service.callCount).toBe(2);
|
||||
|
||||
// Test caching for both
|
||||
const result3 = await service.asyncMethod('test');
|
||||
expect(result3).toBe('async-test');
|
||||
expect(service.callCount).toBe(2); // No new call
|
||||
|
||||
const result4 = service.syncMethod('test');
|
||||
expect(result4).toBe('sync-test');
|
||||
expect(service.callCount).toBe(2); // No new call
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
149
libs/common/decorators/src/lib/cache.decorator.ts
Normal file
149
libs/common/decorators/src/lib/cache.decorator.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
export const CacheTimeToLive = {
|
||||
oneMinute: 60 * 1000, // 1 minute
|
||||
fiveMinutes: 5 * 60 * 1000, // 5 minutes
|
||||
tenMinutes: 10 * 60 * 1000, // 10 minutes
|
||||
thirtyMinutes: 30 * 60 * 1000, // 30 minutes
|
||||
oneHour: 60 * 60 * 1000, // 1 hour
|
||||
} as const;
|
||||
|
||||
export type CacheTimeToLive =
|
||||
(typeof CacheTimeToLive)[keyof typeof CacheTimeToLive];
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/**
|
||||
* Options for configuring the Cache decorator
|
||||
*/
|
||||
export interface CacheOptions<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.
|
||||
* If not provided, cache never expires.
|
||||
*/
|
||||
ttl?: number | CacheTimeToLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator that caches the results of both sync and async method calls.
|
||||
* Results are cached based on method arguments and expire after the specified TTL.
|
||||
*
|
||||
* @param options Configuration options for the decorator
|
||||
* @example
|
||||
* ```typescript
|
||||
* class DataService {
|
||||
* // Async function caching
|
||||
* @Cache({
|
||||
* ttl: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
* keyGenerator: (params: QueryParams) => params.query
|
||||
* })
|
||||
* async searchData(params: QueryParams): Promise<SearchResult> {
|
||||
* return await api.search(params);
|
||||
* }
|
||||
*
|
||||
* // Sync function caching (heavy calculations)
|
||||
* @Cache({
|
||||
* ttl: 10 * 60 * 1000, // Cache for 10 minutes
|
||||
* keyGenerator: (n: number) => n.toString()
|
||||
* })
|
||||
* fibonacci(n: number): number {
|
||||
* if (n <= 1) return n;
|
||||
* return this.fibonacci(n - 1) + this.fibonacci(n - 2);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function Cache<T extends (...args: any[]) => any>(
|
||||
options: CacheOptions<T> = {},
|
||||
): MethodDecorator {
|
||||
const cacheMap = new WeakMap<
|
||||
object,
|
||||
Map<string, { result: any; expiry?: number; isAsync: boolean }>
|
||||
>();
|
||||
|
||||
return function (
|
||||
_target: any,
|
||||
_propertyKey: string | symbol,
|
||||
descriptor: PropertyDescriptor,
|
||||
): PropertyDescriptor {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = function (
|
||||
this: any,
|
||||
...args: Parameters<T>
|
||||
): ReturnType<T> {
|
||||
// Initialize cache for this instance if needed
|
||||
if (!cacheMap.has(this)) {
|
||||
cacheMap.set(this, new Map());
|
||||
}
|
||||
const instanceCache = cacheMap.get(this);
|
||||
if (!instanceCache) {
|
||||
throw new Error('Cache map not initialized properly');
|
||||
}
|
||||
|
||||
// Generate cache key
|
||||
const key = options.keyGenerator
|
||||
? options.keyGenerator(...args)
|
||||
: JSON.stringify(args);
|
||||
|
||||
// Check cache first
|
||||
const cached = instanceCache.get(key);
|
||||
if (cached) {
|
||||
// If no TTL or not expired, return cached result
|
||||
if (!cached.expiry || cached.expiry > Date.now()) {
|
||||
// For async functions, wrap cached result in a Promise
|
||||
if (cached.isAsync) {
|
||||
return Promise.resolve(cached.result) as ReturnType<T>;
|
||||
}
|
||||
return cached.result;
|
||||
}
|
||||
// Clean up expired cache entry
|
||||
instanceCache.delete(key);
|
||||
}
|
||||
|
||||
// Execute original method
|
||||
const result = originalMethod.apply(this, args);
|
||||
|
||||
// Handle both sync and async functions
|
||||
// Use more robust Promise detection for testing environments
|
||||
const isPromise =
|
||||
result instanceof Promise ||
|
||||
(result &&
|
||||
typeof result.then === 'function' &&
|
||||
typeof result.catch === 'function');
|
||||
|
||||
if (isPromise) {
|
||||
// Async function: only cache successful results
|
||||
const promise = result
|
||||
.then((value: any) => {
|
||||
instanceCache.set(key, {
|
||||
result: value,
|
||||
expiry: options.ttl ? Date.now() + options.ttl : undefined,
|
||||
isAsync: true,
|
||||
});
|
||||
return value;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// Don't cache errors - ensure cache is clean
|
||||
instanceCache.delete(key);
|
||||
throw error;
|
||||
});
|
||||
|
||||
return promise as ReturnType<T>;
|
||||
} else {
|
||||
// Sync function: cache result directly
|
||||
instanceCache.set(key, {
|
||||
result,
|
||||
expiry: options.ttl ? Date.now() + options.ttl : undefined,
|
||||
isAsync: false,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
202
libs/common/decorators/src/lib/in-flight.decorator.spec.ts
Normal file
202
libs/common/decorators/src/lib/in-flight.decorator.spec.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { InFlight } from './in-flight.decorator';
|
||||
|
||||
describe('InFlight Decorator', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Basic usage (no options)', () => {
|
||||
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 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('With key generator', () => {
|
||||
class UserService {
|
||||
callCounts = new Map<string, number>();
|
||||
|
||||
@InFlight({
|
||||
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}` };
|
||||
}
|
||||
|
||||
@InFlight({
|
||||
keyGenerator: undefined, // Uses JSON.stringify
|
||||
})
|
||||
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 when keyGenerator is undefined', 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
115
libs/common/decorators/src/lib/in-flight.decorator.ts
Normal file
115
libs/common/decorators/src/lib/in-flight.decorator.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/**
|
||||
* Options for configuring the InFlight decorator
|
||||
*/
|
||||
export interface InFlightOptions<T extends (...args: any[]) => any> {
|
||||
/**
|
||||
* Generate a cache key from the method arguments.
|
||||
* If not provided, uses JSON.stringify on all arguments.
|
||||
* If omitted entirely, the decorator will not differentiate between calls with different arguments.
|
||||
*/
|
||||
keyGenerator?: (...args: Parameters<T>) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator that prevents multiple simultaneous calls to the same async method.
|
||||
* All concurrent calls will receive the same Promise result.
|
||||
*
|
||||
* @param options Configuration options for the decorator
|
||||
* @example
|
||||
* ```typescript
|
||||
* class MyService {
|
||||
* // Basic usage - all calls share the same in-flight request
|
||||
* @InFlight()
|
||||
* async fetchData(): Promise<Data> {
|
||||
* return await api.getData();
|
||||
* }
|
||||
*
|
||||
* // With key generator - calls with different arguments can execute simultaneously
|
||||
* @InFlight({
|
||||
* keyGenerator: (userId: string) => userId
|
||||
* })
|
||||
* async fetchUser(userId: string): Promise<User> {
|
||||
* return await api.getUser(userId);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function InFlight<T extends (...args: any[]) => Promise<any>>(
|
||||
options: InFlightOptions<T> = {},
|
||||
): MethodDecorator {
|
||||
// If keyGenerator is explicitly provided (even if undefined), use keyed mode
|
||||
const useKeys = 'keyGenerator' in options;
|
||||
|
||||
const simpleInFlightMap = new WeakMap<object, Promise<any>>();
|
||||
const keyedInFlightMap = 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>> {
|
||||
if (!useKeys) {
|
||||
// Simple mode: one in-flight request per instance
|
||||
const existingRequest = simpleInFlightMap.get(this);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const promise = originalMethod
|
||||
.apply(this, args)
|
||||
.then((result: any) => {
|
||||
simpleInFlightMap.delete(this);
|
||||
return result;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
simpleInFlightMap.delete(this);
|
||||
throw error;
|
||||
});
|
||||
|
||||
simpleInFlightMap.set(this, promise);
|
||||
return promise;
|
||||
} else {
|
||||
// Keyed mode: separate in-flight requests per key
|
||||
if (!keyedInFlightMap.has(this)) {
|
||||
keyedInFlightMap.set(this, new Map());
|
||||
}
|
||||
const instanceMap = keyedInFlightMap.get(this);
|
||||
if (!instanceMap) {
|
||||
throw new Error('In-flight map not initialized properly');
|
||||
}
|
||||
|
||||
const key = options.keyGenerator
|
||||
? options.keyGenerator(...args)
|
||||
: JSON.stringify(args);
|
||||
|
||||
const existingRequest = instanceMap.get(key);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const promise = originalMethod
|
||||
.apply(this, args)
|
||||
.then((result: any) => {
|
||||
instanceMap.delete(key);
|
||||
return result;
|
||||
})
|
||||
.catch((error: any) => {
|
||||
instanceMap.delete(key);
|
||||
throw error;
|
||||
});
|
||||
|
||||
instanceMap.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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,17 @@
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,166 +1,166 @@
|
||||
import { LogLevel } from './log-level.enum';
|
||||
import { Type } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Represents a destination where log messages are sent.
|
||||
* Implement this interface to create custom logging destinations like
|
||||
* console logging, remote logging services, or file logging.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* export class CustomLogSink implements Sink {
|
||||
* log(
|
||||
* level: LogLevel,
|
||||
* message: string,
|
||||
* context?: LoggerContext,
|
||||
* error?: Error
|
||||
* ): void {
|
||||
* // Custom logging implementation
|
||||
* if (level === LogLevel.Error) {
|
||||
* // Send to monitoring service
|
||||
* this.monitoringService.reportError(message, error, context);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface Sink {
|
||||
/**
|
||||
* Method called by the LoggingService to send a log entry to this sink.
|
||||
*
|
||||
* @param level - The severity level of the log message
|
||||
* @param message - The main log message content
|
||||
* @param context - Optional structured data or metadata about the log event
|
||||
* @param error - Optional error object when logging errors
|
||||
*/
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: LoggerContext,
|
||||
error?: Error,
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function that creates a logging sink function.
|
||||
* Useful when the sink needs access to injected dependencies or
|
||||
* requires initialization logic.
|
||||
*
|
||||
* @returns A function matching the Sink.log method signature
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const httpLogSink: SinkFn = () => {
|
||||
* const http = inject(HttpClient);
|
||||
* const config = inject(ConfigService);
|
||||
*
|
||||
* return (level, message, context?, error?) => {
|
||||
* http.post(config.loggingEndpoint, {
|
||||
* level,
|
||||
* message,
|
||||
* context,
|
||||
* error: error && {
|
||||
* name: error.name,
|
||||
* message: error.message,
|
||||
* stack: error.stack
|
||||
* }
|
||||
* }).subscribe();
|
||||
* };
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export type SinkFn = () => (
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: LoggerContext,
|
||||
error?: Error,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Configuration options for the logging service.
|
||||
* Used to set up the logging behavior during application initialization.
|
||||
*/
|
||||
export interface LoggingConfig {
|
||||
/** The minimum log level to process. Messages below this level are ignored. */
|
||||
level: LogLevel;
|
||||
|
||||
/**
|
||||
* An array of logging destinations where messages will be sent.
|
||||
* Can be sink instances, classes, or factory functions.
|
||||
*/
|
||||
sinks: (Sink | SinkFn | Type<Sink>)[];
|
||||
|
||||
/**
|
||||
* Optional global context included with every log message.
|
||||
* Useful for adding application-wide metadata like version or environment.
|
||||
*/
|
||||
context?: LoggerContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the public API for logging operations.
|
||||
* This interface is returned by the logger factory and provides
|
||||
* methods for logging at different severity levels.
|
||||
*/
|
||||
export interface LoggerApi {
|
||||
/**
|
||||
* Logs a trace message with optional context.
|
||||
* Use for fine-grained debugging information.
|
||||
*/
|
||||
trace(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs a debug message with optional context.
|
||||
* Use for development-time debugging information.
|
||||
*/
|
||||
debug(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs an info message with optional context.
|
||||
* Use for general runtime information.
|
||||
*/
|
||||
info(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs a warning message with optional context.
|
||||
* Use for potentially harmful situations.
|
||||
*/
|
||||
warn(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs an error message with an optional error object and context.
|
||||
* Use for error conditions that affect functionality.
|
||||
*
|
||||
* @param message - The error message to log
|
||||
* @param error - Optional error object that caused this error condition
|
||||
* @param context - Optional context data associated with the error
|
||||
*/
|
||||
error(message: string, error?: Error, context?: () => LoggerContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents context data associated with a log message.
|
||||
* Context allows adding structured metadata to log messages,
|
||||
* making them more informative and easier to filter/analyze.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Component context
|
||||
* const context: LoggerContext = {
|
||||
* component: 'UserProfile',
|
||||
* userId: '12345',
|
||||
* action: 'save'
|
||||
* };
|
||||
*
|
||||
* // Error context
|
||||
* const errorContext: LoggerContext = {
|
||||
* operationId: 'op-123',
|
||||
* attemptNumber: 3,
|
||||
* inputData: { ... }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export interface LoggerContext {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
import { LogLevel } from './log-level.enum';
|
||||
import { Type } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Represents a destination where log messages are sent.
|
||||
* Implement this interface to create custom logging destinations like
|
||||
* console logging, remote logging services, or file logging.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Injectable()
|
||||
* export class CustomLogSink implements Sink {
|
||||
* log(
|
||||
* level: LogLevel,
|
||||
* message: string,
|
||||
* context?: LoggerContext,
|
||||
* error?: Error
|
||||
* ): void {
|
||||
* // Custom logging implementation
|
||||
* if (level === LogLevel.Error) {
|
||||
* // Send to monitoring service
|
||||
* this.monitoringService.reportError(message, error, context);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface Sink {
|
||||
/**
|
||||
* Method called by the LoggingService to send a log entry to this sink.
|
||||
*
|
||||
* @param level - The severity level of the log message
|
||||
* @param message - The main log message content
|
||||
* @param context - Optional structured data or metadata about the log event
|
||||
* @param error - Optional error object when logging errors
|
||||
*/
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: LoggerContext,
|
||||
error?: Error,
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function that creates a logging sink function.
|
||||
* Useful when the sink needs access to injected dependencies or
|
||||
* requires initialization logic.
|
||||
*
|
||||
* @returns A function matching the Sink.log method signature
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const httpLogSink: SinkFn = () => {
|
||||
* const http = inject(HttpClient);
|
||||
* const config = inject(ConfigService);
|
||||
*
|
||||
* return (level, message, context?, error?) => {
|
||||
* http.post(config.loggingEndpoint, {
|
||||
* level,
|
||||
* message,
|
||||
* context,
|
||||
* error: error && {
|
||||
* name: error.name,
|
||||
* message: error.message,
|
||||
* stack: error.stack
|
||||
* }
|
||||
* }).subscribe();
|
||||
* };
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export type SinkFn = () => (
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: LoggerContext,
|
||||
error?: Error,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Configuration options for the logging service.
|
||||
* Used to set up the logging behavior during application initialization.
|
||||
*/
|
||||
export interface LoggingConfig {
|
||||
/** The minimum log level to process. Messages below this level are ignored. */
|
||||
level: LogLevel;
|
||||
|
||||
/**
|
||||
* An array of logging destinations where messages will be sent.
|
||||
* Can be sink instances, classes, or factory functions.
|
||||
*/
|
||||
sinks: (Sink | SinkFn | Type<Sink>)[];
|
||||
|
||||
/**
|
||||
* Optional global context included with every log message.
|
||||
* Useful for adding application-wide metadata like version or environment.
|
||||
*/
|
||||
context?: LoggerContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the public API for logging operations.
|
||||
* This interface is returned by the logger factory and provides
|
||||
* methods for logging at different severity levels.
|
||||
*/
|
||||
export interface LoggerApi {
|
||||
/**
|
||||
* Logs a trace message with optional context.
|
||||
* Use for fine-grained debugging information.
|
||||
*/
|
||||
trace(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs a debug message with optional context.
|
||||
* Use for development-time debugging information.
|
||||
*/
|
||||
debug(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs an info message with optional context.
|
||||
* Use for general runtime information.
|
||||
*/
|
||||
info(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs a warning message with optional context.
|
||||
* Use for potentially harmful situations.
|
||||
*/
|
||||
warn(message: string, context?: () => LoggerContext): void;
|
||||
|
||||
/**
|
||||
* Logs an error message with an optional error object and context.
|
||||
* Use for error conditions that affect functionality.
|
||||
*
|
||||
* @param message - The error message to log
|
||||
* @param error - Optional error object that caused this error condition
|
||||
* @param context - Optional context data associated with the error
|
||||
*/
|
||||
error(message: string, error?: unknown, context?: () => LoggerContext): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents context data associated with a log message.
|
||||
* Context allows adding structured metadata to log messages,
|
||||
* making them more informative and easier to filter/analyze.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Component context
|
||||
* const context: LoggerContext = {
|
||||
* component: 'UserProfile',
|
||||
* userId: '12345',
|
||||
* action: 'save'
|
||||
* };
|
||||
*
|
||||
* // Error context
|
||||
* const errorContext: LoggerContext = {
|
||||
* operationId: 'op-123',
|
||||
* attemptNumber: 3,
|
||||
* inputData: { ... }
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export interface LoggerContext {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
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,11 +1,20 @@
|
||||
import { Type } from '@angular/core';
|
||||
import { getState, patchState, signalStoreFeature, withHooks, withMethods } from '@ngrx/signals';
|
||||
import {
|
||||
getState,
|
||||
patchState,
|
||||
signalStoreFeature,
|
||||
withHooks,
|
||||
withMethods,
|
||||
} from '@ngrx/signals';
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { StorageProvider } from './storage-provider';
|
||||
import { injectStorage } from './storage';
|
||||
import { debounceTime, pipe, switchMap } from 'rxjs';
|
||||
|
||||
export function withStorage(storageKey: string, storageProvider: Type<StorageProvider>) {
|
||||
export function withStorage(
|
||||
storageKey: string,
|
||||
storageProvider: Type<StorageProvider>,
|
||||
) {
|
||||
return signalStoreFeature(
|
||||
withMethods((store, storage = injectStorage(storageProvider)) => ({
|
||||
storeState: rxMethod<void>(
|
||||
@@ -16,7 +25,7 @@ export function withStorage(storageKey: string, storageProvider: Type<StoragePro
|
||||
),
|
||||
restoreState: async () => {
|
||||
const data = await storage.get(storageKey);
|
||||
if (data) {
|
||||
if (data && typeof data === 'object') {
|
||||
patchState(store, data);
|
||||
}
|
||||
},
|
||||
|
||||
40
libs/core/tabs/eslint.config.js
Normal file
40
libs/core/tabs/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.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,6 +1,18 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { TabService } from './tab.service';
|
||||
|
||||
export function injectActivatedTabId() {
|
||||
/**
|
||||
* Injects the current activated tab as a signal.
|
||||
* @returns A signal that emits the current activated tab or null if no tab is activated.
|
||||
*/
|
||||
export function injectTab() {
|
||||
return inject(TabService).activatedTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the current tab ID as a signal.
|
||||
* @returns A signal that emits the current tab ID or null if no tab is activated.
|
||||
*/
|
||||
export function injectTabId() {
|
||||
return inject(TabService).activatedTabId;
|
||||
}
|
||||
|
||||
@@ -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}"],
|
||||
|
||||
@@ -114,7 +114,7 @@ export const isaFiliale =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <path fill-rule="evenodd" clip-rule="evenodd" d="M11.4772 1.33637C11.8195 1.24369 12.1803 1.24369 12.5226 1.33637C12.92 1.44396 13.2545 1.70662 13.5215 1.91625C13.547 1.93627 13.5719 1.9558 13.5961 1.97466L20.3784 7.24979C20.4046 7.27009 20.4305 7.2902 20.4562 7.31015C20.8328 7.60242 21.1646 7.85993 21.4119 8.19422C21.6288 8.48761 21.7905 8.81812 21.8889 9.16952C22.0009 9.56993 22.0005 9.98994 22 10.4667C21.9999 10.4992 21.9999 10.532 21.9999 10.5651V17.8386C21.9999 18.3657 21.9999 18.8205 21.9693 19.195C21.937 19.5904 21.8657 19.9836 21.6729 20.362C21.3853 20.9265 20.9264 21.3854 20.3619 21.673C19.9835 21.8658 19.5903 21.9371 19.1949 21.9694C18.8204 22 18.3656 22 17.8385 22H6.16133C5.63419 22 5.17943 22 4.80487 21.9694C4.40952 21.9371 4.0163 21.8658 3.63792 21.673C3.07344 21.3854 2.6145 20.9265 2.32688 20.362C2.13408 19.9836 2.06277 19.5904 2.03046 19.195C1.99986 18.8205 1.99988 18.3657 1.99989 17.8385L1.9999 10.5651C1.9999 10.532 1.99986 10.4992 1.99982 10.4667C1.99931 9.98994 1.99886 9.56993 2.11094 9.16952C2.2093 8.81811 2.37094 8.48761 2.58794 8.19422C2.83519 7.85992 3.16701 7.60242 3.54364 7.31013C3.56934 7.29019 3.59524 7.27009 3.62134 7.24979L10.4037 1.97466C10.4279 1.9558 10.4528 1.93626 10.4783 1.91625C10.7453 1.70662 11.0798 1.44396 11.4772 1.33637ZM9.9999 20H13.9999V13.6C13.9999 13.3035 13.9991 13.1412 13.9896 13.0246C13.9892 13.02 13.9888 13.0156 13.9884 13.0114C13.9843 13.0111 13.9799 13.0107 13.9753 13.0103C13.8587 13.0008 13.6964 13 13.3999 13H10.5999C10.3034 13 10.1411 13.0008 10.0245 13.0103C10.0199 13.0107 10.0155 13.0111 10.0113 13.0114C10.011 13.0156 10.0106 13.02 10.0102 13.0246C10.0007 13.1412 9.9999 13.3035 9.9999 13.6V20ZM15.9999 20L15.9999 13.5681C15.9999 13.3157 16 13.0699 15.983 12.8618C15.9643 12.6332 15.9202 12.3634 15.7819 12.092C15.5902 11.7157 15.2842 11.4097 14.9079 11.218C14.6365 11.0797 14.3667 11.0356 14.1381 11.0169C13.93 10.9999 13.6842 11 13.4318 11H10.568C10.3156 11 10.0698 10.9999 9.86167 11.0169C9.63307 11.0356 9.36334 11.0797 9.09191 11.218C8.71559 11.4097 8.40963 11.7157 8.21788 12.092C8.07959 12.3634 8.03552 12.6332 8.01684 12.8618C7.99983 13.0699 7.99986 13.3157 7.99989 13.5681L7.9999 20H6.1999C5.62334 20 5.25107 19.9992 4.96773 19.9761C4.69607 19.9539 4.59535 19.9162 4.5459 19.891C4.35774 19.7951 4.20476 19.6422 4.10889 19.454C4.0837 19.4045 4.04602 19.3038 4.02382 19.0322C4.00067 18.7488 3.9999 18.3766 3.9999 17.8V10.5651C3.9999 9.93408 4.00858 9.80982 4.03691 9.70862C4.0697 9.59148 4.12358 9.48131 4.19591 9.38352C4.2584 9.29903 4.35115 9.21588 4.84923 8.82849L11.6315 3.55337C11.8184 3.40799 11.9174 3.33175 11.9926 3.28154C11.9951 3.27984 11.9976 3.27823 11.9999 3.27671C12.0022 3.27823 12.0046 3.27984 12.0072 3.28154C12.0823 3.33175 12.1814 3.40799 12.3683 3.55337L19.1506 8.82849C19.6486 9.21588 19.7414 9.29903 19.8039 9.38352C19.8762 9.48131 19.9301 9.59148 19.9629 9.70862C19.9912 9.80982 19.9999 9.93408 19.9999 10.5651V17.8C19.9999 18.3766 19.9991 18.7488 19.976 19.0322C19.9538 19.3038 19.9161 19.4045 19.8909 19.454C19.795 19.6422 19.642 19.7951 19.4539 19.891C19.4044 19.9162 19.3037 19.9539 19.0321 19.9761C18.7487 19.9992 18.3764 20 17.7999 20H15.9999Z" fill="currentColor"/></svg>';
|
||||
|
||||
export const isaFilialeLocation =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <path d="M12 13C13.6569 13 15 11.6569 15 10C15 8.34315 13.6569 7 12 7C10.3431 7 9 8.34315 9 10C9 11.6569 10.3431 13 12 13Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 22C16 18 20 14.4183 20 10C20 5.58172 16.4183 2 12 2C7.58172 2 4 5.58172 4 10C4 14.4183 8 18 12 22Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M12 13C13.6569 13 15 11.6569 15 10C15 8.34315 13.6569 7 12 7C10.3431 7 9 8.34315 9 10C9 11.6569 10.3431 13 12 13Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 22C16 18 20 14.4183 20 10C20 5.58172 16.4183 2 12 2C7.58172 2 4 5.58172 4 10C4 14.4183 8 18 12 22Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
|
||||
export const isaArtikelKartoniert = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.24787 2.5C7.25947 2.5 7.2711 2.5 7.28276 2.5L7.60047 2.5C8.34332 2.49999 8.95985 2.49998 9.46281 2.54033C9.98596 2.5823 10.4728 2.67271 10.9325 2.90269C11.3354 3.10427 11.6965 3.37467 12.0002 3.6995C12.3038 3.37467 12.665 3.10427 13.0679 2.90269C13.5275 2.67271 14.0144 2.5823 14.5375 2.54033C15.0405 2.49998 15.657 2.49999 16.3999 2.5L16.7525 2.5C17.111 2.49998 17.437 2.49996 17.7088 2.52177C18.0006 2.54518 18.3162 2.59847 18.6274 2.75419C19.0751 2.9782 19.4425 3.3374 19.6739 3.78334C19.8357 4.09512 19.891 4.41165 19.9152 4.70214C19.9376 4.97133 19.9376 5.29341 19.9375 5.64431L19.9375 15.0162C19.9375 15.3671 19.9375 15.6892 19.9151 15.9584C19.8909 16.2489 19.8356 16.5654 19.6739 16.8772C19.4425 17.3231 19.0751 17.6823 18.6273 17.9063C18.3161 18.062 18.0006 18.1153 17.7088 18.1387C17.4369 18.1605 17.111 18.1605 16.7524 18.1605L15.7637 18.1605C14.8337 18.1605 14.5735 18.1704 14.3521 18.2364C14.127 18.3035 13.9187 18.4132 13.7388 18.5585C13.5634 18.7 13.4135 18.9026 12.8969 19.6636L12.8274 19.7659C12.6413 20.04 12.3315 20.2042 12.0001 20.2042C11.6687 20.2042 11.3589 20.04 11.1728 19.7659L11.1033 19.6636C10.5867 18.9026 10.4368 18.7 10.2615 18.5585C10.0815 18.4132 9.87318 18.3035 9.64811 18.2364C9.42671 18.1704 9.1665 18.1605 8.23647 18.1605L7.24783 18.1605C6.88925 18.1605 6.56329 18.1605 6.29144 18.1387C5.99966 18.1153 5.68411 18.062 5.37287 17.9063C4.92515 17.6823 4.55774 17.3231 4.32635 16.8772C4.16457 16.5654 4.10926 16.2489 4.08509 15.9584C4.0627 15.6892 4.06272 15.3671 4.06275 15.0162L4.06281 5.67991C4.06281 5.67991 4.06281 5.67991 4.06281 5.67991C4.06281 5.66801 4.06281 5.65612 4.06281 5.64428C4.06279 5.29339 4.06276 4.97133 4.08516 4.70215C4.10933 4.41166 4.16464 4.09512 4.32642 3.78334C4.55781 3.33739 4.92522 2.9782 5.37293 2.75419C5.68418 2.59847 5.99972 2.54518 6.2915 2.52177C6.56335 2.49996 6.8893 2.49998 7.24787 2.5ZM6.26449 4.54428C6.26445 4.54427 6.26506 4.54398 6.26646 4.54347L6.26449 4.54428ZM6.26646 4.54347C6.27658 4.53983 6.32436 4.52556 6.45145 4.51536C6.63354 4.50075 6.87803 4.5 7.28276 4.5H7.56026C8.35352 4.5 8.88941 4.50075 9.30286 4.53392C9.70517 4.5662 9.90362 4.62429 10.0376 4.69131C10.3731 4.85916 10.6424 5.12525 10.8101 5.44839C10.8752 5.57377 10.9333 5.76196 10.9658 6.15304C10.9994 6.55634 11.0002 7.07998 11.0002 7.85982L11.0001 16.6511C10.7538 16.5122 10.492 16.401 10.2197 16.3198C9.68245 16.1596 9.10813 16.16 8.36282 16.1604C8.32125 16.1605 8.27913 16.1605 8.23647 16.1605H7.2827C6.87797 16.1605 6.63348 16.1598 6.45139 16.1451C6.3243 16.1349 6.2767 16.1207 6.26659 16.1171C6.19397 16.0805 6.13794 16.0243 6.10332 15.9593C6.0995 15.9476 6.08735 15.9024 6.07821 15.7925C6.06355 15.6164 6.06275 15.3789 6.06275 14.9806L6.06281 5.67992C6.06281 5.2816 6.06362 5.04409 6.07827 4.86798C6.08742 4.75806 6.09956 4.71289 6.10339 4.7012C6.13801 4.63617 6.19384 4.58011 6.26646 4.54347ZM13.0001 16.6511C13.2464 16.5122 13.5082 16.401 13.7805 16.3198C14.3178 16.1596 14.8921 16.16 15.6374 16.1604C15.679 16.1605 15.7211 16.1605 15.7637 16.1605H16.7175C17.1222 16.1605 17.3667 16.1598 17.5488 16.1451C17.6759 16.1349 17.7235 16.1207 17.7336 16.1171C17.8062 16.0805 17.8623 16.0243 17.8969 15.9593C17.9007 15.9476 17.9129 15.9024 17.922 15.7925C17.9367 15.6164 17.9375 15.3789 17.9375 14.9806L17.9375 5.67991C17.9375 5.28159 17.9367 5.04409 17.9221 4.86798C17.9129 4.75807 17.9008 4.7129 17.8969 4.7012C17.8623 4.63617 17.8063 4.58004 17.7337 4.5434C17.7236 4.53976 17.676 4.52556 17.5489 4.51536C17.3668 4.50075 17.1223 4.5 16.7176 4.5H16.4401C15.6468 4.5 15.1109 4.50075 14.6975 4.53392C14.2952 4.5662 14.0967 4.62429 13.9628 4.69131C13.6273 4.85916 13.3579 5.12525 13.1902 5.44839C13.1252 5.57377 13.0671 5.76196 13.0345 6.15304C13.001 6.55634 13.0002 7.07998 13.0002 7.85983L13.0001 16.6511ZM17.7358 16.1162C17.7358 16.1162 17.735 16.1166 17.7336 16.1171L17.7358 16.1162Z" fill="#212529"/>
|
||||
|
||||
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: {},
|
||||
},
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user