chore: update dependencies and add new path mappings

- Updated @nx/js from 20.4.6 to 20.8.1
- Updated angular-eslint from 19.1.0 to 19.2.0
- Added path mapping for @isa/common/print in tsconfig.base.json
- Added path mapping for @isa/ui/dialog in tsconfig.base.json
- Added path mapping for @isa/ui/list in tsconfig.base.json
This commit is contained in:
Lorenz Hilpert
2025-05-21 14:38:24 +02:00
parent 08f8686791
commit d84bc276d5
82 changed files with 2363 additions and 2742 deletions

32
.github/instructions/nx.instructions.md vendored Normal file
View File

@@ -0,0 +1,32 @@
---
applyTo: '**'
---
// This file is automatically generated by Nx Console
You are in an nx workspace using Nx 20.4.6 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
undefined

3
.gitignore vendored
View File

@@ -62,3 +62,6 @@ libs/swagger/src/lib/*
storybook-static
.cursor\rules\nx-rules.mdc
.github\instructions\nx.instructions.md

View File

@@ -33,12 +33,6 @@
{
"file": ".github/copilot-instructions.md"
},
{
"file": ".github/review-instructions.md"
},
{
"file": ".github/testing-instructions.md"
},
{
"file": "docs/tech-stack.md"
},
@@ -95,4 +89,5 @@
"file": "docs/guidelines/testing.md"
}
],
"nxConsole.generateAiAgentRules": true,
}

View File

@@ -1,4 +1,6 @@
@use '../../../libs/ui/buttons/src/buttons.scss';
@use '../../../libs/ui/datepicker/src/datepicker.scss';
@use '../../../libs/ui/input-controls/src/input-controls.scss';
@use '../../../libs/ui/progress-bar/src/lib/progress-bar.scss';
@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/list/src/list.scss";
@use "../../../libs/ui/progress-bar/src/lib/progress-bar.scss";

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
export * from './lib/models';
export * from './lib/services';

View File

@@ -0,0 +1,2 @@
export * from './printer.model';
export * from './printer-type.enum';

View File

@@ -0,0 +1,15 @@
/**
* Enum representing the different types of printers available in the system
*/
export const PrinterType = {
/** Standard office printers for documents */
OFFICE: 'office',
/** Specialized label printers */
LABEL: 'label',
} as const;
/**
* Type representing the possible printer types
* Used for type safety when specifying printer types
*/
export type PrinterType = (typeof PrinterType)[keyof typeof PrinterType];

View File

@@ -0,0 +1,18 @@
import { KeyValueDTOOfStringAndString } from '@generated/swagger/print-api';
/**
* Represents a printer in the system
* Extends the basic KeyValueDTO with additional printer-specific properties
*/
export interface Printer extends KeyValueDTOOfStringAndString {
/** Unique identifier for the printer */
key: string;
/** Display name of the printer */
value: string;
/** Whether this printer is currently selected as default */
selected: boolean;
/** Whether this printer is currently enabled for use */
enabled: boolean;
/** Additional information about the printer */
description: string;
}

View File

@@ -0,0 +1,34 @@
<p>
Bitte wählen Sie aus der Liste den Drucker aus, mit dem Sie drucken möchten.
</p>
@if (error()) {
<div>
<p class="text-isa-accent-red">{{ formatError(error()) }}</p>
</div>
}
<div
uiList
[value]="selected()"
(valueChange)="select($event.value[0])"
[compareWith]="compareWith"
[disabled]="printing()"
class="max-h-96 overflow-y-auto"
>
@for (printer of data.printers; track printer.key) {
<button uiTextListItem [value]="printer">{{ printer.value }}</button>
}
</div>
<div class="grid grid-cols-2 gap-2">
<button uiButton color="secondary" (click)="close({ printer: undefined })">
Verlassen
</button>
<button
uiButton
color="primary"
(click)="print()"
[disabled]="!canPrint()"
[pending]="printing()"
>
Drucken
</button>
</div>

View File

@@ -0,0 +1,127 @@
import {
ChangeDetectionStrategy,
Component,
computed,
signal,
} from '@angular/core';
import { Printer } from '../models';
import { ButtonComponent } from '@isa/ui/buttons';
import { DialogContentDirective } from '@isa/ui/dialog';
import { ListDirective, TextListItemDirective } from '@isa/ui/list';
/**
* Input data for the printer dialog component
*/
export interface PrinterDialogData {
/** Array of available printers to display */
printers: Printer[];
/** Optional error to display in the dialog */
error?: unknown;
/** Function to call when user selects a printer and clicks print */
print: (printer: Printer) => Promise<unknown>;
}
/**
* Result returned when the printer dialog closes
*/
export interface PrinterDialogResult {
/** Selected printer or undefined if user cancelled */
printer: Printer | undefined;
}
/**
* Dialog component for printer selection and print operations
* Displays a list of printers, allows selection, and handles print errors
*/
@Component({
selector: 'common-print-dialog',
templateUrl: './print-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonComponent, ListDirective, TextListItemDirective],
})
export class PrintDialogComponent extends DialogContentDirective<
PrinterDialogData,
PrinterDialogResult
> {
/** Signal for the currently selected printer */
printer = signal<Printer | undefined>(
this.data.printers.find((p) => p.selected),
);
/** Computed array of selected printers for the listbox component */
selected = computed(() => {
const printer = this.printer();
if (printer) {
return [printer];
}
return [];
});
/** Signal indicating if a print operation is in progress */
printing = signal(false);
/** Signal for displaying error information */
error = signal<unknown>(this.data.error);
/** Computed property determining if the print button should be enabled */
canPrint = computed(() => {
return this.printer() !== undefined && !this.printing();
});
/**
* Comparison function for printer objects
* @param a First printer to compare
* @param b Second printer to compare
* @returns True if the printers have the same key
*/
compareWith(a: Printer, b: Printer) {
return a.key === b.key;
}
/**
* Handles printer selection in the list
* @param printer The printer that was selected
*/
select(printer: Printer) {
this.error.set(undefined);
this.printer.set(printer);
}
/**
* Executes the print operation with the selected printer
* Sets error state if the print operation fails
* Closes the dialog with the selected printer if successful
*/
async print() {
this.error.set(undefined);
const printer = this.printer();
if (!this.canPrint() || !printer) {
return;
}
this.printing.set(true);
try {
await this.data.print(printer);
} catch (error) {
this.error.set(error);
this.printing.set(false);
return;
}
this.close({ printer });
}
/**
* Formats error objects for display in the UI
* @param error The error object to format
* @returns A user-friendly error message string
*/
formatError(error: unknown): string {
if (error instanceof Error) {
return error.message;
} else if (typeof error === 'string') {
return error;
} else {
return 'Unbekannter Fehler';
}
}
}

View File

@@ -0,0 +1 @@
export * from './print.service';

View File

@@ -0,0 +1,94 @@
import { inject, Injectable } from '@angular/core';
import { PrintService as PrintApiService } from '@generated/swagger/print-api';
import { Platform } from '@angular/cdk/platform';
import { firstValueFrom, map, Observable } from 'rxjs';
import { Printer, PrinterType } from '../models';
import { injectDialog } from '@isa/ui/dialog';
import { PrintDialogComponent } from '../print-dialog/print-dialog.component';
/**
* Service that provides access to printers available in the system.
* Communicates with the print API to retrieve label and office printers.
*/
@Injectable({ providedIn: 'root' })
export class PrintService {
#printService = inject(PrintApiService);
#printDailog = injectDialog(PrintDialogComponent, 'Drucken');
#platform = inject(Platform);
/**
* Retrieves a list of available label printers
* @returns Observable of label printer array
*/
labelPrinters(): Observable<Printer[]> {
return this.#printService
.PrintLabelPrinters()
.pipe(map((res) => res.result as Printer[]));
}
/**
* Retrieves a list of available office printers
* @returns Observable of office printer array
*/
officePrinters(): Observable<Printer[]> {
return this.#printService
.PrintOfficePrinters()
.pipe(map((res) => res.result as Printer[]));
}
/**
* Initiates a print operation with platform-specific optimizations
* On desktop, attempts to print directly using the default printer if available
* Falls back to showing a printer selection dialog if needed or on mobile devices
*
* @param printerType The type of printer to use (LABEL or OFFICE)
* @param printFn Function that performs the actual print operation with the selected printer
* @returns Object containing the selected printer or undefined if operation was cancelled
*/
async print(
printerType: PrinterType,
printFn: (printer: Printer) => Promise<unknown>,
): Promise<{ printer?: Printer }> {
// Get the list of printers based on the printer type
const printers$ =
printerType === PrinterType.LABEL
? this.labelPrinters()
: this.officePrinters();
const printers = await firstValueFrom(printers$);
// If the platform is not Android or iOS, we can assume this is stationary devices
// and we can try to print directly to the selected printer.
// If it fails, we show the print dialog with the error.
let error: unknown | undefined = undefined;
if (!(this.#platform.ANDROID || this.#platform.IOS)) {
const selectedPrinter = printers.find((p) => p.selected);
if (selectedPrinter) {
try {
await printFn(selectedPrinter);
return { printer: selectedPrinter };
} catch (e) {
error = e;
}
}
}
// Default behavior: show the print dialog
// and let the user select a printer.
const result = await firstValueFrom(
this.#printDailog({
data: {
printers,
error,
print: printFn,
},
}).closed,
);
// Returns a printer if the print was successful
// or undefined if the user canceled the dialog.
return { printer: result?.printer };
}
}

View File

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

View File

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

View File

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

View File

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

View File

View File

View File

View File

View File

View File

@@ -17,13 +17,5 @@ export * from './lib/errors';
export * from './lib/models';
export * from './lib/helpers/return-process';
export * from './lib/schemas';
export * from './lib/return-details.service';
export * from './lib/return-details.store';
export * from './lib/return-process.service';
export * from './lib/return-process.store';
export * from './lib/return-search.service';
export * from './lib/return-search.store';
export * from './lib/return-print-receipts.service';
export * from './lib/return-task-list.service';
export * from './lib/return-task-list.store';
export * from './lib/return-can-return.service';
export * from './lib/services';
export * from './lib/stores';

View File

@@ -1,14 +0,0 @@
export * from './errors';
export * from './models';
export * from './helpers/return-process';
export * from './return-details.service';
export * from './return-details.store';
export * from './return-process.service';
export * from './return-process.store';
export * from './return-search.service';
export * from './return-search.store';
export * from './return-print-receipts.service';
export * from './return-task-list.service';
export * from './return-task-list.store';
export * from './return-can-return.service';
export * from './schemas';

View File

@@ -1,67 +0,0 @@
import { inject, Injectable } from '@angular/core';
import { EnvironmentService } from '@core/environment';
import { DomainPrinterService, Printer } from '@domain/printer';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { UiModalService } from '@ui/modal';
import { firstValueFrom } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ReturnPrintReceiptsService {
#printService = inject(DomainPrinterService);
#environmentSerivce = inject(EnvironmentService);
// TODO: Refactor: CDK Dialog verwenden für neuen Printer Dialog
#uiModal = inject(UiModalService);
// TODO: Refactor: CDK Dialog verwenden für neuen Printer Dialog
/**
* Prints return receipts using the appropriate printer
*
* This method:
* 1. Retrieves available label printers
* 2. Determines if a printer is selected or if running on tablet mode
* 3. Opens a print modal dialog if no printer is selected or on tablet
* 4. Prints directly to the selected printer when available
*
* @param {number[]} receiptIds - Array of receipt IDs to print
* @returns {Promise<number[]>} Promise resolving to the same array of receipt IDs
* @throws {Error} When printing operations fail
*/
async printReturns(receiptIds: number[]) {
const printerList = await firstValueFrom(
this.#printService.getAvailableLabelPrinters(),
);
let printer: Printer | undefined = undefined;
if (Array.isArray(printerList)) {
printer = printerList.find((printer) => printer.selected === true);
}
if (!printer || this.#environmentSerivce.matchTablet()) {
await this.#uiModal
.open({
content: PrintModalComponent,
config: { showScrollbarY: false },
data: {
printImmediately: !this.#environmentSerivce.matchTablet(),
printerType: 'Label',
print: (printer) =>
this.#printService
.printReturnReceipt({
printer: printer,
receiptIds,
})
.toPromise(),
} as PrintModalData,
})
.afterClosed$.toPromise();
} else {
await firstValueFrom(
this.#printService.printReturnReceipt({
printer: printer.key,
receiptIds,
}),
);
}
return receiptIds;
}
}

View File

@@ -0,0 +1,6 @@
export * from './return-can-return.service';
export * from './return-details.service';
export * from './return-print-receipts.service';
export * from './return-process.service';
export * from './return-search.service';
export * from './return-task-list.service';

View File

@@ -3,16 +3,16 @@ import {
ReceiptService,
ReturnReceiptValuesDTO,
} from '@generated/swagger/oms-api';
import { ReturnReceiptValues, ReturnReceiptValuesSchema } from './schemas';
import { ReturnReceiptValues, ReturnReceiptValuesSchema } from '../schemas';
import { debounceTime, firstValueFrom, map } from 'rxjs';
import { CanReturn, ReturnProcess } from './models';
import { CanReturn, ReturnProcess } from '../models';
import {
allReturnProcessQuestionsAnswered,
getReturnProcessQuestions,
returnReceiptValuesMapping,
} from './helpers/return-process';
} from '../helpers/return-process';
import { memorize } from '@utils/common';
import { isReturnProcessTypeGuard } from './guards';
import { isReturnProcessTypeGuard } from '../guards';
/**
* Service for determining if a return process can proceed based on

View File

@@ -2,8 +2,8 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { ReturnDetailsService } from './return-details.service';
import { ReceiptService } from '@generated/swagger/oms-api';
import { of } from 'rxjs';
import { FetchReturnDetails } from './schemas';
import { Receipt } from './models';
import { FetchReturnDetails } from '../schemas';
import { Receipt } from '../models';
describe('ReturnDetailsService', () => {
let spectator: SpectatorService<ReturnDetailsService>;

View File

@@ -3,11 +3,11 @@ import {
FetchReturnDetails,
FetchReturnDetailsSchema,
ReturnReceiptValues,
} from './schemas';
} from '../schemas';
import { map, Observable, throwError } from 'rxjs';
import { ReceiptService } from '@generated/swagger/oms-api';
import { Receipt, ReceiptItem } from './models';
import { CategoryQuestions } from './questions';
import { Receipt, ReceiptItem } from '../models';
import { CategoryQuestions } from '../questions';
import { KeyValue } from '@angular/common';
import { ReturnCanReturnService } from './return-can-return.service';

View File

@@ -0,0 +1,25 @@
import { inject, Injectable } from '@angular/core';
import { OMSPrintService } from '@generated/swagger/print-api';
import { PrinterType, PrintService } from '@isa/common/print';
import { firstValueFrom } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ReturnPrintReceiptsService {
#omsPrintService = inject(OMSPrintService);
#printService = inject(PrintService);
async printReturnReceipts({
returnReceiptIds,
}: {
returnReceiptIds: number[];
}) {
return this.#printService.print(PrinterType.LABEL, (printer) => {
return firstValueFrom(
this.#omsPrintService.OMSPrintReturnReceipt({
printer: printer.key,
data: returnReceiptIds,
}),
);
});
}
}

View File

@@ -8,19 +8,19 @@ import {
ReturnProcessAnswers,
ReturnProcessQuestion,
ReturnProcessQuestionType,
} from './models';
import { ProductCategory } from './questions';
} from '../models';
import { ProductCategory } from '../questions';
import {
ReturnProcessChecklistAnswerSchema,
ReturnReceiptValues,
ReturnReceiptValuesSchema,
} from './schemas';
} from '../schemas';
import { logger } from '@isa/core/logging';
import {
PropertyIsEmptyError,
PropertyNullOrUndefinedError,
} from '@isa/common/data-access';
import { ReturnProcessIsNotCompleteError } from './errors/return-process';
import { ReturnProcessIsNotCompleteError } from '../errors/return-process';
import {
activeReturnProcessQuestions,
allReturnProcessQuestionsAnswered,
@@ -31,7 +31,7 @@ import {
isTolinoEligibleForReturn,
isTonDatentraegerEligibleForReturn,
returnReceiptValuesMapping,
} from './helpers/return-process';
} from '../helpers/return-process';
import { isEmpty, isNil } from 'lodash';
import {
ReceiptService,
@@ -301,7 +301,9 @@ export class ReturnProcessService {
const receipts = response.result as Receipt[];
const receiptIds = receipts.map((receipt) => receipt.id);
await this.#printReceiptsService.printReturns(receiptIds);
await this.#printReceiptsService.printReturnReceipts({
returnReceiptIds: receiptIds,
});
return receipts;
}

View File

@@ -1,13 +1,13 @@
import { inject, Injectable } from '@angular/core';
import { QuerySettingsDTO, ReceiptService } from '@generated/swagger/oms-api';
import { map, Observable, throwError } from 'rxjs';
import { QueryTokenInput, QueryTokenSchema } from './schemas';
import { ReceiptListItem } from './models';
import { QueryTokenInput, QueryTokenSchema } from '../schemas';
import { ReceiptListItem } from '../models';
import { ListResponseArgs } from '@isa/common/data-access';
import {
ReturnParseQueryTokenError,
ReturnSearchSearchError,
} from './errors/return-search.error';
} from '../errors/return-search.error';
import { ZodError } from 'zod';
@Injectable({ providedIn: 'root' })

View File

@@ -1,9 +1,9 @@
import { inject, Injectable } from '@angular/core';
import { map, Observable, throwError } from 'rxjs';
import { ReceiptItemTaskListItem, TaskActionTypeType } from './models';
import { QueryTokenInput, QueryTokenSchema } from './schemas';
import { ReceiptItemTaskListItem, TaskActionTypeType } from '../models';
import { QueryTokenInput, QueryTokenSchema } from '../schemas';
import { ZodError } from 'zod';
import { ReturnParseQueryTokenError } from './errors';
import { ReturnParseQueryTokenError } from '../errors';
import {
ReceiptService,
ResponseArgsOfReceiptItemTaskListItemDTO,

View File

@@ -0,0 +1,4 @@
export * from './return-details.store';
export * from './return-process.store';
export * from './return-search.store';
export * from './return-task-list.store';

View File

@@ -1,10 +1,10 @@
import { createServiceFactory } from '@ngneat/spectator/jest';
import { ReturnDetailsStore } from './return-details.store';
import { ReturnDetailsService } from './return-details.service';
import { ReturnDetailsService } from '../services';
import { patchState } from '@ngrx/signals';
import { AsyncResultStatus } from '@isa/common/data-access';
import { addEntity } from '@ngrx/signals/entities';
import { Receipt } from './models';
import { Receipt } from '../models';
import { of, throwError } from 'rxjs';
describe('ReturnDetailsStore', () => {

View File

@@ -4,9 +4,9 @@ import { AsyncResult, AsyncResultStatus } from '@isa/common/data-access';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { pipe, switchMap, tap } from 'rxjs';
import { inject } from '@angular/core';
import { ReturnDetailsService } from './return-details.service';
import { ReturnDetailsService } from '../services';
import { tapResponse } from '@ngrx/operators';
import { Receipt } from './models';
import { Receipt } from '../models';
/**
* Represents the result of a return operation, including the receipt data and status.

View File

@@ -4,8 +4,8 @@ import { IDBStorageProvider } from '@isa/core/storage';
import { ProcessService } from '@isa/core/process';
import { patchState } from '@ngrx/signals';
import { setAllEntities } from '@ngrx/signals/entities';
import { Product, Receipt, ReturnProcess } from './models';
import { CreateReturnProcessError } from './errors/return-process';
import { Product, Receipt, ReturnProcess } from '../models';
import { CreateReturnProcessError } from '../errors/return-process';
const TEST_ITEMS: Record<number, ReturnProcess['receiptItem']> = {
1: {

View File

@@ -13,12 +13,12 @@ import {
import { IDBStorageProvider, withStorage } from '@isa/core/storage';
import { computed, effect, inject } from '@angular/core';
import { ProcessService } from '@isa/core/process';
import { Receipt, ReceiptItem, ReturnProcess } from './models';
import { Receipt, ReceiptItem, ReturnProcess } from '../models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
CreateReturnProcessError,
CreateReturnProcessErrorReason,
} from './errors/return-process';
} from '../errors/return-process';
/**
* Interface representing the parameters required to start a return process.

View File

@@ -1,9 +1,9 @@
import { createServiceFactory } from '@ngneat/spectator/jest';
import { ReturnSearchStore, ReturnSearchStatus } from './return-search.store';
import { ReturnSearchService } from './return-search.service';
import { ReturnSearchService } from '../services';
import { of, throwError } from 'rxjs';
import { ListResponseArgs } from '@isa/common/data-access';
import { ReceiptListItem } from './models';
import { ReceiptListItem } from '../models';
describe('ReturnSearchStore', () => {
const createService = createServiceFactory({

View File

@@ -14,12 +14,12 @@ import {
withEntities,
} from '@ngrx/signals/entities';
import { pipe, switchMap, tap } from 'rxjs';
import { ReturnSearchService } from './return-search.service';
import { ReturnSearchService } from '../services';
import { tapResponse } from '@ngrx/operators';
import { effect, inject } from '@angular/core';
import { QueryTokenSchema } from './schemas';
import { QueryTokenSchema } from '../schemas';
import { Callback, ListResponseArgs } from '@isa/common/data-access';
import { ReceiptListItem } from './models';
import { ReceiptListItem } from '../models';
import { Query } from '@isa/shared/filter';
import { SessionStorageProvider, withStorage } from '@isa/core/storage';
import { ProcessService } from '@isa/core/process';

View File

@@ -1,14 +1,14 @@
import { patchState, signalStore, withMethods } from '@ngrx/signals';
import { withEntities, updateEntity, addEntity } from '@ngrx/signals/entities';
import { inject } from '@angular/core';
import { ReceiptItemTaskListItem } from './models';
import { ReceiptItemTaskListItem } from '../models';
import { AsyncResult, AsyncResultStatus } from '@isa/common/data-access';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { ReturnTaskListService } from './return-task-list.service';
import { ReturnTaskListService } from '../services';
import { pipe, switchMap, tap } from 'rxjs';
import { tapResponse } from '@ngrx/operators';
import { logger } from '@isa/core/logging';
import { QueryTokenInput } from './schemas';
import { QueryTokenInput } from '../schemas';
/**
* Represents the structure of a return task list item in the store.

View File

@@ -27,7 +27,9 @@ export class ReturnReviewComponent {
this.#returnProcessStore.entityMap()[processId].receiptId;
if (receiptId) {
await this.#printReceiptsService.printReturns([receiptId]);
await this.#printReceiptsService.printReturnReceipts({
returnReceiptIds: [receiptId],
});
}
}
}

View File

@@ -1,4 +1,10 @@
import { ChangeDetectionStrategy, Component, effect, inject, untracked } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
effect,
inject,
untracked,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { injectActivatedProcessId } from '@isa/core/process';
@@ -19,9 +25,15 @@ function querySettingsFactory() {
template: `<router-outlet></router-outlet>`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterOutlet],
providers: [provideFilter(withQuerySettingsFactory(querySettingsFactory), withQueryParamsSync())],
providers: [
provideFilter(
withQuerySettingsFactory(querySettingsFactory),
withQueryParamsSync(),
),
],
host: {
'[class]': '"flex flex-col gap-5 isa-desktop:gap-6 items-center overflow-x-hidden"',
'[class]':
'"flex flex-col gap-5 isa-desktop:gap-6 items-center overflow-x-hidden"',
},
})
export class ReturnSearchComponent {
@@ -64,7 +76,10 @@ export class ReturnSearchComponent {
if (items) {
if (items?.length === 1) {
return await this._navigateTo(['receipts', items[0].id.toString()]);
return await this._navigateTo([
'receipts',
items[0].id.toString(),
]);
}
if (items?.length >= 0) {

7
libs/ui/dialog/README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
.ui-dialog {
@apply bg-isa-white p-8 flex gap-8 items-start rounded-[2rem] flex-col text-isa-neutral-900;
.ui-dialog-title {
@apply isa-text-subtitle-1-bold;
}
.ui-dialog-content {
@apply flex flex-col gap-8;
}
}

View File

@@ -0,0 +1,5 @@
export * from './lib/dialog-content.directive';
export * from './lib/dialog.component';
export * from './lib/injects';
export * from './lib/message-dialog/message-dialog.component';
export * from './lib/tokens';

View File

@@ -0,0 +1,30 @@
import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog';
import { Directive, inject } from '@angular/core';
/**
* Base directive for dialog content components
* Provides common dialog functionality and styling
*
* @template D Type of the data passed to the dialog
* @template R Type of the result returned when the dialog closes
*/
@Directive({
host: {
'[class]': '["ui-dialog-content"]',
},
})
export abstract class DialogContentDirective<D, R> {
/** Reference to the dialog instance */
readonly dialogRef = inject(DialogRef<R, DialogContentDirective<D, R>>);
/** Data passed to the dialog */
readonly data = inject(DIALOG_DATA) as D;
/**
* Closes the dialog with the specified result
* @param result The value to return from the dialog
*/
close(result: R): void {
this.dialogRef.close(result);
}
}

View File

@@ -0,0 +1,5 @@
<h2 class="ui-dialog-title">
{{ title }}
</h2>
<ng-container *ngComponentOutlet="component"> </ng-container>

View File

@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { DialogContentDirective } from './dialog-content.directive';
import { DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
import { ComponentType } from '@angular/cdk/portal';
import { NgComponentOutlet } from '@angular/common';
/**
* Base dialog component that serves as a container for dialog content
* Handles the outer dialog shell including title and content projection
*
* @template D Type of data passed to the dialog
* @template R Type of result returned from the dialog
* @template C Type of content component displayed in the dialog
*/
@Component({
selector: 'ui-dialog',
templateUrl: './dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgComponentOutlet],
host: {
'[class]': '["ui-dialog"]',
},
})
export class DialogComponent<D, R, C extends DialogContentDirective<D, R>> {
/** The title to display at the top of the dialog */
title = inject(DIALOG_TITLE);
/** The component type to instantiate as the dialog content */
readonly component = inject(DIALOG_CONTENT) as ComponentType<C>;
}

View File

@@ -0,0 +1,74 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Dialog } from '@angular/cdk/dialog';
import { ComponentType } from '@angular/cdk/portal';
import { inject, Injector } from '@angular/core';
import { DialogContentDirective } from './dialog-content.directive';
import { DialogComponent } from './dialog.component';
import { DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
import { MessageDialogComponent } from './message-dialog/message-dialog.component';
/**
* Options for opening a dialog using injectDialog function
* @template D The type of data passed to the dialog
*/
export interface OpenDialogOptions<D> {
/** Optional title override for the dialog */
title?: string;
/** Data to pass to the dialog component */
data: D;
}
/**
* Creates a function to open a dialog with a specific component
* Uses Angular's DI system for dialog configuration
*
* @template C The type of component to display in the dialog
* @param componentType The component class to instantiate in the dialog
* @param title Default title for the dialog
* @returns A function that opens the dialog with provided options
*/
export function injectDialog<C extends DialogContentDirective<any, any>>(
componentType: ComponentType<C>,
title?: string,
) {
type D = C extends DialogContentDirective<infer D, any> ? D : never;
type R = C extends DialogContentDirective<any, infer R> ? R : never;
const cdkDialog = inject(Dialog);
const injector = inject(Injector);
return (options?: OpenDialogOptions<D>) => {
const dialogInjector = Injector.create({
parent: injector,
providers: [
{
provide: DIALOG_CONTENT,
useValue: componentType,
},
{
provide: DIALOG_TITLE,
useValue: options?.title ?? title,
},
],
});
const dialogRef = cdkDialog.open<R, D, DialogComponent<D, R, C>>(
DialogComponent<D, R, C>,
{
data: options?.data,
injector: dialogInjector,
width: '30rem',
hasBackdrop: true,
disableClose: true,
},
);
return dialogRef;
};
}
/**
* Convenience function that returns a pre-configured MessageDialog injector
* @returns A function to open a message dialog
*/
export const injectMessageDialog = () => injectDialog(MessageDialogComponent);

View File

@@ -0,0 +1,8 @@
<p class="isa-text-body-1-regular text-isa-neutral-600">
{{ data.message }}
</p>
<div class="text-right">
<button uiButton (click)="close()">
{{ data.closeText || 'Schließen' }}
</button>
</div>

View File

@@ -0,0 +1,31 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ButtonComponent } from '@isa/ui/buttons';
import { DialogContentDirective } from '../dialog-content.directive';
/**
* Input data for the message dialog
*/
export interface MessageDialogData {
/** The message text to display in the dialog */
message: string;
/** Optional custom text for the close button (defaults to "Close" or equivalent) */
closeText?: string;
}
/**
* Simple message dialog component
* Used for displaying informational messages to the user
* Returns void when closed (no result)
*/
@Component({
selector: 'ui-message-dialog',
templateUrl: './message-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonComponent],
})
export class MessageDialogComponent extends DialogContentDirective<
MessageDialogData,
void
> {}

View File

@@ -0,0 +1,16 @@
import { ComponentType } from '@angular/cdk/portal';
import { InjectionToken } from '@angular/core';
/**
* Injection token for providing the dialog title text
* Used internally by the dialog system to display the title
*/
export const DIALOG_TITLE = new InjectionToken<string>('DIALOG_TITLE');
/**
* Injection token for providing the dialog content component
* Used internally by the dialog system to instantiate the correct content component
*/
export const DIALOG_CONTENT = new InjectionToken<ComponentType<unknown>>(
'DIALOG_CONTENT',
);

View File

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

View File

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

View File

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

View File

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

7
libs/ui/list/README.md Normal file
View File

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

View File

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

View File

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

20
libs/ui/list/project.json Normal file
View File

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

View File

@@ -0,0 +1,2 @@
export * from './lib/list.directive';
export * from './lib/text-list-item.directive';

View File

@@ -0,0 +1,14 @@
.ui-list {
@apply flex flex-col items-stretch w-full;
}
.ui-text-list-item {
@apply flex h-12 px-6 flex-col justify-center items-start gap-[0.65rem] rounded-[.5rem] text-isa-neutral-700 bg-isa-white;
@apply isa-text-body-2-bold;
&.cdk-option-active,
&.active,
&:hover {
@apply text-isa-accent-blue bg-isa-neutral-200;
}
}

View File

View File

@@ -0,0 +1,34 @@
import { Directive } from '@angular/core';
import { CdkListbox } from '@angular/cdk/listbox';
/**
* Directive that adds UI styling to an Angular CDK listbox
* Simplifies listbox creation by exposing common CDK listbox inputs/outputs
* with more intuitive names for the application design system.
*
* Usage:
* ```html
* <div uiList [value]="selected" (valueChange)="handleChange($event)"
* [compareWith]="compareItems" [disabled]="isDisabled">
* <!-- List items go here -->
* </div>
* ```
*/
@Directive({
selector: '[uiList]',
host: {
class: 'ui-list',
},
hostDirectives: [
{
directive: CdkListbox,
inputs: [
'cdkListboxValue: value',
'cdkListboxCompareWith: compareWith',
'cdkListboxDisabled: disabled',
],
outputs: ['cdkListboxValueChange: valueChange'],
},
],
})
export class ListDirective {}

View File

View File

View File

@@ -0,0 +1,27 @@
import { Directive } from '@angular/core';
import { CdkOption } from '@angular/cdk/listbox';
/**
* Directive for text-based list items within a uiList container
* Applies text-specific styling and behavior for list items
*
* Usage:
* ```html
* <button uiTextListItem [value]="item" [disabled]="isItemDisabled">
* {{ item.label }}
* </button>
* ```
*/
@Directive({
selector: '[uiTextListItem]',
host: {
class: 'ui-text-list-item',
},
hostDirectives: [
{
directive: CdkOption,
inputs: ['cdkOption: value', 'cdkOptionDisabled: disabled'],
},
],
})
export class TextListItemDirective {}

View File

@@ -0,0 +1 @@
@use "./lib/list";

View File

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

View File

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

View File

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

View File

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

3787
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -71,7 +71,7 @@
"@nx/eslint": "20.4.6",
"@nx/eslint-plugin": "20.4.6",
"@nx/jest": "20.4.6",
"@nx/js": "20.4.6",
"@nx/js": "20.8.1",
"@nx/storybook": "^20.4.6",
"@nx/web": "20.4.6",
"@nx/workspace": "20.4.6",
@@ -93,7 +93,7 @@
"@types/node": "18.16.9",
"@types/uuid": "^10.0.0",
"@typescript-eslint/utils": "^8.19.0",
"angular-eslint": "^19.1.0",
"angular-eslint": "^19.2.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.8.0",
"eslint-config-prettier": "^9.0.0",

View File

@@ -41,6 +41,7 @@
"@hub/*": ["apps/isa-app/src/hub/*/index.ts"],
"@isa/catalogue/data-access": ["libs/catalogue/data-access/src/index.ts"],
"@isa/common/data-access": ["libs/common/data-access/src/index.ts"],
"@isa/common/print": ["libs/common/print/src/index.ts"],
"@isa/core/config": ["libs/core/config/src/index.ts"],
"@isa/core/logging": ["libs/core/logging/src/index.ts"],
"@isa/core/notifications": ["libs/core/notifications/src/index.ts"],
@@ -73,10 +74,12 @@
"@isa/shared/product-image": ["libs/shared/product-image/src/index.ts"],
"@isa/ui/buttons": ["libs/ui/buttons/src/index.ts"],
"@isa/ui/datepicker": ["libs/ui/datepicker/src/index.ts"],
"@isa/ui/dialog": ["libs/ui/dialog/src/index.ts"],
"@isa/ui/empty-state": ["libs/ui/empty-state/src/index.ts"],
"@isa/ui/input-controls": ["libs/ui/input-controls/src/index.ts"],
"@isa/ui/item-rows": ["libs/ui/item-rows/src/index.ts"],
"@isa/ui/layout": ["libs/ui/layout/src/index.ts"],
"@isa/ui/list": ["libs/ui/list/src/index.ts"],
"@isa/ui/progress-bar": ["libs/ui/progress-bar/src/index.ts"],
"@isa/ui/search-bar": ["libs/ui/search-bar/src/index.ts"],
"@isa/ui/toolbar": ["libs/ui/toolbar/src/index.ts"],