refactor(print): update print dialog component to use new listbox directives

test(print): add unit tests for PrintService and PrintReceiptsService

feat(print): modify PrintService methods to return promises instead of observables

refactor(oms): rename return-print-receipts.service to print-receipts.service and update references

chore(ui): remove deprecated ui-list library and integrate listbox components

style(ui): add styles for listbox and listbox items

test(ui): implement unit tests for listbox directives

docs(ui): update README and remove unused files related to ui-list
This commit is contained in:
Lorenz Hilpert
2025-05-22 19:07:00 +02:00
parent 4bbdb870f8
commit cdd27aeeb0
36 changed files with 814 additions and 239 deletions

View File

@@ -30,9 +30,6 @@
}
],
"github.copilot.chat.codeGeneration.instructions": [
{
"file": ".github/copilot-instructions.md"
},
{
"file": "docs/tech-stack.md"
},
@@ -50,9 +47,6 @@
}
],
"github.copilot.chat.testGeneration.instructions": [
{
"file": ".github/copilot-instructions.md"
},
{
"file": ".github/testing-instructions.md"
},
@@ -90,4 +84,6 @@
}
],
"nxConsole.generateAiAgentRules": true,
"chat.mcp.enabled": true,
"chat.mcp.discovery.enabled": true
}

View File

@@ -2,5 +2,4 @@
@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

@@ -13,6 +13,4 @@ export interface Printer extends KeyValueDTOOfStringAndString {
selected: boolean;
/** Whether this printer is currently enabled for use */
enabled: boolean;
/** Additional information about the printer */
description: string;
}

View File

@@ -7,7 +7,7 @@
</div>
}
<div
uiList
uiListbox
[value]="selected()"
(valueChange)="select($event.value[0])"
[compareWith]="compareWith"
@@ -15,7 +15,7 @@
class="max-h-96 overflow-y-auto"
>
@for (printer of data.printers; track printer.key) {
<button uiTextListItem [value]="printer">{{ printer.value }}</button>
<button uiListboxItem [value]="printer">{{ printer.value }}</button>
}
</div>
<div class="grid grid-cols-2 gap-2">

View File

@@ -0,0 +1,290 @@
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import {
PrintDialogComponent,
PrinterDialogData,
} from './print-dialog.component';
import { ButtonComponent } from '@isa/ui/buttons';
import { ListboxDirective, ListboxItemDirective } from '@isa/ui/input-controls';
import { MockComponent, MockDirective } from 'ng-mocks';
import { Printer } from '../models';
import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog';
describe('PrintDialogComponent', () => {
let spectator: Spectator<PrintDialogComponent>;
let component: PrintDialogComponent;
// Mock printers for testing
const mockPrinters: Printer[] = [
{
key: 'printer1',
value: 'Printer 1',
selected: false,
enabled: true,
description: 'First test printer',
},
{
key: 'printer2',
value: 'Printer 2',
selected: true,
enabled: true,
description: 'Second test printer',
},
{
key: 'printer3',
value: 'Printer 3',
selected: false,
enabled: false,
description: 'Disabled test printer',
},
];
// Mock print function
const mockPrintFn = jest.fn().mockResolvedValue(undefined);
// Default dialog data
const defaultData: PrinterDialogData = {
printers: mockPrinters,
print: mockPrintFn,
};
// Mock DialogRef
const mockDialogRef = {
close: jest.fn(),
};
const createComponent = createComponentFactory({
component: PrintDialogComponent,
declarations: [
MockComponent(ButtonComponent),
MockDirective(ListboxDirective),
MockDirective(ListboxItemDirective),
],
providers: [
{ provide: DialogRef, useValue: mockDialogRef },
{ provide: DIALOG_DATA, useValue: defaultData },
],
detectChanges: false,
});
beforeEach(() => {
// Reset mocks
mockPrintFn.mockClear();
mockDialogRef.close.mockClear();
// Create component without providing data prop since we provide it via DIALOG_DATA
spectator = createComponent();
component = spectator.component;
spectator.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initialize with the selected printer', () => {
// Assert
expect(component.printer()).toEqual(mockPrinters[1]); // The one with selected: true
});
it('should compute selected array correctly with a printer', () => {
// Arrange
const selectedPrinter = mockPrinters[0];
component.printer.set(selectedPrinter);
// Act
const result = component.selected();
// Assert
expect(result).toEqual([selectedPrinter]);
});
it('should compute selected array as empty when no printer is selected', () => {
// Arrange
component.printer.set(undefined);
// Act
const result = component.selected();
// Assert
expect(result).toEqual([]);
});
it('should compute canPrint as true when printer is selected and not printing', () => {
// Arrange
component.printer.set(mockPrinters[0]);
component.printing.set(false);
// Act
const result = component.canPrint();
// Assert
expect(result).toBe(true);
});
it('should compute canPrint as false when no printer is selected', () => {
// Arrange
component.printer.set(undefined);
component.printing.set(false);
// Act
const result = component.canPrint();
// Assert
expect(result).toBe(false);
});
it('should compute canPrint as false when printing is in progress', () => {
// Arrange
component.printer.set(mockPrinters[0]);
component.printing.set(true);
// Act
const result = component.canPrint();
// Assert
expect(result).toBe(false);
});
it('should compare printers by key', () => {
// Arrange
const printer1 = { ...mockPrinters[0] };
const printer2 = { ...mockPrinters[0] }; // Same key as printer1
const printer3 = { ...mockPrinters[1] }; // Different key
// Act & Assert
expect(component.compareWith(printer1, printer2)).toBe(true);
expect(component.compareWith(printer1, printer3)).toBe(false);
});
it('should select a printer and clear error', () => {
// Arrange
const initialError = new Error('Test error');
component.error.set(initialError);
const printer = mockPrinters[0];
// Act
component.select(printer);
// Assert
expect(component.printer()).toEqual(printer);
expect(component.error()).toBeUndefined();
});
it('should not print when canPrint is false', async () => {
// Arrange
component.printer.set(undefined); // Makes canPrint() false
const closeSpy = jest.spyOn(component, 'close');
// Act
await component.print();
// Assert
expect(mockPrintFn).not.toHaveBeenCalled();
expect(closeSpy).not.toHaveBeenCalled();
expect(component.printing()).toBe(false);
});
it('should print and close dialog when print succeeds', async () => {
// Arrange
const selectedPrinter = mockPrinters[0];
component.printer.set(selectedPrinter);
const closeSpy = jest.spyOn(component, 'close');
// Act
await component.print();
// Assert
// The printing flag stays true when success happens and dialog is closed
expect(component.printing()).toBe(true);
expect(mockPrintFn).toHaveBeenCalledWith(selectedPrinter);
expect(closeSpy).toHaveBeenCalledWith({ printer: selectedPrinter });
});
it('should handle print errors', async () => {
// Arrange
const selectedPrinter = mockPrinters[0];
component.printer.set(selectedPrinter);
const testError = new Error('Print failed');
mockPrintFn.mockRejectedValueOnce(testError);
const closeSpy = jest.spyOn(component, 'close');
// Act
await component.print();
// Assert
expect(component.printing()).toBe(false); // Reset to false after error
expect(mockPrintFn).toHaveBeenCalledWith(selectedPrinter);
expect(closeSpy).not.toHaveBeenCalled();
expect(component.error()).toBe(testError);
});
it('should format Error objects correctly', () => {
// Arrange
const errorMessage = 'Test error message';
const testError = new Error(errorMessage);
// Act
const result = component.formatError(testError);
// Assert
expect(result).toBe(errorMessage);
});
it('should format string errors correctly', () => {
// Arrange
const errorMessage = 'Test error message';
// Act
const result = component.formatError(errorMessage);
// Assert
expect(result).toBe(errorMessage);
});
it('should format unknown errors correctly', () => {
// Arrange
const unknownError = { something: 'wrong' };
// Act
const result = component.formatError(unknownError);
// Assert
expect(result).toBe('Unbekannter Fehler');
});
it('should show error message in template when error exists', () => {
// Arrange
const errorMessage = 'Display this error';
component.error.set(errorMessage);
spectator.detectChanges();
// Act
const errorElement = spectator.query('.text-isa-accent-red');
// Assert
expect(errorElement).toHaveText(errorMessage);
});
it('should display printers in the listbox', () => {
// Arrange & Act
spectator.detectChanges();
const listboxItems = spectator.queryAll('button[uiListboxItem]');
// Assert
expect(listboxItems.length).toBe(mockPrinters.length);
expect(listboxItems[0]).toHaveText(mockPrinters[0].value);
expect(listboxItems[1]).toHaveText(mockPrinters[1].value);
expect(listboxItems[2]).toHaveText(mockPrinters[2].value);
});
it('should call close with undefined printer when cancel button is clicked', () => {
// Arrange
const closeSpy = jest.spyOn(component, 'close');
// Act
spectator.click('button[color="secondary"]');
// Assert
expect(closeSpy).toHaveBeenCalledWith({ printer: undefined });
});
});

View File

@@ -8,7 +8,7 @@ import {
import { Printer } from '../models';
import { ButtonComponent } from '@isa/ui/buttons';
import { DialogContentDirective } from '@isa/ui/dialog';
import { ListDirective, TextListItemDirective } from '@isa/ui/list';
import { ListboxDirective, ListboxItemDirective } from '@isa/ui/input-controls';
/**
* Input data for the printer dialog component
@@ -38,7 +38,7 @@ export interface PrinterDialogResult {
selector: 'common-print-dialog',
templateUrl: './print-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ButtonComponent, ListDirective, TextListItemDirective],
imports: [ButtonComponent, ListboxDirective, ListboxItemDirective],
})
export class PrintDialogComponent extends DialogContentDirective<
PrinterDialogData,

View File

@@ -0,0 +1,279 @@
import { SpectatorService, createServiceFactory } from '@ngneat/spectator/jest';
import { PrintService } from './print.service';
import { Platform } from '@angular/cdk/platform';
import {
ListResponseArgsOfKeyValueDTOOfStringAndString,
PrintService as PrintApiService,
} from '@generated/swagger/print-api';
import { of } from 'rxjs';
import { PrinterType } from '../models';
import { PrinterDialogResult } from '../print-dialog/print-dialog.component';
// Mock dialog function that can be configured in tests
const mockDialogClosedObservable = {
closed: of<PrinterDialogResult | undefined>({ printer: undefined }),
};
const mockDialogFn = jest.fn().mockReturnValue(mockDialogClosedObservable);
jest.mock('@isa/ui/dialog', () => ({
injectDialog: jest.fn().mockImplementation(() => mockDialogFn),
}));
jest.mock('../print-dialog/print-dialog.component', () => ({
PrintDialogComponent: jest
.fn()
.mockReturnValue(class MockPrintDialogComponent {}),
}));
describe('PrintService', () => {
let spectator: SpectatorService<PrintService>;
const createService = createServiceFactory({
service: PrintService,
mocks: [PrintApiService, Platform],
});
beforeEach(() => {
spectator = createService();
});
it('should be created', () => {
expect(spectator.service).toBeTruthy();
});
describe('labelPrinters', () => {
it('should call PrintLabelPrinters and return printers', async () => {
const mockPrinters: ListResponseArgsOfKeyValueDTOOfStringAndString = {
error: false,
result: [
{
key: 'printer1',
value: 'Printer 1',
},
{
key: 'printer2',
value: 'Printer 2',
},
],
};
const printApiService: jest.Mocked<PrintApiService> =
spectator.inject(PrintApiService);
printApiService.PrintLabelPrinters.mockReturnValue(of(mockPrinters));
const printers = await spectator.service.labelPrinters();
expect(printApiService.PrintLabelPrinters).toHaveBeenCalled();
expect(printers).toEqual(mockPrinters.result);
});
});
describe('officePrinters', () => {
it('should call PrintOfficePrinters and return printers', async () => {
const mockPrinters: ListResponseArgsOfKeyValueDTOOfStringAndString = {
error: false,
result: [
{
key: 'printer1',
value: 'Printer 1',
},
{
key: 'printer2',
value: 'Printer 2',
},
],
};
const printApiService: jest.Mocked<PrintApiService> =
spectator.inject(PrintApiService);
printApiService.PrintOfficePrinters.mockReturnValue(of(mockPrinters));
const printers = await spectator.service.officePrinters();
expect(printApiService.PrintOfficePrinters).toHaveBeenCalled();
expect(printers).toEqual(mockPrinters.result);
});
});
describe('printers', () => {
it('should call labelPrinters when printerType is LABEL', async () => {
const labelPrintersSpy = jest
.spyOn(spectator.service, 'labelPrinters')
.mockReturnValue(Promise.resolve([]));
await spectator.service.printers(PrinterType.LABEL);
expect(labelPrintersSpy).toHaveBeenCalled();
});
it('should call officePrinters when printerType is OFFICE', async () => {
const officePrintersSpy = jest
.spyOn(spectator.service, 'officePrinters')
.mockReturnValue(Promise.resolve([]));
await spectator.service.printers(PrinterType.OFFICE);
expect(officePrintersSpy).toHaveBeenCalled();
});
});
describe('print', () => {
const mockPrinters = [
{ key: 'printer1', value: 'Printer 1', selected: false, enabled: true },
{ key: 'printer2', value: 'Printer 2', selected: true, enabled: true },
];
let mockPrint: jest.Mock;
let platform: jest.Mocked<Platform>;
let printSpy: jest.SpyInstance;
beforeEach(() => {
mockPrint = jest.fn().mockResolvedValue(undefined);
platform = spectator.inject(Platform);
// Mock the printers method to return our mock printers
printSpy = jest
.spyOn(spectator.service, 'printers')
.mockResolvedValue(mockPrinters);
// Reset platform flags before each test
platform.ANDROID = false;
platform.IOS = false;
// Reset the dialog mock before each test
mockDialogFn.mockClear();
mockDialogClosedObservable.closed = of({ printer: mockPrinters[1] });
});
it('should fetch printers of the specified type', async () => {
// Act
await spectator.service.print(PrinterType.LABEL, mockPrint);
// Assert
expect(printSpy).toHaveBeenCalledWith(PrinterType.LABEL);
});
it('should attempt direct printing on desktop when a printer is selected', async () => {
// Act
await spectator.service.print(PrinterType.LABEL, mockPrint);
// Assert
expect(mockPrint).toHaveBeenCalledWith(mockPrinters[1]);
expect(mockDialogFn).not.toHaveBeenCalled();
});
it('should return the selected printer after successful direct print', async () => {
// Act
const result = await spectator.service.print(
PrinterType.LABEL,
mockPrint,
);
// Assert
expect(result).toEqual({ printer: mockPrinters[1] });
});
it('should show dialog with error when direct printing fails', async () => {
// Arrange
const printError = new Error('Print failed');
mockPrint.mockRejectedValueOnce(printError);
// Act
await spectator.service.print(PrinterType.LABEL, mockPrint);
// Assert
expect(mockDialogFn).toHaveBeenCalledWith({
data: {
printers: mockPrinters,
error: printError,
print: mockPrint,
},
});
});
it('should not attempt direct printing on Android', async () => {
// Arrange
platform.ANDROID = true;
// Act
await spectator.service.print(PrinterType.LABEL, mockPrint);
// Assert
expect(mockPrint).not.toHaveBeenCalled();
expect(mockDialogFn).toHaveBeenCalled();
});
it('should not attempt direct printing on iOS', async () => {
// Arrange
platform.IOS = true;
// Act
await spectator.service.print(PrinterType.LABEL, mockPrint);
// Assert
expect(mockPrint).not.toHaveBeenCalled();
expect(mockDialogFn).toHaveBeenCalled();
});
it('should show print dialog when no printer is selected', async () => {
// Arrange
const printersWithoutSelection = [
{ key: 'printer1', value: 'Printer 1', selected: false, enabled: true },
{ key: 'printer2', value: 'Printer 2', selected: false, enabled: true },
];
printSpy.mockResolvedValueOnce(printersWithoutSelection);
// Act
await spectator.service.print(PrinterType.LABEL, mockPrint);
// Assert
expect(mockPrint).not.toHaveBeenCalled();
expect(mockDialogFn).toHaveBeenCalled();
});
it('should pass the correct data to the print dialog', async () => {
// Arrange
platform.ANDROID = true; // Force dialog to show
// Act
await spectator.service.print(PrinterType.LABEL, mockPrint);
// Assert
expect(mockDialogFn).toHaveBeenCalledWith({
data: {
printers: mockPrinters,
error: undefined,
print: mockPrint,
},
});
});
it('should return the printer selected in the dialog', async () => {
// Arrange
platform.ANDROID = true; // Force dialog to show
const selectedPrinter = mockPrinters[0];
mockDialogClosedObservable.closed = of({ printer: selectedPrinter });
// Act
const result = await spectator.service.print(
PrinterType.LABEL,
mockPrint,
);
// Assert
expect(result).toEqual({ printer: selectedPrinter });
});
it('should return undefined printer when dialog is cancelled', async () => {
// Arrange
platform.ANDROID = true; // Force dialog to show
mockDialogClosedObservable.closed = of(undefined);
// Act
const result = await spectator.service.print(
PrinterType.LABEL,
mockPrint,
);
// Assert
expect(result).toEqual({ printer: undefined });
});
});
});

View File

@@ -1,7 +1,7 @@
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 { firstValueFrom, map } from 'rxjs';
import { Printer, PrinterType } from '../models';
import { injectDialog } from '@isa/ui/dialog';
@@ -21,20 +21,30 @@ export class PrintService {
* Retrieves a list of available label printers
* @returns Observable of label printer array
*/
labelPrinters(): Observable<Printer[]> {
return this.#printService
labelPrinters(): Promise<Printer[]> {
return firstValueFrom(
this.#printService
.PrintLabelPrinters()
.pipe(map((res) => res.result as Printer[]));
.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
officePrinters(): Promise<Printer[]> {
return firstValueFrom(
this.#printService
.PrintOfficePrinters()
.pipe(map((res) => res.result as Printer[]));
.pipe(map((res) => res.result as Printer[])),
);
}
printers(printerType: PrinterType): Promise<Printer[]> {
return printerType === PrinterType.LABEL
? this.labelPrinters()
: this.officePrinters();
}
/**
@@ -48,24 +58,21 @@ export class PrintService {
*/
async print(
printerType: PrinterType,
printFn: (printer: Printer) => Promise<unknown>,
printFn: (printer: Printer) => Promise<any>,
): 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 this.printers(printerType);
const printers = await firstValueFrom(printers$);
const selectedPrinter = printers.find((p) => p.selected);
// 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);
const directPrintAllowed =
selectedPrinter && !(this.#platform.ANDROID || this.#platform.IOS);
if (selectedPrinter) {
let error: unknown | undefined = undefined;
if (directPrintAllowed) {
try {
await printFn(selectedPrinter);
return { printer: selectedPrinter };
@@ -73,7 +80,6 @@ export class PrintService {
error = e;
}
}
}
// Default behavior: show the print dialog
// and let the user select a printer.

View File

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

View File

@@ -0,0 +1,68 @@
import { SpectatorService, createServiceFactory } from '@ngneat/spectator/jest';
import { PrintReceiptsService } from './print-receipts.service';
import { Printer, PrinterType, PrintService } from '@isa/common/print';
import { OMSPrintService } from '@generated/swagger/print-api';
import { of } from 'rxjs';
describe('PrintReceiptsService', () => {
let spectator: SpectatorService<PrintReceiptsService>;
const createService = createServiceFactory({
service: PrintReceiptsService,
mocks: [OMSPrintService, PrintService],
});
let mockPrintService: jest.Mocked<PrintService>;
let mockOmsPrintService: jest.Mocked<OMSPrintService>;
beforeEach(() => {
spectator = createService();
mockPrintService = spectator.inject(PrintService);
mockOmsPrintService = spectator.inject(OMSPrintService);
});
it('should be created', () => {
expect(spectator.service).toBeTruthy();
});
describe('printReturnReceipts', () => {
it('should throw an error if no return receipt IDs are provided', async () => {
await expect(
spectator.service.printReturnReceipts({ returnReceiptIds: [] }),
).rejects.toThrow('No return receipt IDs provided');
});
it('should call the print service with the correct parameters', async () => {
const mockReturnReceiptIds = [1, 2, 3];
mockPrintService.print.mockImplementation((printerType, callback) => {
expect(printerType).toBe(PrinterType.LABEL);
const mockPrinter: Printer = {
key: 'mockPrinterKey',
value: 'Mock Printer',
selected: true,
enabled: true,
description: 'Mock printer description',
};
return callback(mockPrinter);
});
mockOmsPrintService.OMSPrintReturnReceipt.mockReturnValue(
of({ error: false }),
);
await spectator.service.printReturnReceipts({
returnReceiptIds: mockReturnReceiptIds,
});
expect(mockPrintService.print).toHaveBeenCalledWith(
expect.anything(),
expect.any(Function),
);
expect(mockOmsPrintService.OMSPrintReturnReceipt).toHaveBeenCalledWith({
printer: expect.any(String),
data: mockReturnReceiptIds,
});
});
});
});

View File

@@ -0,0 +1,43 @@
import { inject, Injectable } from '@angular/core';
import { OMSPrintService } from '@generated/swagger/print-api';
import { PrinterType, PrintService } from '@isa/common/print';
import { firstValueFrom } from 'rxjs';
/**
* Service responsible for printing return receipts using the OMS print API.
*
* This service handles the communication with backend printing services and manages
* the print job lifecycle through the common print infrastructure.
*/
@Injectable({ providedIn: 'root' })
export class PrintReceiptsService {
#omsPrintService = inject(OMSPrintService);
#printService = inject(PrintService);
/**
* Prints return receipts for the provided receipt IDs.
*
* @param options - The printing options
* @param options.returnReceiptIds - Array of return receipt IDs to print
* @throws {Error} When no return receipt IDs are provided
* @returns A promise that resolves when the print job is complete
*/
async printReturnReceipts({
returnReceiptIds,
}: {
returnReceiptIds: number[];
}) {
if (returnReceiptIds.length === 0) {
throw new Error('No return receipt IDs provided');
}
return this.#printService.print(PrinterType.LABEL, (printer) => {
return firstValueFrom(
this.#omsPrintService.OMSPrintReturnReceipt({
printer: printer.key,
data: returnReceiptIds,
}),
);
});
}
}

View File

@@ -1,25 +0,0 @@
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

@@ -38,7 +38,7 @@ import {
ReturnReceiptValuesDTO,
} from '@generated/swagger/oms-api';
import { firstValueFrom } from 'rxjs';
import { ReturnPrintReceiptsService } from './return-print-receipts.service';
import { PrintReceiptsService } from './print-receipts.service';
import { z } from 'zod';
/**
@@ -51,7 +51,7 @@ export class ReturnProcessService {
#logger = logger();
#receiptService = inject(ReceiptService);
#printReceiptsService = inject(ReturnPrintReceiptsService);
#printReceiptsService = inject(PrintReceiptsService);
/**
* Gets active questions in the return process based on previously provided answers.

View File

@@ -13,5 +13,6 @@ import { NgIconComponent, provideIcons } from '@ng-icons/core';
providers: [provideIcons({ isaActionPrinter })],
})
export class ReturnReviewHeaderComponent {
// TODO: Kann direkt in der Komponente gehandled werden
printReceipt = output<void>();
}

View File

@@ -1,8 +1,5 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {
ReturnPrintReceiptsService,
ReturnProcessStore,
} from '@isa/oms/data-access';
import { PrintReceiptsService, ReturnProcessStore } from '@isa/oms/data-access';
import { injectActivatedProcessId } from '@isa/core/process';
import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';
import { ReturnReviewHeaderComponent } from './return-review-header/return-review-header.component';
@@ -16,21 +13,23 @@ import { ReturnReviewHeaderComponent } from './return-review-header/return-revie
imports: [ReturnTaskListComponent, ReturnReviewHeaderComponent],
})
export class ReturnReviewComponent {
#printReceiptsService = inject(ReturnPrintReceiptsService);
#printReceiptsService = inject(PrintReceiptsService);
#returnProcessStore = inject(ReturnProcessStore);
processId = injectActivatedProcessId();
async printReceipt() {
const processId = this.processId();
if (processId) {
const receiptId =
this.#returnProcessStore.entityMap()[processId].receiptId;
const receiptIds = this.#returnProcessStore
.entities()
.filter((p) => p.processId === processId && p.returnReceipt?.id)
.map((p) => p.returnReceipt!.id);
console.log('receiptIds', this.#returnProcessStore.entities());
if (receiptId) {
await this.#printReceiptsService.printReturnReceipts({
returnReceiptIds: [receiptId],
returnReceiptIds: receiptIds,
});
}
}
}
}

View File

@@ -5,6 +5,8 @@ export * from './lib/checkbox/checklist.component';
export * from './lib/core/input-control.directive';
export * from './lib/dropdown/dropdown.component';
export * from './lib/dropdown/dropdown.types';
export * from './lib/listbox/listbox-item.directive';
export * from './lib/listbox/listbox.directive';
export * from './lib/text-field/text-field.component';
export * from './lib/text-field/textarea.component';
export * from './lib/chips/chips.component';

View File

@@ -2,5 +2,6 @@
@use "./lib/checkbox/checklist";
@use "./lib/chips/chips";
@use "./lib/dropdown/dropdown";
@use "./lib/listbox/listbox";
@use "./lib/text-field/text-field";
@use "./lib/text-field/textarea";

View File

@@ -1,8 +1,8 @@
.ui-list {
.ui-listbox {
@apply flex flex-col items-stretch w-full;
}
.ui-text-list-item {
.ui-listbox-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;

View File

@@ -0,0 +1,39 @@
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { ListboxItemDirective } from './listbox-item.directive';
import { CdkOption } from '@angular/cdk/listbox';
import { Component } from '@angular/core';
import { ListboxDirective } from './listbox.directive';
@Component({
selector: 'ui-test-host',
template: `
<div uiListbox>
<button uiListboxItem>Item</button>
</div>
`,
imports: [ListboxDirective, ListboxItemDirective],
})
class TestHostComponent {}
describe('ListboxItemDirective', () => {
let spectator: SpectatorHost<TestHostComponent>;
const createHost = createHostFactory(TestHostComponent);
beforeEach(() => {
spectator = createHost('<ui-test-host></ui-test-host>');
});
it('should create an instance', () => {
expect(spectator.query(ListboxItemDirective)).toBeTruthy();
});
it('should have the ui-listbox-item class', () => {
expect(spectator.query('[uiListboxItem]')).toHaveClass('ui-listbox-item');
});
it('should have CdkOption applied', () => {
const cdkOptionInstance = spectator.query(CdkOption);
expect(cdkOptionInstance).toBeTruthy();
});
});

View File

@@ -13,9 +13,9 @@ import { CdkOption } from '@angular/cdk/listbox';
* ```
*/
@Directive({
selector: '[uiTextListItem]',
selector: '[uiListboxItem]',
host: {
class: 'ui-text-list-item',
class: 'ui-listbox-item',
},
hostDirectives: [
{
@@ -24,4 +24,4 @@ import { CdkOption } from '@angular/cdk/listbox';
},
],
})
export class TextListItemDirective {}
export class ListboxItemDirective {}

View File

@@ -0,0 +1,32 @@
import {
createDirectiveFactory,
SpectatorDirective,
} from '@ngneat/spectator/jest';
import { ListboxDirective } from './listbox.directive';
import { CdkListbox } from '@angular/cdk/listbox';
describe('ListboxDirective', () => {
let spectator: SpectatorDirective<ListboxDirective>;
const createDirective = createDirectiveFactory({
directive: ListboxDirective,
});
beforeEach(() => {
spectator = createDirective('<div uiListbox></div>');
});
it('should create an instance', () => {
expect(spectator.directive).toBeTruthy();
});
it('should have the ui-listbox class', () => {
expect(spectator.element).toHaveClass('ui-listbox');
});
it('should have CdkListbox applied', () => {
const cdkListboxInstance = spectator.query(CdkListbox);
expect(cdkListboxInstance).toBeTruthy();
});
});

View File

@@ -15,9 +15,9 @@ import { CdkListbox } from '@angular/cdk/listbox';
* ```
*/
@Directive({
selector: '[uiList]',
selector: '[uiListbox]',
host: {
class: 'ui-list',
class: 'ui-listbox',
},
hostDirectives: [
{
@@ -31,4 +31,4 @@ import { CdkListbox } from '@angular/cdk/listbox';
},
],
})
export class ListDirective {}
export class ListboxDirective {}

View File

@@ -1,7 +0,0 @@
# 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

@@ -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: '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

@@ -1,21 +0,0 @@
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',
],
};

View File

@@ -1,20 +0,0 @@
{
"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

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

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
{
"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

@@ -1,17 +0,0 @@
{
"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

@@ -1,16 +0,0 @@
{
"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

@@ -79,7 +79,6 @@
"@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"],