mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
26 Commits
nx-build-e
...
4.0-hotfix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4241cbd7a | ||
|
|
ac728f2dd9 | ||
|
|
c2f393d249 | ||
|
|
0addf392b6 | ||
|
|
afe6c6abcc | ||
|
|
6f9d4d9218 | ||
|
|
e674378080 | ||
|
|
7c907645dc | ||
|
|
6fee35c756 | ||
|
|
c15077aa86 | ||
|
|
f051a97e53 | ||
|
|
1b26a44a37 | ||
|
|
80b2508708 | ||
|
|
e9affd2359 | ||
|
|
8f8b9153b0 | ||
|
|
9a4121e2bf | ||
|
|
50b7f21394 | ||
|
|
a67375557d | ||
|
|
6e7c56fcb9 | ||
|
|
05e257b922 | ||
|
|
d7d61915fa | ||
|
|
d0220b6246 | ||
|
|
32336ba5b4 | ||
|
|
1f26d5285b | ||
|
|
be0bff0535 | ||
|
|
cb7391e66f |
10
.github/instructions/nx.instructions.md
vendored
10
.github/instructions/nx.instructions.md
vendored
@@ -4,7 +4,7 @@ applyTo: '**'
|
||||
|
||||
// This file is automatically generated by Nx Console
|
||||
|
||||
You are in an nx workspace using Nx 20.4.6 and npm as the package manager.
|
||||
You are in an nx workspace using Nx 21.2.1 and npm as the package manager.
|
||||
|
||||
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
|
||||
|
||||
@@ -28,5 +28,13 @@ If the user wants to generate something, use the following flow:
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,18 +1,18 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Logger, LogLevel } from '@core/logger';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { debounceTime, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { RootState } from './root.state';
|
||||
import packageInfo from 'packageJson';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { Subject } from 'rxjs';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { injectStorage, UserStorageProvider } from '@isa/core/storage';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Logger, LogLevel } from "@core/logger";
|
||||
import { Store } from "@ngrx/store";
|
||||
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
||||
import { RootState } from "./root.state";
|
||||
import packageInfo from "packageJson";
|
||||
import { environment } from "../../environments/environment";
|
||||
import { Subject } from "rxjs";
|
||||
import { AuthService } from "@core/auth";
|
||||
import { injectStorage, UserStorageProvider } from "@isa/core/storage";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class RootStateService {
|
||||
static LOCAL_STORAGE_KEY = 'ISA_APP_INITIALSTATE';
|
||||
static LOCAL_STORAGE_KEY = "ISA_APP_INITIALSTATE";
|
||||
|
||||
#storage = injectStorage(UserStorageProvider);
|
||||
|
||||
@@ -29,14 +29,17 @@ export class RootStateService {
|
||||
);
|
||||
}
|
||||
|
||||
window['clearUserState'] = () => {
|
||||
window["clearUserState"] = () => {
|
||||
this.clear();
|
||||
};
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.load();
|
||||
this._store.dispatch({ type: 'HYDRATE', payload: RootStateService.LoadFromLocalStorage() });
|
||||
this._store.dispatch({
|
||||
type: "HYDRATE",
|
||||
payload: RootStateService.LoadFromLocalStorage(),
|
||||
});
|
||||
this.initSave();
|
||||
}
|
||||
|
||||
@@ -50,14 +53,10 @@ export class RootStateService {
|
||||
const data = {
|
||||
...state,
|
||||
version: packageInfo.version,
|
||||
sub: this._authService.getClaimByKey('sub'),
|
||||
sub: this._authService.getClaimByKey("sub"),
|
||||
};
|
||||
RootStateService.SaveToLocalStorageRaw(JSON.stringify(data));
|
||||
return this.#storage.set('state', {
|
||||
...state,
|
||||
version: packageInfo.version,
|
||||
sub: this._authService.getClaimByKey('sub'),
|
||||
});
|
||||
return this.#storage.set("state", data);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
@@ -68,7 +67,7 @@ export class RootStateService {
|
||||
*/
|
||||
async load(): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.#storage.get('state');
|
||||
const res = await this.#storage.get("state");
|
||||
|
||||
const storageContent = RootStateService.LoadFromLocalStorageRaw();
|
||||
|
||||
@@ -88,7 +87,7 @@ export class RootStateService {
|
||||
async clear() {
|
||||
try {
|
||||
this._cancelSave.next();
|
||||
await this.#storage.clear('state');
|
||||
await this.#storage.clear("state");
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
RootStateService.RemoveFromLocalStorage();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
@@ -112,7 +111,7 @@ export class RootStateService {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
console.error('Error parsing local storage:', error);
|
||||
console.error("Error parsing local storage:", error);
|
||||
this.RemoveFromLocalStorage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
|
||||
@@ -16,20 +16,20 @@ import {
|
||||
forwardRef,
|
||||
Optional,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { UiAutocompleteComponent } from '@ui/autocomplete';
|
||||
import { UiFormControlDirective } from '@ui/form-control';
|
||||
import { containsElement } from '@utils/common';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ScanAdapterService } from '@adapter/scan';
|
||||
import { injectCancelSearch } from '@shared/services/cancel-subject';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
} from "@angular/core";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
import { UiAutocompleteComponent } from "@ui/autocomplete";
|
||||
import { UiFormControlDirective } from "@ui/form-control";
|
||||
import { containsElement } from "@utils/common";
|
||||
import { Subscription } from "rxjs";
|
||||
import { ScanAdapterService } from "@adapter/scan";
|
||||
import { injectCancelSearch } from "@shared/services/cancel-subject";
|
||||
import { EnvironmentService } from "@core/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'shared-searchbox',
|
||||
templateUrl: 'searchbox.component.html',
|
||||
styleUrls: ['searchbox.component.scss'],
|
||||
selector: "shared-searchbox",
|
||||
templateUrl: "searchbox.component.html",
|
||||
styleUrls: ["searchbox.component.scss"],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
@@ -49,9 +49,9 @@ export class SearchboxComponent
|
||||
cancelSearch = injectCancelSearch({ optional: true });
|
||||
|
||||
disabled: boolean;
|
||||
type = 'text';
|
||||
type = "text";
|
||||
|
||||
@ViewChild('input', { read: ElementRef, static: true })
|
||||
@ViewChild("input", { read: ElementRef, static: true })
|
||||
input: ElementRef;
|
||||
|
||||
@ContentChild(UiAutocompleteComponent)
|
||||
@@ -61,9 +61,9 @@ export class SearchboxComponent
|
||||
focusAfterViewInit = true;
|
||||
|
||||
@Input()
|
||||
placeholder = '';
|
||||
placeholder = "";
|
||||
|
||||
private _query = '';
|
||||
private _query = "";
|
||||
|
||||
@Input()
|
||||
get query() {
|
||||
@@ -94,7 +94,7 @@ export class SearchboxComponent
|
||||
scanner = false;
|
||||
|
||||
@Input()
|
||||
hint = '';
|
||||
hint = "";
|
||||
|
||||
@Input()
|
||||
autocompleteValueSelector: (item: any) => string = (item: any) => item;
|
||||
@@ -104,11 +104,11 @@ export class SearchboxComponent
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.setQuery('');
|
||||
this.setQuery("");
|
||||
this.cancelSearch();
|
||||
}
|
||||
|
||||
@HostBinding('class.autocomplete-opend')
|
||||
@HostBinding("class.autocomplete-opend")
|
||||
get autocompleteOpen() {
|
||||
return this.autocomplete?.opend;
|
||||
}
|
||||
@@ -213,13 +213,13 @@ export class SearchboxComponent
|
||||
}
|
||||
|
||||
clearHint() {
|
||||
this.hint = '';
|
||||
this.hint = "";
|
||||
this.focused.emit(true);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
onKeyup(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
if (this.autocomplete?.opend && this.autocomplete?.activeItem) {
|
||||
this.setQuery(this.autocomplete?.activeItem?.item);
|
||||
this.autocomplete?.close();
|
||||
@@ -227,7 +227,7 @@ export class SearchboxComponent
|
||||
this.search.emit(this.query);
|
||||
|
||||
event.preventDefault();
|
||||
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
this.handleArrowUpDownEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -242,7 +242,7 @@ export class SearchboxComponent
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
@HostListener("window:click", ["$event"])
|
||||
focusLost(event: MouseEvent) {
|
||||
if (
|
||||
this.autocomplete?.opend &&
|
||||
@@ -256,9 +256,11 @@ export class SearchboxComponent
|
||||
this.search.emit(this.query);
|
||||
}
|
||||
|
||||
@HostListener('focusout', ['$event'])
|
||||
@HostListener("focusout", ["$event"])
|
||||
onBlur() {
|
||||
this.onTouched();
|
||||
if (typeof this.onTouched === "function") {
|
||||
this.onTouched();
|
||||
}
|
||||
this.focused.emit(false);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
@@ -16,20 +16,20 @@ import {
|
||||
forwardRef,
|
||||
Optional,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { UiAutocompleteComponent } from '@ui/autocomplete';
|
||||
import { UiFormControlDirective } from '@ui/form-control';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ScanAdapterService } from '@adapter/scan';
|
||||
import { injectCancelSearch } from '@shared/services/cancel-subject';
|
||||
import { containsElement } from '@utils/common';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
} from "@angular/core";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
import { UiAutocompleteComponent } from "@ui/autocomplete";
|
||||
import { UiFormControlDirective } from "@ui/form-control";
|
||||
import { Subscription } from "rxjs";
|
||||
import { ScanAdapterService } from "@adapter/scan";
|
||||
import { injectCancelSearch } from "@shared/services/cancel-subject";
|
||||
import { containsElement } from "@utils/common";
|
||||
import { EnvironmentService } from "@core/environment";
|
||||
|
||||
@Component({
|
||||
selector: 'ui-searchbox',
|
||||
templateUrl: 'searchbox.component.html',
|
||||
styleUrls: ['searchbox.component.scss'],
|
||||
selector: "ui-searchbox",
|
||||
templateUrl: "searchbox.component.html",
|
||||
styleUrls: ["searchbox.component.scss"],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
@@ -49,9 +49,9 @@ export class UiSearchboxNextComponent
|
||||
private readonly _cancelSearch = injectCancelSearch({ optional: true });
|
||||
|
||||
disabled: boolean;
|
||||
type = 'text';
|
||||
type = "text";
|
||||
|
||||
@ViewChild('input', { read: ElementRef, static: true })
|
||||
@ViewChild("input", { read: ElementRef, static: true })
|
||||
input: ElementRef;
|
||||
|
||||
@ContentChild(UiAutocompleteComponent)
|
||||
@@ -61,9 +61,9 @@ export class UiSearchboxNextComponent
|
||||
focusAfterViewInit: boolean = true;
|
||||
|
||||
@Input()
|
||||
placeholder: string = '';
|
||||
placeholder: string = "";
|
||||
|
||||
private _query = '';
|
||||
private _query = "";
|
||||
|
||||
@Input()
|
||||
get query() {
|
||||
@@ -94,7 +94,7 @@ export class UiSearchboxNextComponent
|
||||
scanner = false;
|
||||
|
||||
@Input()
|
||||
hint: string = '';
|
||||
hint: string = "";
|
||||
|
||||
@Output()
|
||||
hintCleared = new EventEmitter<void>();
|
||||
@@ -107,11 +107,11 @@ export class UiSearchboxNextComponent
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.setQuery('');
|
||||
this.setQuery("");
|
||||
this._cancelSearch();
|
||||
}
|
||||
|
||||
@HostBinding('class.autocomplete-opend')
|
||||
@HostBinding("class.autocomplete-opend")
|
||||
get autocompleteOpen() {
|
||||
return this.autocomplete?.opend;
|
||||
}
|
||||
@@ -212,14 +212,14 @@ export class UiSearchboxNextComponent
|
||||
}
|
||||
|
||||
clearHint() {
|
||||
this.hint = '';
|
||||
this.hint = "";
|
||||
this.focused.emit(true);
|
||||
this.hintCleared.emit();
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
onKeyup(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === "Enter") {
|
||||
if (this.autocomplete?.opend && this.autocomplete?.activeItem) {
|
||||
this.setQuery(this.autocomplete?.activeItem?.item);
|
||||
this.autocomplete?.close();
|
||||
@@ -227,7 +227,7 @@ export class UiSearchboxNextComponent
|
||||
this.search.emit(this.query);
|
||||
|
||||
event.preventDefault();
|
||||
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||
this.handleArrowUpDownEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -235,12 +235,14 @@ export class UiSearchboxNextComponent
|
||||
handleArrowUpDownEvent(event: KeyboardEvent) {
|
||||
this.autocomplete?.handleKeyboardEvent(event);
|
||||
if (this.autocomplete?.activeItem) {
|
||||
const query = this.autocompleteValueSelector(this.autocomplete.activeItem.item);
|
||||
const query = this.autocompleteValueSelector(
|
||||
this.autocomplete.activeItem.item,
|
||||
);
|
||||
this.setQuery(query, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
@HostListener("window:click", ["$event"])
|
||||
focusLost(event: MouseEvent) {
|
||||
if (
|
||||
this.autocomplete?.opend &&
|
||||
@@ -254,9 +256,11 @@ export class UiSearchboxNextComponent
|
||||
this.search.emit(this.query);
|
||||
}
|
||||
|
||||
@HostListener('focusout', ['$event'])
|
||||
@HostListener("focusout", ["$event"])
|
||||
onBlur() {
|
||||
this.onTouched();
|
||||
if (typeof this.onTouched === "function") {
|
||||
this.onTouched();
|
||||
}
|
||||
this.focused.emit(false);
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';
|
||||
import { ClientRowComponent, ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
|
||||
import { type Meta, type StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
import {
|
||||
ClientRowComponent,
|
||||
ClientRowImports,
|
||||
ItemRowDataImports,
|
||||
} from "@isa/ui/item-rows";
|
||||
|
||||
const meta: Meta<ClientRowComponent> = {
|
||||
component: ClientRowComponent,
|
||||
title: 'ui/item-rows/ClientRow',
|
||||
title: "ui/item-rows/ClientRow",
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [ClientRowImports, ItemRowDataImports],
|
||||
@@ -21,25 +25,25 @@ const meta: Meta<ClientRowComponent> = {
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label>Belegdatum</ui-item-row-data-label>
|
||||
<ui-item-row-data-value>
|
||||
<span class="isa-text-body-2-bold">
|
||||
<span class="isa-text-body-2-bold">
|
||||
01.11.2024
|
||||
</span>
|
||||
</ui-item-row-data-value>
|
||||
</ui-item-row-data-row>
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label>Rechnugsnr.</ui-item-row-data-label>
|
||||
<ui-item-row-data-label>Beleg-Nr.</ui-item-row-data-label>
|
||||
<ui-item-row-data-value>
|
||||
<span class="isa-text-body-2-bold">
|
||||
<span class="isa-text-body-2-bold">
|
||||
1234567890
|
||||
</span>
|
||||
</ui-item-row-data-value>
|
||||
</ui-item-row-data-row>
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label >Vorgangs-ID</ui-item-row-data-label>
|
||||
<ui-item-row-data-value >
|
||||
<span class="isa-text-body-2-bold">
|
||||
640175214390060/0
|
||||
</span>
|
||||
<ui-item-row-data-value >
|
||||
<span class="isa-text-body-2-bold">
|
||||
640175214390060/0
|
||||
</span>
|
||||
</ui-item-row-data-value>
|
||||
</ui-item-row-data-row>
|
||||
</ui-item-row-data>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './item';
|
||||
export * from './product';
|
||||
export * from "./item";
|
||||
export * from "./product";
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { StorageProvider } from './storage-provider';
|
||||
import { UserStateService } from '@generated/swagger/isa-api';
|
||||
import { firstValueFrom, map, shareReplay } from 'rxjs';
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { StorageProvider } from "./storage-provider";
|
||||
import { UserStateService } from "@generated/swagger/isa-api";
|
||||
import { catchError, firstValueFrom, map, of } from "rxjs";
|
||||
import { isEmpty } from "lodash";
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class UserStorageProvider implements StorageProvider {
|
||||
#userStateService = inject(UserStateService);
|
||||
|
||||
private state$ = this.#userStateService.UserStateGetUserState().pipe(
|
||||
map((res) => {
|
||||
if (res.result?.content) {
|
||||
if (res?.result?.content) {
|
||||
return JSON.parse(res.result.content);
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
shareReplay(1),
|
||||
catchError((err) => {
|
||||
console.warn(
|
||||
"No UserStateGetUserState found, returning empty object:",
|
||||
err,
|
||||
);
|
||||
return of({}); // Return empty state fallback
|
||||
}),
|
||||
// shareReplay(1), #5249, #5270 Würde beim Fehlerfall den fehlerhaften Zustand behalten
|
||||
// Aktuell wird nun jedes mal 2 mal der UserState aufgerufen (GET + POST)
|
||||
// Damit bei der set Funktion immer der aktuelle Zustand verwendet wird
|
||||
);
|
||||
|
||||
async set(key: string, value: unknown): Promise<void> {
|
||||
async set(key: string, value: Record<string, unknown>): Promise<void> {
|
||||
const current = await firstValueFrom(this.state$);
|
||||
firstValueFrom(
|
||||
const content =
|
||||
current && !isEmpty(current)
|
||||
? { ...current, [key]: value }
|
||||
: { [key]: value };
|
||||
|
||||
await firstValueFrom(
|
||||
this.#userStateService.UserStateSetUserState({
|
||||
content: JSON.stringify({ ...current, [key]: value }),
|
||||
content: JSON.stringify(content),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -32,7 +47,6 @@ export class UserStorageProvider implements StorageProvider {
|
||||
}
|
||||
|
||||
async clear(key: string): Promise<void> {
|
||||
|
||||
const current = await firstValueFrom(this.state$);
|
||||
delete current[key];
|
||||
firstValueFrom(this.#userStateService.UserStateResetUserState());
|
||||
|
||||
@@ -1,27 +1,51 @@
|
||||
import { DataAccessError } from '@isa/common/data-access';
|
||||
import { Receipt, ReceiptItem } from '../../models';
|
||||
import { DataAccessError } from "@isa/common/data-access";
|
||||
import { Receipt, ReceiptItem } from "../../models";
|
||||
import {
|
||||
CreateReturnProcessError,
|
||||
CreateReturnProcessErrorReason,
|
||||
CreateReturnProcessErrorMessages,
|
||||
} from './create-return-process.error';
|
||||
} from "./create-return-process.error";
|
||||
import { ProductCategory } from "../../questions";
|
||||
|
||||
describe('CreateReturnProcessError', () => {
|
||||
describe("CreateReturnProcessError", () => {
|
||||
const params = {
|
||||
processId: 123,
|
||||
returns: [
|
||||
{
|
||||
receipt: { id: 321 } as Receipt,
|
||||
items: [] as ReceiptItem[],
|
||||
items: [
|
||||
// Provide at least one valid item object, or an empty array if testing "no items"
|
||||
// For NO_RETURNABLE_ITEMS, an empty array is valid, but must match the expected shape
|
||||
// So, keep as [], but type is now correct
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('should create an error instance with NO_RETURNABLE_ITEMS reason', () => {
|
||||
// For tests that require items, use the correct shape:
|
||||
const validParams = {
|
||||
processId: 123,
|
||||
returns: [
|
||||
{
|
||||
receipt: { id: 321 } as Receipt,
|
||||
items: [
|
||||
{
|
||||
receiptItem: { id: 111 } as ReceiptItem,
|
||||
quantity: 1,
|
||||
category: "A" as ProductCategory,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it("should create an error instance with NO_RETURNABLE_ITEMS reason", () => {
|
||||
// Arrange, Act
|
||||
const error = new CreateReturnProcessError(
|
||||
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS,
|
||||
params,
|
||||
);
|
||||
// Assert
|
||||
expect(error).toBeInstanceOf(CreateReturnProcessError);
|
||||
expect(error).toBeInstanceOf(DataAccessError);
|
||||
expect(error.reason).toBe(
|
||||
@@ -33,25 +57,103 @@ describe('CreateReturnProcessError', () => {
|
||||
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS
|
||||
],
|
||||
);
|
||||
expect(error.code).toBe('CREATE_RETURN_PROCESS');
|
||||
expect(error.code).toBe("CREATE_RETURN_PROCESS");
|
||||
});
|
||||
|
||||
it('should create an error instance with MISMATCH_RETURNABLE_ITEMS reason', () => {
|
||||
it("should create an error instance with MISMATCH_RETURNABLE_ITEMS reason", () => {
|
||||
// Arrange, Act
|
||||
const error = new CreateReturnProcessError(
|
||||
CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS,
|
||||
params,
|
||||
validParams,
|
||||
);
|
||||
// Assert
|
||||
expect(error).toBeInstanceOf(CreateReturnProcessError);
|
||||
expect(error).toBeInstanceOf(DataAccessError);
|
||||
expect(error.reason).toBe(
|
||||
CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS,
|
||||
);
|
||||
expect(error.params).toEqual(params);
|
||||
expect(error.params).toEqual(validParams);
|
||||
expect(error.message).toBe(
|
||||
CreateReturnProcessErrorMessages[
|
||||
CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS
|
||||
],
|
||||
);
|
||||
expect(error.code).toBe('CREATE_RETURN_PROCESS');
|
||||
expect(error.code).toBe("CREATE_RETURN_PROCESS");
|
||||
});
|
||||
|
||||
it("should expose the correct params structure", () => {
|
||||
const error = new CreateReturnProcessError(
|
||||
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS,
|
||||
params,
|
||||
);
|
||||
expect(error.params).toHaveProperty("processId", 123);
|
||||
expect(error.params).toHaveProperty("returns");
|
||||
expect(Array.isArray(error.params.returns)).toBe(true);
|
||||
expect(error.params.returns[0]).toHaveProperty("receipt");
|
||||
expect(error.params.returns[0]).toHaveProperty("items");
|
||||
});
|
||||
|
||||
it("should throw and be catchable as CreateReturnProcessError", () => {
|
||||
try {
|
||||
throw new CreateReturnProcessError(
|
||||
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS,
|
||||
params,
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(CreateReturnProcessError);
|
||||
expect((err as CreateReturnProcessError).reason).toBe(
|
||||
CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("should use the correct message for each reason", () => {
|
||||
Object.values(CreateReturnProcessErrorReason).forEach((reason) => {
|
||||
const error = new CreateReturnProcessError(reason, params);
|
||||
expect(error.message).toBe(CreateReturnProcessErrorMessages[reason]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have code "CREATE_RETURN_PROCESS" for all reasons', () => {
|
||||
Object.values(CreateReturnProcessErrorReason).forEach((reason) => {
|
||||
const error = new CreateReturnProcessError(reason, params);
|
||||
expect(error.code).toBe("CREATE_RETURN_PROCESS");
|
||||
});
|
||||
});
|
||||
|
||||
it("should support params with multiple returns and items", () => {
|
||||
const extendedParams = {
|
||||
processId: 999,
|
||||
returns: [
|
||||
{
|
||||
receipt: { id: 1 } as Receipt,
|
||||
items: [
|
||||
{
|
||||
receiptItem: { id: 10 } as ReceiptItem,
|
||||
quantity: 2,
|
||||
category: "A" as ProductCategory,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
receipt: { id: 2 } as Receipt,
|
||||
items: [
|
||||
{
|
||||
receiptItem: { id: 20 } as ReceiptItem,
|
||||
quantity: 1,
|
||||
category: "B" as ProductCategory,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const error = new CreateReturnProcessError(
|
||||
CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS,
|
||||
extendedParams,
|
||||
);
|
||||
expect(error.params.processId).toBe(999);
|
||||
expect(error.params.returns.length).toBe(2);
|
||||
expect(error.params.returns[0].items[0].quantity).toBe(2);
|
||||
expect(error.params.returns[1].items[0].category).toBe("B");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { DataAccessError } from '@isa/common/data-access';
|
||||
import { Receipt, ReceiptItem } from '../../models';
|
||||
import { DataAccessError } from "@isa/common/data-access";
|
||||
import { Receipt, ReceiptItem } from "../../models";
|
||||
import { ProductCategory } from "../../questions";
|
||||
|
||||
/**
|
||||
* Enum-like object defining possible reasons for return process creation failures.
|
||||
* Used to provide consistent and type-safe error categorization.
|
||||
*/
|
||||
export const CreateReturnProcessErrorReason = {
|
||||
NO_RETURNABLE_ITEMS: 'NO_RETURNABLE_ITEMS',
|
||||
MISMATCH_RETURNABLE_ITEMS: 'MISMATCH_RETURNABLE_ITEMS',
|
||||
NO_RETURNABLE_ITEMS: "NO_RETURNABLE_ITEMS",
|
||||
MISMATCH_RETURNABLE_ITEMS: "MISMATCH_RETURNABLE_ITEMS",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -32,9 +33,9 @@ export const CreateReturnProcessErrorMessages: Record<
|
||||
string
|
||||
> = {
|
||||
[CreateReturnProcessErrorReason.NO_RETURNABLE_ITEMS]:
|
||||
'No returnable items found.',
|
||||
"No returnable items found.",
|
||||
[CreateReturnProcessErrorReason.MISMATCH_RETURNABLE_ITEMS]:
|
||||
'Mismatch in the number of returnable items.',
|
||||
"Mismatch in the number of returnable items.",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,14 +74,21 @@ export const CreateReturnProcessErrorMessages: Record<
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class CreateReturnProcessError extends DataAccessError<'CREATE_RETURN_PROCESS'> {
|
||||
export class CreateReturnProcessError extends DataAccessError<"CREATE_RETURN_PROCESS"> {
|
||||
constructor(
|
||||
public readonly reason: CreateReturnProcessErrorReason,
|
||||
public readonly params: {
|
||||
processId: number;
|
||||
returns: { receipt: Receipt; items: ReceiptItem[] }[];
|
||||
returns: {
|
||||
receipt: Receipt;
|
||||
items: {
|
||||
receiptItem: ReceiptItem;
|
||||
quantity: number;
|
||||
category: ProductCategory;
|
||||
}[];
|
||||
}[];
|
||||
},
|
||||
) {
|
||||
super('CREATE_RETURN_PROCESS', CreateReturnProcessErrorMessages[reason]);
|
||||
super("CREATE_RETURN_PROCESS", CreateReturnProcessErrorMessages[reason]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './create-return-process.error';
|
||||
export * from './return-process-is-not-complete.error';
|
||||
export * from "./create-return-process.error";
|
||||
export * from "./return-process-is-not-complete.error";
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { returnReceiptValuesMapping } from './return-receipt-values-mapping.helper';
|
||||
import { PropertyNullOrUndefinedError } from '@isa/common/data-access';
|
||||
import { getReturnProcessQuestions } from './get-return-process-questions.helper';
|
||||
import { getReturnInfo } from './get-return-info.helper';
|
||||
import { serializeReturnDetails } from './return-details-mapping.helper';
|
||||
import { returnReceiptValuesMapping } from "./return-receipt-values-mapping.helper";
|
||||
import { PropertyNullOrUndefinedError } from "@isa/common/data-access";
|
||||
import { getReturnProcessQuestions } from "./get-return-process-questions.helper";
|
||||
import { getReturnInfo } from "./get-return-info.helper";
|
||||
import { serializeReturnDetails } from "./return-details-mapping.helper";
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('./get-return-process-questions.helper', () => ({
|
||||
jest.mock("./get-return-process-questions.helper", () => ({
|
||||
getReturnProcessQuestions: jest.fn(),
|
||||
}));
|
||||
jest.mock('./get-return-info.helper', () => ({
|
||||
jest.mock("./get-return-info.helper", () => ({
|
||||
getReturnInfo: jest.fn(),
|
||||
}));
|
||||
jest.mock('./return-details-mapping.helper', () => ({
|
||||
jest.mock("./return-details-mapping.helper", () => ({
|
||||
serializeReturnDetails: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('returnReceiptValuesMapping', () => {
|
||||
describe("returnReceiptValuesMapping", () => {
|
||||
const processMock: any = {
|
||||
receiptItem: {
|
||||
id: 'item-1',
|
||||
quantity: { quantity: 2 },
|
||||
features: { category: 'shoes' },
|
||||
id: "item-1",
|
||||
},
|
||||
answers: { foo: 'bar' },
|
||||
quantity: 2, // <-- Add this
|
||||
productCategory: "shoes", // <-- Add this
|
||||
answers: { foo: "bar" },
|
||||
};
|
||||
|
||||
const questionsMock = [{ id: 'q1' }];
|
||||
const questionsMock = [{ id: "q1" }];
|
||||
const returnInfoMock = {
|
||||
comment: 'Test comment',
|
||||
itemCondition: 'NEW',
|
||||
otherProduct: 'Other',
|
||||
returnDetails: { detail: 'details' },
|
||||
returnReason: 'Damaged',
|
||||
comment: "Test comment",
|
||||
itemCondition: "NEW",
|
||||
otherProduct: "Other",
|
||||
returnDetails: { detail: "details" },
|
||||
returnReason: "Damaged",
|
||||
};
|
||||
const serializedDetails = { detail: 'serialized' };
|
||||
const serializedDetails = { detail: "serialized" };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -42,32 +42,24 @@ describe('returnReceiptValuesMapping', () => {
|
||||
(serializeReturnDetails as jest.Mock).mockReturnValue(serializedDetails);
|
||||
});
|
||||
|
||||
it('should map values correctly when all dependencies return valid data', () => {
|
||||
it("should map values correctly when all dependencies return valid data", () => {
|
||||
// Act
|
||||
const result = returnReceiptValuesMapping(processMock);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
quantity: 2,
|
||||
comment: 'Test comment',
|
||||
itemCondition: 'NEW',
|
||||
otherProduct: 'Other',
|
||||
comment: "Test comment",
|
||||
itemCondition: "NEW",
|
||||
otherProduct: "Other",
|
||||
returnDetails: serializedDetails,
|
||||
returnReason: 'Damaged',
|
||||
category: 'shoes',
|
||||
receiptItem: { id: 'item-1' },
|
||||
returnReason: "Damaged",
|
||||
category: "shoes",
|
||||
receiptItem: { id: "item-1" },
|
||||
});
|
||||
expect(getReturnProcessQuestions).toHaveBeenCalledWith(processMock);
|
||||
expect(getReturnInfo).toHaveBeenCalledWith({
|
||||
questions: questionsMock,
|
||||
answers: processMock.answers,
|
||||
});
|
||||
expect(serializeReturnDetails).toHaveBeenCalledWith(
|
||||
returnInfoMock.returnDetails,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw PropertyNullOrUndefinedError if questions is undefined', () => {
|
||||
it("should throw PropertyNullOrUndefinedError if questions is undefined", () => {
|
||||
// Arrange
|
||||
(getReturnProcessQuestions as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
@@ -75,10 +67,10 @@ describe('returnReceiptValuesMapping', () => {
|
||||
expect(() => returnReceiptValuesMapping(processMock)).toThrow(
|
||||
PropertyNullOrUndefinedError,
|
||||
);
|
||||
expect(() => returnReceiptValuesMapping(processMock)).toThrow('questions');
|
||||
expect(() => returnReceiptValuesMapping(processMock)).toThrow("questions");
|
||||
});
|
||||
|
||||
it('should throw PropertyNullOrUndefinedError if returnInfo is undefined', () => {
|
||||
it("should throw PropertyNullOrUndefinedError if returnInfo is undefined", () => {
|
||||
// Arrange
|
||||
(getReturnInfo as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
@@ -86,28 +78,55 @@ describe('returnReceiptValuesMapping', () => {
|
||||
expect(() => returnReceiptValuesMapping(processMock)).toThrow(
|
||||
PropertyNullOrUndefinedError,
|
||||
);
|
||||
expect(() => returnReceiptValuesMapping(processMock)).toThrow('returnInfo');
|
||||
expect(() => returnReceiptValuesMapping(processMock)).toThrow("returnInfo");
|
||||
});
|
||||
|
||||
it('should handle missing category gracefully', () => {
|
||||
// Arrange
|
||||
const processNoCategory = {
|
||||
...processMock,
|
||||
receiptItem: { ...processMock.receiptItem, features: {} },
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = returnReceiptValuesMapping(processNoCategory);
|
||||
|
||||
// Assert
|
||||
expect(result?.category).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing receiptItem gracefully (may throw)', () => {
|
||||
it("should handle missing receiptItem gracefully (may throw)", () => {
|
||||
// Arrange
|
||||
const processNoReceiptItem = { ...processMock, receiptItem: undefined };
|
||||
|
||||
// Act & Assert
|
||||
expect(() => returnReceiptValuesMapping(processNoReceiptItem)).toThrow();
|
||||
});
|
||||
|
||||
// Additional tests for edge cases and error scenarios
|
||||
|
||||
it("should return correct quantity when process.quantity is 0", () => {
|
||||
const processZeroQuantity = { ...processMock, quantity: 0 };
|
||||
const result = returnReceiptValuesMapping(processZeroQuantity);
|
||||
expect(result?.quantity).toBe(0);
|
||||
});
|
||||
|
||||
it("should propagate the correct receiptItem id", () => {
|
||||
const result = returnReceiptValuesMapping(processMock);
|
||||
expect(result?.receiptItem).toEqual({ id: "item-1" });
|
||||
});
|
||||
|
||||
it("should throw if process is null", () => {
|
||||
expect(() => returnReceiptValuesMapping(null as any)).toThrow();
|
||||
});
|
||||
|
||||
it("should throw if process is undefined", () => {
|
||||
expect(() => returnReceiptValuesMapping(undefined as any)).toThrow();
|
||||
});
|
||||
|
||||
it("should call serializeReturnDetails with undefined if returnDetails is missing", () => {
|
||||
// Arrange
|
||||
const returnInfoNoDetails = { ...returnInfoMock, returnDetails: undefined };
|
||||
(getReturnInfo as jest.Mock).mockReturnValue(returnInfoNoDetails);
|
||||
|
||||
// Act
|
||||
returnReceiptValuesMapping(processMock);
|
||||
|
||||
// Assert
|
||||
expect(serializeReturnDetails).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it("should return undefined if process.quantity is undefined", () => {
|
||||
const processNoQuantity = { ...processMock };
|
||||
delete processNoQuantity.quantity;
|
||||
// Should not throw, but quantity will be undefined in result
|
||||
const result = returnReceiptValuesMapping(processNoQuantity);
|
||||
expect(result?.quantity).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ReturnProcess } from '../../models';
|
||||
import { ReturnReceiptValues } from '../../schemas';
|
||||
import { getReturnProcessQuestions } from './get-return-process-questions.helper';
|
||||
import { getReturnInfo } from './get-return-info.helper';
|
||||
import { PropertyNullOrUndefinedError } from '@isa/common/data-access';
|
||||
import { serializeReturnDetails } from './return-details-mapping.helper';
|
||||
import { ReturnProcess } from "../../models";
|
||||
import { ReturnReceiptValues } from "../../schemas";
|
||||
import { getReturnProcessQuestions } from "./get-return-process-questions.helper";
|
||||
import { getReturnInfo } from "./get-return-info.helper";
|
||||
import { PropertyNullOrUndefinedError } from "@isa/common/data-access";
|
||||
import { serializeReturnDetails } from "./return-details-mapping.helper";
|
||||
|
||||
export const returnReceiptValuesMapping = (
|
||||
process: ReturnProcess,
|
||||
): ReturnReceiptValues | undefined => {
|
||||
const questions = getReturnProcessQuestions(process);
|
||||
if (!questions) {
|
||||
throw new PropertyNullOrUndefinedError('questions');
|
||||
throw new PropertyNullOrUndefinedError("questions");
|
||||
}
|
||||
|
||||
const returnInfo = getReturnInfo({
|
||||
@@ -19,17 +19,17 @@ export const returnReceiptValuesMapping = (
|
||||
});
|
||||
|
||||
if (!returnInfo) {
|
||||
throw new PropertyNullOrUndefinedError('returnInfo');
|
||||
throw new PropertyNullOrUndefinedError("returnInfo");
|
||||
}
|
||||
|
||||
return {
|
||||
quantity: process.receiptItem.quantity.quantity,
|
||||
quantity: process.quantity,
|
||||
comment: returnInfo.comment,
|
||||
itemCondition: returnInfo.itemCondition,
|
||||
otherProduct: returnInfo.otherProduct,
|
||||
returnDetails: serializeReturnDetails(returnInfo.returnDetails),
|
||||
returnReason: returnInfo.returnReason,
|
||||
category: process?.receiptItem?.features?.['category'],
|
||||
category: process.productCategory,
|
||||
receiptItem: {
|
||||
id: process.receiptItem.id,
|
||||
},
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
export * from './address-type';
|
||||
export * from './buyer';
|
||||
export * from './can-return';
|
||||
export * from './eligible-for-return';
|
||||
export * from './gender';
|
||||
export * from './product';
|
||||
export * from './quantity';
|
||||
export * from './receipt-item-list-item';
|
||||
export * from './receipt-item-task-list-item';
|
||||
export * from './receipt-item';
|
||||
export * from './receipt-list-item';
|
||||
export * from './receipt-type';
|
||||
export * from './receipt';
|
||||
export * from './return-info';
|
||||
export * from './return-process-answer';
|
||||
export * from './return-process-question-key';
|
||||
export * from './return-process-question-type';
|
||||
export * from './return-process-question';
|
||||
export * from './return-process-status';
|
||||
export * from './return-process';
|
||||
export * from './shipping-address-2';
|
||||
export * from './shipping-type';
|
||||
export * from './task-action-type';
|
||||
export * from "./address-type";
|
||||
export * from "./buyer";
|
||||
export * from "./can-return";
|
||||
export * from "./eligible-for-return";
|
||||
export * from "./gender";
|
||||
export * from "./product";
|
||||
export * from "./quantity";
|
||||
export * from "./receipt-item-list-item";
|
||||
export * from "./receipt-item-task-list-item";
|
||||
export * from "./receipt-item";
|
||||
export * from "./receipt-list-item";
|
||||
export * from "./receipt-type";
|
||||
export * from "./receipt";
|
||||
export * from "./return-info";
|
||||
export * from "./return-process-answer";
|
||||
export * from "./return-process-question-key";
|
||||
export * from "./return-process-question-type";
|
||||
export * from "./return-process-question";
|
||||
export * from "./return-process-status";
|
||||
export * from "./return-process";
|
||||
export * from "./shipping-address-2";
|
||||
export * from "./shipping-type";
|
||||
export * from "./task-action-type";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Receipt } from './receipt';
|
||||
import { ReceiptItem } from './receipt-item';
|
||||
import { Receipt } from "./receipt";
|
||||
import { ReceiptItem } from "./receipt-item";
|
||||
|
||||
/**
|
||||
* Interface representing a return process within the OMS system.
|
||||
@@ -21,6 +21,7 @@ export interface ReturnProcess {
|
||||
receiptItem: ReceiptItem;
|
||||
receiptDate: string | undefined;
|
||||
answers: Record<string, unknown>;
|
||||
productCategory?: string;
|
||||
productCategory: string;
|
||||
quantity: number;
|
||||
returnReceipt?: Receipt;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import {
|
||||
FetchReturnDetails,
|
||||
FetchReturnDetailsSchema,
|
||||
ReturnReceiptValues,
|
||||
} from '../schemas';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { ReceiptService } from '@generated/swagger/oms-api';
|
||||
import { CanReturn, Receipt, ReceiptItem, ReceiptListItem } from '../models';
|
||||
import { CategoryQuestions, ProductCategory } from '../questions';
|
||||
import { KeyValue } from '@angular/common';
|
||||
import { ReturnCanReturnService } from './return-can-return.service';
|
||||
import { takeUntilAborted } from '@isa/common/data-access';
|
||||
import { z } from 'zod';
|
||||
} from "../schemas";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { ReceiptService } from "@generated/swagger/oms-api";
|
||||
import { CanReturn, Receipt, ReceiptItem, ReceiptListItem } from "../models";
|
||||
import { CategoryQuestions, ProductCategory } from "../questions";
|
||||
import { KeyValue } from "@angular/common";
|
||||
import { ReturnCanReturnService } from "./return-can-return.service";
|
||||
import { takeUntilAborted } from "@isa/common/data-access";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Service responsible for managing receipt return details and operations.
|
||||
@@ -22,7 +22,7 @@ import { z } from 'zod';
|
||||
* - Query receipts by customer email
|
||||
* - Get available product categories for returns
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class ReturnDetailsService {
|
||||
#receiptService = inject(ReceiptService);
|
||||
#returnCanReturnService = inject(ReturnCanReturnService);
|
||||
@@ -38,13 +38,17 @@ export class ReturnDetailsService {
|
||||
* @throws Will throw an error if the return check fails or is aborted.
|
||||
*/
|
||||
async canReturn(
|
||||
{ item, category }: { item: ReceiptItem; category: ProductCategory },
|
||||
{
|
||||
receiptItemId,
|
||||
quantity,
|
||||
category,
|
||||
}: { receiptItemId: number; quantity: number; category: ProductCategory },
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<CanReturn> {
|
||||
const returnReceiptValues: ReturnReceiptValues = {
|
||||
quantity: item.quantity.quantity,
|
||||
quantity,
|
||||
receiptItem: {
|
||||
id: item.id,
|
||||
id: receiptItemId,
|
||||
},
|
||||
category,
|
||||
};
|
||||
@@ -102,7 +106,7 @@ export class ReturnDetailsService {
|
||||
const res = await firstValueFrom(req$);
|
||||
|
||||
if (res.error || !res.result) {
|
||||
throw new Error(res.message || 'Failed to fetch return details');
|
||||
throw new Error(res.message || "Failed to fetch return details");
|
||||
}
|
||||
|
||||
return res.result as Receipt;
|
||||
@@ -112,7 +116,7 @@ export class ReturnDetailsService {
|
||||
* Validates that the email parameter is a properly formatted email address.
|
||||
*/
|
||||
static FetchReceiptsEmailParamsSchema = z.object({
|
||||
email: z.string().email(),
|
||||
email: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -137,7 +141,7 @@ export class ReturnDetailsService {
|
||||
let req$ = this.#receiptService.ReceiptQueryReceipt({
|
||||
queryToken: {
|
||||
input: { qs: email },
|
||||
filter: { receipt_type: '1;128;1024' },
|
||||
filter: { receipt_type: "1;128;1024" },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -147,7 +151,7 @@ export class ReturnDetailsService {
|
||||
|
||||
const res = await firstValueFrom(req$);
|
||||
if (res.error || !res.result) {
|
||||
throw new Error(res.message || 'Failed to fetch return items by email');
|
||||
throw new Error(res.message || "Failed to fetch return items by email");
|
||||
}
|
||||
|
||||
return res.result as ReceiptListItem[];
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { computed, inject, resource, untracked } from '@angular/core';
|
||||
import { computed, inject, resource } from "@angular/core";
|
||||
import {
|
||||
CanReturn,
|
||||
ProductCategory,
|
||||
Receipt,
|
||||
ReceiptItem,
|
||||
ReturnDetailsService,
|
||||
} from '@isa/oms/data-access';
|
||||
} from "@isa/oms/data-access";
|
||||
import {
|
||||
getState,
|
||||
patchState,
|
||||
signalStore,
|
||||
type,
|
||||
@@ -15,20 +14,18 @@ import {
|
||||
withMethods,
|
||||
withProps,
|
||||
withState,
|
||||
} from '@ngrx/signals';
|
||||
import { setEntity, withEntities, entityConfig } from '@ngrx/signals/entities';
|
||||
} from "@ngrx/signals";
|
||||
import { setEntity, withEntities, entityConfig } from "@ngrx/signals/entities";
|
||||
import {
|
||||
canReturnReceiptItem,
|
||||
getReceiptItemQuantity,
|
||||
getReceiptItemProductCategory,
|
||||
receiptItemHasCategory,
|
||||
} from '../helpers/return-process';
|
||||
import { SessionStorageProvider } from '@isa/core/storage';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { clone } from 'lodash';
|
||||
getReceiptItemReturnedQuantity,
|
||||
} from "../helpers/return-process";
|
||||
import { logger } from "@isa/core/logging";
|
||||
|
||||
interface ReturnDetailsState {
|
||||
_storageId: number | undefined;
|
||||
_selectedItemIds: number[];
|
||||
selectedProductCategory: Record<number, ProductCategory>;
|
||||
selectedQuantity: Record<number, number>;
|
||||
@@ -36,7 +33,6 @@ interface ReturnDetailsState {
|
||||
}
|
||||
|
||||
const initialState: ReturnDetailsState = {
|
||||
_storageId: undefined,
|
||||
_selectedItemIds: [],
|
||||
selectedProductCategory: {},
|
||||
selectedQuantity: {},
|
||||
@@ -45,40 +41,15 @@ const initialState: ReturnDetailsState = {
|
||||
|
||||
export const receiptConfig = entityConfig({
|
||||
entity: type<Receipt>(),
|
||||
collection: 'receipts',
|
||||
collection: "receipts",
|
||||
});
|
||||
|
||||
export const ReturnDetailsStore = signalStore(
|
||||
{ providedIn: 'root' },
|
||||
withState(initialState),
|
||||
withEntities(receiptConfig),
|
||||
withProps(() => ({
|
||||
_logger: logger(() => ({ store: 'ReturnDetailsStore' })),
|
||||
_logger: logger(() => ({ store: "ReturnDetailsStore" })),
|
||||
_returnDetailsService: inject(ReturnDetailsService),
|
||||
_storage: inject(SessionStorageProvider),
|
||||
})),
|
||||
withMethods((store) => ({
|
||||
_storageKey: () => `ReturnDetailsStore:${store._storageId}`,
|
||||
})),
|
||||
withMethods((store) => ({
|
||||
_storeState: () => {
|
||||
const state = getState(store);
|
||||
if (!store._storageId) {
|
||||
return;
|
||||
}
|
||||
store._storage.set(store._storageKey(), state);
|
||||
store._logger.debug('State stored:', () => state);
|
||||
},
|
||||
_restoreState: async () => {
|
||||
const data = await store._storage.get(store._storageKey());
|
||||
if (data) {
|
||||
patchState(store, data);
|
||||
store._logger.debug('State restored:', () => ({ data }));
|
||||
} else {
|
||||
patchState(store, { ...initialState, _storageId: store._storageId() });
|
||||
store._logger.debug('No state found, initialized with default state');
|
||||
}
|
||||
},
|
||||
})),
|
||||
withComputed((store) => ({
|
||||
items: computed<Array<ReceiptItem>>(() =>
|
||||
@@ -86,43 +57,56 @@ export const ReturnDetailsStore = signalStore(
|
||||
.receiptsEntities()
|
||||
.map((receipt) => receipt.items)
|
||||
.flat()
|
||||
.map((container) => {
|
||||
const item = container.data;
|
||||
if (!item) {
|
||||
const err = new Error('Item data is undefined');
|
||||
store._logger.error('Item data is undefined', err, () => ({
|
||||
item: container,
|
||||
}));
|
||||
throw err;
|
||||
}
|
||||
|
||||
const itemData = clone(item);
|
||||
|
||||
const quantityMap = store.selectedQuantity();
|
||||
|
||||
if (quantityMap[itemData.id]) {
|
||||
itemData.quantity = { quantity: quantityMap[itemData.id] };
|
||||
} else {
|
||||
const quantity = getReceiptItemQuantity(itemData);
|
||||
if (!itemData.quantity) {
|
||||
itemData.quantity = { quantity };
|
||||
} else {
|
||||
itemData.quantity.quantity = quantity;
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemData.features) {
|
||||
itemData.features = {};
|
||||
}
|
||||
|
||||
itemData.features['category'] =
|
||||
store.selectedProductCategory()[itemData.id] ||
|
||||
getReceiptItemProductCategory(itemData);
|
||||
|
||||
return itemData;
|
||||
}),
|
||||
.map((container) => container.data!),
|
||||
),
|
||||
})),
|
||||
withComputed((store) => ({
|
||||
availableQuantityMap: computed(() => {
|
||||
const items = store.items();
|
||||
const availableQuantity: Record<number, number> = {};
|
||||
|
||||
items.forEach((item) => {
|
||||
const itemId = item.id;
|
||||
const quantity = getReceiptItemQuantity(item);
|
||||
const returnedQuantity = getReceiptItemReturnedQuantity(item);
|
||||
availableQuantity[itemId] = quantity - returnedQuantity;
|
||||
});
|
||||
|
||||
return availableQuantity;
|
||||
}),
|
||||
|
||||
itemCategoryMap: computed(() => {
|
||||
const items = store.items();
|
||||
const categoryMap: Record<number, ProductCategory> = {};
|
||||
|
||||
items.forEach((item) => {
|
||||
const itemId = item.id;
|
||||
const selectedCategory = store.selectedProductCategory()[itemId];
|
||||
const category = getReceiptItemProductCategory(item);
|
||||
categoryMap[itemId] = selectedCategory ?? category;
|
||||
});
|
||||
|
||||
return categoryMap;
|
||||
}),
|
||||
})),
|
||||
|
||||
withComputed((store) => ({
|
||||
selectedQuantityMap: computed(() => {
|
||||
const items = store.items();
|
||||
const selectedQuantity: Record<number, number> = {};
|
||||
|
||||
items.forEach((item) => {
|
||||
const itemId = item.id;
|
||||
const quantity =
|
||||
store.selectedQuantity()[itemId] ||
|
||||
store.availableQuantityMap()[itemId];
|
||||
selectedQuantity[itemId] = quantity;
|
||||
});
|
||||
|
||||
return selectedQuantity;
|
||||
}),
|
||||
})),
|
||||
|
||||
withComputed((store) => ({
|
||||
selectedItemIds: computed(() => {
|
||||
const selectedIds = store._selectedItemIds();
|
||||
@@ -130,7 +114,7 @@ export const ReturnDetailsStore = signalStore(
|
||||
|
||||
return selectedIds.filter((id) => {
|
||||
const canReturnResult = canReturn[id]?.result;
|
||||
return typeof canReturnResult === 'boolean' ? canReturnResult : true;
|
||||
return typeof canReturnResult === "boolean" ? canReturnResult : true;
|
||||
});
|
||||
}),
|
||||
})),
|
||||
@@ -167,8 +151,8 @@ export const ReturnDetailsStore = signalStore(
|
||||
{ receiptId: request },
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
patchState(store, setEntity(receipt, receiptConfig));
|
||||
store._storeState();
|
||||
return receipt;
|
||||
},
|
||||
}),
|
||||
@@ -182,18 +166,21 @@ export const ReturnDetailsStore = signalStore(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const receiptItemId = item.id;
|
||||
const quantity = store.selectedQuantityMap()[receiptItemId];
|
||||
const category = store.itemCategoryMap()[receiptItemId];
|
||||
|
||||
return {
|
||||
item: item,
|
||||
category:
|
||||
store.selectedProductCategory()[item.id] ||
|
||||
getReceiptItemProductCategory(item),
|
||||
receiptItemId,
|
||||
quantity,
|
||||
category,
|
||||
};
|
||||
},
|
||||
loader: async ({ request, abortSignal }) => {
|
||||
if (request === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const key = `${request.item.id}:${request.category}`;
|
||||
const key = `${request.receiptItemId}:${request.category}`;
|
||||
|
||||
if (store.canReturn()[key]) {
|
||||
return store.canReturn()[key];
|
||||
@@ -207,7 +194,6 @@ export const ReturnDetailsStore = signalStore(
|
||||
canReturn: { ...store.canReturn(), [key]: res },
|
||||
});
|
||||
|
||||
store._storeState();
|
||||
return res;
|
||||
},
|
||||
}),
|
||||
@@ -248,37 +234,25 @@ export const ReturnDetailsStore = signalStore(
|
||||
})),
|
||||
|
||||
withMethods((store) => ({
|
||||
selectStorage: (id: number) => {
|
||||
untracked(() => {
|
||||
patchState(store, { _storageId: id });
|
||||
store._restoreState();
|
||||
store._storeState();
|
||||
store._logger.debug('Storage ID set:', () => ({ id }));
|
||||
});
|
||||
},
|
||||
addSelectedItems(itemIds: number[]) {
|
||||
const currentIds = store.selectedItemIds();
|
||||
const newIds = Array.from(new Set([...currentIds, ...itemIds]));
|
||||
patchState(store, { _selectedItemIds: newIds });
|
||||
store._storeState();
|
||||
},
|
||||
removeSelectedItems(itemIds: number[]) {
|
||||
const currentIds = store.selectedItemIds();
|
||||
const newIds = currentIds.filter((id) => !itemIds.includes(id));
|
||||
patchState(store, { _selectedItemIds: newIds });
|
||||
store._storeState();
|
||||
},
|
||||
async setProductCategory(itemId: number, category: ProductCategory) {
|
||||
const currentCategory = store.selectedProductCategory();
|
||||
const newCategory = { ...currentCategory, [itemId]: category };
|
||||
patchState(store, { selectedProductCategory: newCategory });
|
||||
store._storeState();
|
||||
},
|
||||
setQuantity(itemId: number, quantity: number) {
|
||||
const currentQuantity = store.selectedQuantity();
|
||||
const newQuantity = { ...currentQuantity, [itemId]: quantity };
|
||||
patchState(store, { selectedQuantity: newQuantity });
|
||||
store._storeState();
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -1,69 +1,70 @@
|
||||
import { createServiceFactory } from '@ngneat/spectator/jest';
|
||||
import { ReturnProcessStore } from './return-process.store';
|
||||
import { IDBStorageProvider } from '@isa/core/storage';
|
||||
import { ProcessService } from '@isa/core/process';
|
||||
import { patchState } from '@ngrx/signals';
|
||||
import { setAllEntities, setEntity } from '@ngrx/signals/entities';
|
||||
import { unprotected } from '@ngrx/signals/testing';
|
||||
import { Product, ReturnProcess } from '../models';
|
||||
import { CreateReturnProcessError } from '../errors/return-process';
|
||||
import { createServiceFactory } from "@ngneat/spectator/jest";
|
||||
import { ReturnProcessStore } from "./return-process.store";
|
||||
import { IDBStorageProvider } from "@isa/core/storage";
|
||||
import { ProcessService } from "@isa/core/process";
|
||||
import { patchState } from "@ngrx/signals";
|
||||
import { setAllEntities, setEntity } from "@ngrx/signals/entities";
|
||||
import { unprotected } from "@ngrx/signals/testing";
|
||||
import { Product, ReturnProcess } from "../models";
|
||||
import { CreateReturnProcessError } from "../errors/return-process";
|
||||
import { ProductCategory } from "../questions";
|
||||
|
||||
const TEST_ITEMS: Record<number, ReturnProcess['receiptItem']> = {
|
||||
const TEST_ITEMS: Record<number, ReturnProcess["receiptItem"]> = {
|
||||
1: {
|
||||
id: 1,
|
||||
actions: [{ key: 'canReturn', value: 'true' }],
|
||||
actions: [{ key: "canReturn", value: "true" }],
|
||||
product: {
|
||||
ean: '1234567890',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch',
|
||||
ean: "1234567890",
|
||||
format: "TB",
|
||||
formatDetail: "Taschenbuch",
|
||||
} as Product,
|
||||
quantity: { quantity: 1 },
|
||||
receiptNumber: 'R-001',
|
||||
receiptNumber: "R-001",
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
actions: [{ key: 'canReturn', value: 'false' }],
|
||||
actions: [{ key: "canReturn", value: "false" }],
|
||||
product: {
|
||||
ean: '0987654321',
|
||||
format: 'GEB',
|
||||
formatDetail: 'Buch',
|
||||
ean: "0987654321",
|
||||
format: "GEB",
|
||||
formatDetail: "Buch",
|
||||
} as Product,
|
||||
quantity: { quantity: 1 },
|
||||
receiptNumber: 'R-002',
|
||||
receiptNumber: "R-002",
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
actions: [{ key: 'canReturn', value: 'true' }],
|
||||
actions: [{ key: "canReturn", value: "true" }],
|
||||
product: {
|
||||
ean: '1122334455',
|
||||
format: 'AU',
|
||||
formatDetail: 'Audio',
|
||||
ean: "1122334455",
|
||||
format: "AU",
|
||||
formatDetail: "Audio",
|
||||
} as Product,
|
||||
quantity: { quantity: 1 },
|
||||
receiptNumber: 'R-003',
|
||||
receiptNumber: "R-003",
|
||||
},
|
||||
};
|
||||
|
||||
describe('ReturnProcessStore', () => {
|
||||
describe("ReturnProcessStore", () => {
|
||||
const createService = createServiceFactory({
|
||||
service: ReturnProcessStore,
|
||||
mocks: [IDBStorageProvider, ProcessService],
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should create an instance of ReturnProcessStore', () => {
|
||||
describe("Initialization", () => {
|
||||
it("should create an instance of ReturnProcessStore", () => {
|
||||
const spectator = createService();
|
||||
expect(spectator.service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a nextId computed property', () => {
|
||||
it("should have a nextId computed property", () => {
|
||||
const spectator = createService();
|
||||
expect(spectator.service.nextId()).toBe(1); // Assuming no entities exist initially
|
||||
});
|
||||
});
|
||||
|
||||
describe('Entity Management', () => {
|
||||
it('should remove all entities by process id', () => {
|
||||
describe("Entity Management", () => {
|
||||
it("should remove all entities by process id", () => {
|
||||
const spectator = createService();
|
||||
const store = spectator.service;
|
||||
|
||||
@@ -75,9 +76,10 @@ describe('ReturnProcessStore', () => {
|
||||
processId: 1,
|
||||
receiptId: 1,
|
||||
receiptItem: TEST_ITEMS[1],
|
||||
receiptDate: '',
|
||||
receiptDate: "",
|
||||
answers: {},
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
},
|
||||
{
|
||||
@@ -85,9 +87,10 @@ describe('ReturnProcessStore', () => {
|
||||
processId: 2,
|
||||
receiptId: 2,
|
||||
receiptItem: TEST_ITEMS[2],
|
||||
receiptDate: '',
|
||||
receiptDate: "",
|
||||
answers: {},
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
},
|
||||
{
|
||||
@@ -95,9 +98,10 @@ describe('ReturnProcessStore', () => {
|
||||
processId: 1,
|
||||
receiptId: 3,
|
||||
receiptItem: TEST_ITEMS[3],
|
||||
receiptDate: '',
|
||||
receiptDate: "",
|
||||
answers: {},
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
},
|
||||
] as ReturnProcess[]),
|
||||
@@ -108,7 +112,7 @@ describe('ReturnProcessStore', () => {
|
||||
expect(store.entities()[0].processId).toBe(2);
|
||||
});
|
||||
|
||||
it('should set an answer for a given entity', () => {
|
||||
it("should set an answer for a given entity", () => {
|
||||
const spectator = createService();
|
||||
const store = spectator.service;
|
||||
|
||||
@@ -120,19 +124,20 @@ describe('ReturnProcessStore', () => {
|
||||
processId: 1,
|
||||
receiptId: 1,
|
||||
receiptItem: TEST_ITEMS[1],
|
||||
receiptDate: '',
|
||||
receiptDate: "",
|
||||
answers: {},
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
},
|
||||
] as ReturnProcess[]),
|
||||
);
|
||||
|
||||
store.setAnswer(1, 'question1', 'answer1');
|
||||
expect(store.entityMap()[1].answers['question1']).toBe('answer1');
|
||||
store.setAnswer(1, "question1", "answer1");
|
||||
expect(store.entityMap()[1].answers["question1"]).toBe("answer1");
|
||||
});
|
||||
|
||||
it('should remove an answer for a given entity', () => {
|
||||
it("should remove an answer for a given entity", () => {
|
||||
const spectator = createService();
|
||||
const store = spectator.service;
|
||||
|
||||
@@ -141,25 +146,26 @@ describe('ReturnProcessStore', () => {
|
||||
setEntity({
|
||||
id: 1,
|
||||
processId: 1,
|
||||
answers: { question1: 'answer1', question2: 'answer2' } as Record<
|
||||
answers: { question1: "answer1", question2: "answer2" } as Record<
|
||||
string,
|
||||
unknown
|
||||
>,
|
||||
receiptDate: new Date().toJSON(),
|
||||
receiptItem: TEST_ITEMS[1],
|
||||
receiptId: 123,
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
} as ReturnProcess),
|
||||
);
|
||||
|
||||
store.removeAnswer(1, 'question1');
|
||||
expect(store.entityMap()[1].answers['question1']).toBeUndefined();
|
||||
store.removeAnswer(1, "question1");
|
||||
expect(store.entityMap()[1].answers["question1"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Process Management', () => {
|
||||
it('should initialize a new return process', () => {
|
||||
describe("Process Management", () => {
|
||||
it("should initialize a new return process", () => {
|
||||
const spectator = createService();
|
||||
const store = spectator.service;
|
||||
|
||||
@@ -169,28 +175,44 @@ describe('ReturnProcessStore', () => {
|
||||
{
|
||||
receipt: {
|
||||
id: 1,
|
||||
printedDate: '',
|
||||
printedDate: "",
|
||||
items: [],
|
||||
buyer: { buyerNumber: '' },
|
||||
buyer: { buyerNumber: "" },
|
||||
},
|
||||
items: [TEST_ITEMS[1]],
|
||||
items: [
|
||||
{
|
||||
receiptItem: TEST_ITEMS[1],
|
||||
quantity: 1,
|
||||
category: ProductCategory.BookCalendar,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
receipt: {
|
||||
id: 2,
|
||||
printedDate: '',
|
||||
printedDate: "",
|
||||
items: [],
|
||||
buyer: { buyerNumber: '' },
|
||||
buyer: { buyerNumber: "" },
|
||||
},
|
||||
items: [TEST_ITEMS[3]],
|
||||
items: [
|
||||
{
|
||||
receiptItem: TEST_ITEMS[3],
|
||||
quantity: 1,
|
||||
category: ProductCategory.BookCalendar,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(store.entities()).toHaveLength(2);
|
||||
expect(store.entities()[0].productCategory).toBe(
|
||||
ProductCategory.BookCalendar,
|
||||
);
|
||||
expect(store.entities()[0].quantity).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw an error if no returnable items are found', () => {
|
||||
it("should throw an error if no returnable items are found", () => {
|
||||
const spectator = createService();
|
||||
const store = spectator.service;
|
||||
|
||||
@@ -201,18 +223,24 @@ describe('ReturnProcessStore', () => {
|
||||
{
|
||||
receipt: {
|
||||
id: 2,
|
||||
printedDate: '',
|
||||
printedDate: "",
|
||||
items: [],
|
||||
buyer: { buyerNumber: '' },
|
||||
buyer: { buyerNumber: "" },
|
||||
},
|
||||
items: [TEST_ITEMS[2]], // Non-returnable item
|
||||
items: [
|
||||
{
|
||||
receiptItem: TEST_ITEMS[2], // Non-returnable item
|
||||
quantity: 1,
|
||||
category: ProductCategory.BookCalendar,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}).toThrow(CreateReturnProcessError);
|
||||
});
|
||||
|
||||
it('should throw an error if the number of returnable items does not match the total items', () => {
|
||||
it("should throw an error if the number of returnable items does not match the total items", () => {
|
||||
const spectator = createService();
|
||||
const store = spectator.service;
|
||||
|
||||
@@ -223,11 +251,27 @@ describe('ReturnProcessStore', () => {
|
||||
{
|
||||
receipt: {
|
||||
id: 3,
|
||||
printedDate: '',
|
||||
printedDate: "",
|
||||
items: [],
|
||||
buyer: { buyerNumber: '' },
|
||||
buyer: { buyerNumber: "" },
|
||||
},
|
||||
items: [TEST_ITEMS[1], TEST_ITEMS[2], TEST_ITEMS[3]],
|
||||
items: [
|
||||
{
|
||||
receiptItem: TEST_ITEMS[1],
|
||||
quantity: 1,
|
||||
category: ProductCategory.BookCalendar,
|
||||
},
|
||||
{
|
||||
receiptItem: TEST_ITEMS[2],
|
||||
quantity: 1,
|
||||
category: ProductCategory.BookCalendar,
|
||||
},
|
||||
{
|
||||
receiptItem: TEST_ITEMS[3],
|
||||
quantity: 1,
|
||||
category: ProductCategory.BookCalendar,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -5,29 +5,37 @@ import {
|
||||
withHooks,
|
||||
withMethods,
|
||||
withProps,
|
||||
} from '@ngrx/signals';
|
||||
} from "@ngrx/signals";
|
||||
import {
|
||||
withEntities,
|
||||
setAllEntities,
|
||||
updateEntity,
|
||||
} from '@ngrx/signals/entities';
|
||||
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';
|
||||
} from "@ngrx/signals/entities";
|
||||
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 {
|
||||
CreateReturnProcessError,
|
||||
CreateReturnProcessErrorReason,
|
||||
} from '../errors/return-process';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { canReturnReceiptItem } from '../helpers/return-process';
|
||||
} from "../errors/return-process";
|
||||
import { logger } from "@isa/core/logging";
|
||||
import { canReturnReceiptItem } from "../helpers/return-process";
|
||||
import { ProductCategory } from "../questions";
|
||||
|
||||
/**
|
||||
* Interface representing the parameters required to start a return process.
|
||||
*/
|
||||
export type StartProcess = {
|
||||
processId: number;
|
||||
returns: { receipt: Receipt; items: ReceiptItem[] }[];
|
||||
returns: {
|
||||
receipt: Receipt;
|
||||
items: {
|
||||
receiptItem: ReceiptItem;
|
||||
quantity: number;
|
||||
category: ProductCategory;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -55,12 +63,12 @@ export type StartProcess = {
|
||||
* - Throws a MismatchReturnableItemsError if the number of returnable items does not match the expected count.
|
||||
*/
|
||||
export const ReturnProcessStore = signalStore(
|
||||
{ providedIn: 'root' },
|
||||
withStorage('return-process', IDBStorageProvider),
|
||||
{ providedIn: "root" },
|
||||
withStorage("return-process", IDBStorageProvider),
|
||||
withEntities<ReturnProcess>(),
|
||||
withProps(() => ({
|
||||
_logger: logger(() => ({
|
||||
store: 'ReturnProcessStore',
|
||||
store: "ReturnProcessStore",
|
||||
})),
|
||||
})),
|
||||
withComputed((store) => ({
|
||||
@@ -142,6 +150,7 @@ export const ReturnProcessStore = signalStore(
|
||||
|
||||
const returnableItems = params.returns
|
||||
.flatMap((r) => r.items)
|
||||
.map((item) => item.receiptItem)
|
||||
.filter(canReturnReceiptItem);
|
||||
|
||||
if (returnableItems.length === 0) {
|
||||
@@ -170,9 +179,10 @@ export const ReturnProcessStore = signalStore(
|
||||
id: nextId + entities.length,
|
||||
processId: params.processId,
|
||||
receiptId: receipt.id,
|
||||
productCategory: item.features?.['category'],
|
||||
productCategory: item.category,
|
||||
quantity: item.quantity,
|
||||
receiptDate: receipt.printedDate,
|
||||
receiptItem: item,
|
||||
receiptItem: item.receiptItem,
|
||||
answers: {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<div class="flex flex-row w-full">
|
||||
<div
|
||||
class="flex flex-row justify-end -mb-4 desktop:mb-0 w-[13.4375rem] desktop:w-full"
|
||||
>
|
||||
@if (quantityDropdownValues().length > 1) {
|
||||
<ui-dropdown
|
||||
class="quantity-dropdown"
|
||||
[disabled]="!canReturnReceiptItem()"
|
||||
[value]="availableQuantity()"
|
||||
[value]="selectedQuantity()"
|
||||
(valueChange)="setQuantity($event)"
|
||||
>
|
||||
@for (quantity of quantityDropdownValues(); track quantity) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
:host {
|
||||
@apply flex flex-col-reverse items-end desktop:flex-row desktop:justify-center desktop:items-center gap-4;
|
||||
@apply flex flex-col-reverse items-end desktop:flex-row desktop:justify-center desktop:items-center gap-4;
|
||||
|
||||
.product-dropdown.ui-dropdown {
|
||||
@apply max-w-[13.4375rem] desktop:max-w-full;
|
||||
}
|
||||
|
||||
:has(.product-dropdown):has(.quantity-dropdown) {
|
||||
.quantity-dropdown.ui-dropdown {
|
||||
@apply border-r-0 pr-4;
|
||||
@apply border-r-0 pr-4 pl-5 max-w-20 desktop:max-w-full;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
@@ -15,7 +19,7 @@
|
||||
}
|
||||
|
||||
.product-dropdown.ui-dropdown {
|
||||
@apply border-l-0 pl-4;
|
||||
@apply border-l-0 max-w-[8.75rem] desktop:max-w-full pr-5 pl-4;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
|
||||
@@ -1,50 +1,52 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { MockDirective } from 'ng-mocks';
|
||||
import { createComponentFactory, Spectator } from "@ngneat/spectator/jest";
|
||||
import { MockDirective } from "ng-mocks";
|
||||
|
||||
import {
|
||||
ReceiptItem,
|
||||
ReturnDetailsService,
|
||||
ReturnDetailsStore,
|
||||
} from '@isa/oms/data-access';
|
||||
} from "@isa/oms/data-access";
|
||||
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { ReturnDetailsOrderGroupItemControlsComponent } from '../return-details-order-group-item-controls/return-details-order-group-item-controls.component';
|
||||
import { CheckboxComponent } from '@isa/ui/input-controls';
|
||||
import { signal } from '@angular/core';
|
||||
import { ProductImageDirective } from "@isa/shared/product-image";
|
||||
import { ReturnDetailsOrderGroupItemControlsComponent } from "../return-details-order-group-item-controls/return-details-order-group-item-controls.component";
|
||||
import { CheckboxComponent } from "@isa/ui/input-controls";
|
||||
import { signal } from "@angular/core";
|
||||
|
||||
// Helper function to create mock ReceiptItem data
|
||||
const createMockItem = (
|
||||
ean: string,
|
||||
canReturn: boolean,
|
||||
name = 'Test Product',
|
||||
category = 'BOOK', // Add default category that's not 'unknown'
|
||||
name = "Test Product",
|
||||
category = "BOOK", // Add default category that's not 'unknown'
|
||||
availableQuantity = 2,
|
||||
selectedQuantity = 1,
|
||||
): ReceiptItem =>
|
||||
({
|
||||
id: 123,
|
||||
receiptNumber: 'R-123456', // Add the required receiptNumber property
|
||||
quantity: { quantity: 1 },
|
||||
receiptNumber: "R-123456",
|
||||
quantity: { quantity: availableQuantity },
|
||||
price: {
|
||||
value: { value: 19.99, currency: 'EUR' },
|
||||
value: { value: 19.99, currency: "EUR" },
|
||||
vat: { inPercent: 19 },
|
||||
},
|
||||
product: {
|
||||
ean: ean,
|
||||
name: name,
|
||||
contributors: 'Test Author',
|
||||
format: 'HC',
|
||||
formatDetail: 'Hardcover',
|
||||
manufacturer: 'Test Publisher',
|
||||
publicationDate: '2024-01-01T00:00:00Z',
|
||||
catalogProductNumber: '1234567890',
|
||||
volume: '1',
|
||||
contributors: "Test Author",
|
||||
format: "HC",
|
||||
formatDetail: "Hardcover",
|
||||
manufacturer: "Test Publisher",
|
||||
publicationDate: "2024-01-01T00:00:00Z",
|
||||
catalogProductNumber: "1234567890",
|
||||
volume: "1",
|
||||
},
|
||||
actions: [{ key: 'canReturn', value: String(canReturn) }],
|
||||
features: { category: category }, // Add the features property with category
|
||||
actions: [{ key: "canReturn", value: String(canReturn) }],
|
||||
features: { category: category },
|
||||
}) as ReceiptItem;
|
||||
|
||||
describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
describe("ReturnDetailsOrderGroupItemControlsComponent", () => {
|
||||
let spectator: Spectator<ReturnDetailsOrderGroupItemControlsComponent>;
|
||||
const mockItemSelectable = createMockItem('1234567890123', true);
|
||||
const mockItemSelectable = createMockItem("1234567890123", true);
|
||||
|
||||
const mockIsSelectable = signal<boolean>(true);
|
||||
const mockGetItemSelectted = signal<boolean>(false);
|
||||
@@ -52,6 +54,11 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
isLoading: signal<boolean>(true),
|
||||
};
|
||||
|
||||
// Mocks for availableQuantityMap and selectedQuantityMap
|
||||
const mockAvailableQuantityMap = { [mockItemSelectable.id]: 2 };
|
||||
const mockSelectedQuantityMap = { [mockItemSelectable.id]: 1 };
|
||||
const mockItemCategoryMap = { [mockItemSelectable.id]: "BOOK" };
|
||||
|
||||
function resetMocks() {
|
||||
mockIsSelectable.set(true);
|
||||
mockGetItemSelectted.set(false);
|
||||
@@ -68,12 +75,16 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
isSelectable: jest.fn(() => mockIsSelectable),
|
||||
getItemSelected: jest.fn(() => mockGetItemSelectted),
|
||||
canReturnResource: jest.fn(() => mockCanReturnResource),
|
||||
availableQuantityMap: jest.fn(() => mockAvailableQuantityMap),
|
||||
selectedQuantityMap: jest.fn(() => mockSelectedQuantityMap),
|
||||
itemCategoryMap: jest.fn(() => mockItemCategoryMap),
|
||||
setProductCategory: jest.fn(),
|
||||
setQuantity: jest.fn(),
|
||||
addSelectedItems: jest.fn(),
|
||||
removeSelectedItems: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
// Spectator automatically stubs standalone dependencies like ItemRowComponent, CheckboxComponent etc.
|
||||
// We don't need deep interaction, just verify the host component renders correctly.
|
||||
// If specific interactions were needed, we could provide mocks or use overrideComponents.
|
||||
overrideComponents: [
|
||||
[
|
||||
ReturnDetailsOrderGroupItemControlsComponent,
|
||||
@@ -85,50 +96,41 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
},
|
||||
],
|
||||
],
|
||||
detectChanges: false, // Control initial detection manually
|
||||
detectChanges: false,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Default setup with a selectable item
|
||||
spectator = createComponent({
|
||||
props: {
|
||||
item: mockItemSelectable, // Use signal for input
|
||||
item: mockItemSelectable,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetMocks(); // Reset mocks after each test
|
||||
resetMocks();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
// Arrange
|
||||
spectator.detectChanges(); // Trigger initial render
|
||||
|
||||
// Assert
|
||||
it("should create", () => {
|
||||
spectator.detectChanges();
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display the checkbox when item is selectable', () => {
|
||||
// Arrange
|
||||
mockCanReturnResource.isLoading.set(false); // Simulate the resource being ready
|
||||
mockIsSelectable.set(true); // Simulate the item being selectable
|
||||
it("should display the checkbox when item is selectable and not loading", () => {
|
||||
mockCanReturnResource.isLoading.set(false);
|
||||
mockIsSelectable.set(true);
|
||||
spectator.detectChanges();
|
||||
// Assert
|
||||
expect(spectator.component.selectable()).toBe(true);
|
||||
const checkbox = spectator.query(CheckboxComponent);
|
||||
expect(checkbox).toBeTruthy();
|
||||
expect(spectator.query(CheckboxComponent)).toBeTruthy();
|
||||
expect(
|
||||
spectator.query(`input[data-what="return-item-checkbox"]`),
|
||||
).toExist();
|
||||
});
|
||||
it('should NOT display the checkbox when item is not selectable', () => {
|
||||
// Arrange
|
||||
mockIsSelectable.set(false); // Simulate the item not being selectable
|
||||
spectator.detectChanges();
|
||||
spectator.detectComponentChanges();
|
||||
|
||||
// Assert
|
||||
it("should NOT display the checkbox when item is not selectable", () => {
|
||||
mockIsSelectable.set(false);
|
||||
mockCanReturnResource.isLoading.set(false);
|
||||
spectator.detectChanges();
|
||||
expect(spectator.component.selectable()).toBe(false);
|
||||
expect(
|
||||
spectator.query(`input[data-what="return-item-checkbox"]`),
|
||||
@@ -136,27 +138,73 @@ describe('ReturnDetailsOrderGroupItemControlsComponent', () => {
|
||||
expect(spectator.query(CheckboxComponent)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should be false when no canReturn action is present', () => {
|
||||
// Arrange
|
||||
const item = { ...createMockItem('0001', true), actions: [] };
|
||||
spectator.setInput('item', item as any);
|
||||
|
||||
// Act
|
||||
it("should show spinner when canReturnResource is loading", () => {
|
||||
mockCanReturnResource.isLoading.set(true);
|
||||
spectator.detectChanges();
|
||||
expect(
|
||||
spectator.query('ui-icon-button[data-what="load-spinner"]'),
|
||||
).toExist();
|
||||
});
|
||||
|
||||
// Assert
|
||||
it("should render correct quantity dropdown values", () => {
|
||||
spectator.detectChanges();
|
||||
expect(spectator.component.quantityDropdownValues()).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("should call setQuantity when dropdown value changes", () => {
|
||||
const store = spectator.inject(ReturnDetailsStore);
|
||||
const spy = jest.spyOn(store, "setQuantity");
|
||||
spectator.detectChanges();
|
||||
// Simulate dropdown value change
|
||||
spectator.component.setQuantity(2);
|
||||
expect(spy).toHaveBeenCalledWith(mockItemSelectable.id, 2);
|
||||
});
|
||||
|
||||
it("should call setProductCategory when product category changes", () => {
|
||||
const store = spectator.inject(ReturnDetailsStore);
|
||||
const spy = jest.spyOn(store, "setProductCategory");
|
||||
spectator.detectChanges();
|
||||
spectator.component.setProductCategory("Buch/Kalender");
|
||||
expect(spy).toHaveBeenCalledWith(mockItemSelectable.id, "Buch/Kalender");
|
||||
});
|
||||
|
||||
it("should call addSelectedItems when setSelected(true) is called", () => {
|
||||
const store = spectator.inject(ReturnDetailsStore);
|
||||
const spy = jest.spyOn(store, "addSelectedItems");
|
||||
spectator.detectChanges();
|
||||
spectator.component.setSelected(true);
|
||||
expect(spy).toHaveBeenCalledWith([mockItemSelectable.id]);
|
||||
});
|
||||
|
||||
it("should call removeSelectedItems when setSelected(false) is called", () => {
|
||||
const store = spectator.inject(ReturnDetailsStore);
|
||||
const spy = jest.spyOn(store, "removeSelectedItems");
|
||||
spectator.detectChanges();
|
||||
spectator.component.setSelected(false);
|
||||
expect(spy).toHaveBeenCalledWith([mockItemSelectable.id]);
|
||||
});
|
||||
|
||||
it("should be false when no canReturn action is present", () => {
|
||||
const item = { ...createMockItem("0001", true), actions: [] };
|
||||
spectator.setInput("item", item as any);
|
||||
spectator.detectChanges();
|
||||
expect(spectator.component.canReturnReceiptItem()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be false when canReturn action has falsy value', () => {
|
||||
// Arrange
|
||||
const item = createMockItem('0001', false);
|
||||
spectator.setInput('item', item);
|
||||
|
||||
// Act
|
||||
it("should be false when canReturn action has falsy value", () => {
|
||||
const item = createMockItem("0001", false);
|
||||
spectator.setInput("item", item);
|
||||
spectator.detectChanges();
|
||||
|
||||
// Assert
|
||||
expect(spectator.component.canReturnReceiptItem()).toBe(false);
|
||||
});
|
||||
|
||||
it("should display correct selected quantity", () => {
|
||||
spectator.detectChanges();
|
||||
expect(spectator.component.selectedQuantity()).toBe(1);
|
||||
});
|
||||
|
||||
it("should display correct product category", () => {
|
||||
spectator.detectChanges();
|
||||
expect(spectator.component.productCategory()).toBe("BOOK");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,30 +5,27 @@ import {
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { provideLoggerContext } from '@isa/core/logging';
|
||||
} from "@angular/core";
|
||||
import { provideLoggerContext } from "@isa/core/logging";
|
||||
import {
|
||||
canReturnReceiptItem,
|
||||
getReceiptItemReturnedQuantity,
|
||||
getReceiptItemProductCategory,
|
||||
getReceiptItemQuantity,
|
||||
ProductCategory,
|
||||
ReceiptItem,
|
||||
ReturnDetailsService,
|
||||
ReturnDetailsStore,
|
||||
} from '@isa/oms/data-access';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
} from "@isa/oms/data-access";
|
||||
import { IconButtonComponent } from "@isa/ui/buttons";
|
||||
import {
|
||||
CheckboxComponent,
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
} from "@isa/ui/input-controls";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
@Component({
|
||||
selector: 'oms-feature-return-details-order-group-item-controls',
|
||||
templateUrl: './return-details-order-group-item-controls.component.html',
|
||||
styleUrls: ['./return-details-order-group-item-controls.component.scss'],
|
||||
selector: "oms-feature-return-details-order-group-item-controls",
|
||||
templateUrl: "./return-details-order-group-item-controls.component.html",
|
||||
styleUrls: ["./return-details-order-group-item-controls.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
@@ -40,7 +37,7 @@ import { FormsModule } from '@angular/forms';
|
||||
],
|
||||
providers: [
|
||||
provideLoggerContext({
|
||||
component: 'ReturnDetailsOrderGroupItemControlsComponent',
|
||||
component: "ReturnDetailsOrderGroupItemControlsComponent",
|
||||
}),
|
||||
],
|
||||
})
|
||||
@@ -66,38 +63,11 @@ export class ReturnDetailsOrderGroupItemControlsComponent {
|
||||
|
||||
availableCategories = this.#returnDetailsService.availableCategories();
|
||||
|
||||
/**
|
||||
* Computes the quantity of the current receipt item that has already been returned.
|
||||
*
|
||||
* This value is derived from the item's return history and is used to indicate
|
||||
* how many units have already been processed for return.
|
||||
*
|
||||
* @returns The number of units already returned for this receipt item.
|
||||
*/
|
||||
returnedQuantity = computed(() => {
|
||||
selectedQuantity = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemReturnedQuantity(item);
|
||||
return this.#store.selectedQuantityMap()[item.id];
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the total quantity for the current receipt item.
|
||||
* Represents the original quantity as recorded in the receipt.
|
||||
*
|
||||
* @returns The total quantity for the item.
|
||||
*/
|
||||
quantity = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemQuantity(item);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the quantity of the item that is still available for return.
|
||||
* Calculated as the difference between the total quantity and the returned quantity.
|
||||
*
|
||||
* @returns The number of units available to be returned.
|
||||
*/
|
||||
availableQuantity = computed(() => this.quantity() - this.returnedQuantity());
|
||||
|
||||
/**
|
||||
* Generates the list of selectable quantities for the dropdown.
|
||||
* The values range from 1 up to the available quantity.
|
||||
@@ -105,13 +75,14 @@ export class ReturnDetailsOrderGroupItemControlsComponent {
|
||||
* @returns An array of selectable quantity values.
|
||||
*/
|
||||
quantityDropdownValues = computed(() => {
|
||||
const itemQuantity = this.availableQuantity();
|
||||
const item = this.item();
|
||||
const itemQuantity = this.#store.availableQuantityMap()[item.id];
|
||||
return Array.from({ length: itemQuantity }, (_, i) => i + 1);
|
||||
});
|
||||
|
||||
productCategory = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemProductCategory(item);
|
||||
return this.#store.itemCategoryMap()[item.id];
|
||||
});
|
||||
|
||||
selectable = this.#store.isSelectable(this.item);
|
||||
@@ -127,8 +98,9 @@ export class ReturnDetailsOrderGroupItemControlsComponent {
|
||||
}
|
||||
|
||||
setQuantity(quantity: number | undefined) {
|
||||
const item = this.item();
|
||||
if (quantity === undefined) {
|
||||
quantity = this.item().quantity.quantity;
|
||||
quantity = this.#store.availableQuantityMap()[item.id];
|
||||
}
|
||||
this.#store.setQuantity(this.item().id, quantity);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
{{ i.product.manufacturer }} | {{ i.product.ean }}
|
||||
</div>
|
||||
<div class="text-isa-neutral-600 isa-text-body-2-regular">
|
||||
{{ i.product.publicationDate | date: 'dd. MMM yyyy' }}
|
||||
{{ i.product.publicationDate | date: "dd. MMM yyyy" }}
|
||||
</div>
|
||||
</div>
|
||||
<oms-feature-return-details-order-group-item-controls [item]="i">
|
||||
@@ -73,11 +73,11 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (returnedQuantity() > 0 && itemQuantity() !== returnedQuantity()) {
|
||||
@if (availableQuantity() !== quantity()) {
|
||||
<div
|
||||
class="flex items-center self-start text-isa-neutral-600 isa-text-body-2-bold pb-6"
|
||||
>
|
||||
Es wurden bereits {{ returnedQuantity() }} von {{ itemQuantity() }} Artikel
|
||||
zurückgegeben.
|
||||
Es wurden bereits {{ quantity() - availableQuantity() }} von
|
||||
{{ quantity() }} Artikel zurückgegeben.
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { CurrencyPipe, DatePipe, LowerCasePipe } from '@angular/common';
|
||||
import { CurrencyPipe, DatePipe, LowerCasePipe } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { isaActionClose, ProductFormatIconGroup } from '@isa/icons';
|
||||
} from "@angular/core";
|
||||
import { isaActionClose, ProductFormatIconGroup } from "@isa/icons";
|
||||
import {
|
||||
getReceiptItemAction,
|
||||
getReceiptItemReturnedQuantity,
|
||||
getReceiptItemQuantity,
|
||||
ReceiptItem,
|
||||
ReturnDetailsStore,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ProductImageDirective } from '@isa/shared/product-image';
|
||||
import { ItemRowComponent } from '@isa/ui/item-rows';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { ReturnDetailsOrderGroupItemControlsComponent } from '../return-details-order-group-item-controls/return-details-order-group-item-controls.component';
|
||||
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
|
||||
} from "@isa/oms/data-access";
|
||||
import { ProductImageDirective } from "@isa/shared/product-image";
|
||||
import { ItemRowComponent } from "@isa/ui/item-rows";
|
||||
import { NgIconComponent, provideIcons } from "@ng-icons/core";
|
||||
import { ReturnDetailsOrderGroupItemControlsComponent } from "../return-details-order-group-item-controls/return-details-order-group-item-controls.component";
|
||||
import { ProductRouterLinkDirective } from "@isa/shared/product-router-link";
|
||||
|
||||
@Component({
|
||||
selector: 'oms-feature-return-details-order-group-item',
|
||||
templateUrl: './return-details-order-group-item.component.html',
|
||||
styleUrls: ['./return-details-order-group-item.component.scss'],
|
||||
selector: "oms-feature-return-details-order-group-item",
|
||||
templateUrl: "./return-details-order-group-item.component.html",
|
||||
styleUrls: ["./return-details-order-group-item.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
@@ -82,7 +81,7 @@ export class ReturnDetailsOrderGroupItemComponent {
|
||||
*/
|
||||
canReturnMessage = computed(() => {
|
||||
const item = this.item();
|
||||
const canReturnAction = getReceiptItemAction(item, 'canReturn');
|
||||
const canReturnAction = getReceiptItemAction(item, "canReturn");
|
||||
|
||||
if (canReturnAction?.description) {
|
||||
return canReturnAction.description;
|
||||
@@ -90,30 +89,32 @@ export class ReturnDetailsOrderGroupItemComponent {
|
||||
|
||||
const canReturnMessage = this.canReturn()?.message;
|
||||
|
||||
return canReturnMessage ?? '';
|
||||
return canReturnMessage ?? "";
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the quantity of the current receipt item that has already been returned.
|
||||
* The original quantity of the item as recorded in the order.
|
||||
* This value is retrieved from the store and represents the total number of units
|
||||
* initially purchased for this receipt item.
|
||||
*
|
||||
* This value is derived using the item's return history and is used to display
|
||||
* how many units of this item have been processed for return so far.
|
||||
*
|
||||
* @returns The number of units already returned for this receipt item.
|
||||
* @readonly
|
||||
* @returns {number} The original quantity of the item in the order.
|
||||
*/
|
||||
returnedQuantity = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemReturnedQuantity(item);
|
||||
});
|
||||
|
||||
/**
|
||||
* Computes the total quantity for the current receipt item.
|
||||
* Represents the original quantity of the item as recorded in the receipt.
|
||||
*
|
||||
* @returns The total quantity for the item.
|
||||
*/
|
||||
itemQuantity = computed(() => {
|
||||
quantity = computed(() => {
|
||||
const item = this.item();
|
||||
return getReceiptItemQuantity(item);
|
||||
});
|
||||
|
||||
/**
|
||||
* The currently available quantity of the item for return.
|
||||
* This value is computed based on the item's current state and may be less than
|
||||
* the original quantity if some units have already been returned or are otherwise unavailable.
|
||||
*
|
||||
* @readonly
|
||||
* @returns {number} The number of units available for return.
|
||||
*/
|
||||
availableQuantity = computed(() => {
|
||||
const item = this.item();
|
||||
return this.#store.availableQuantityMap()[item.id];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
></oms-feature-return-details-static>
|
||||
@if (customerReceiptsResource.isLoading()) {
|
||||
<ui-progress-bar class="w-full" mode="indeterminate"></ui-progress-bar>
|
||||
} @else {
|
||||
} @else if (!customerReceiptsResource.error()) {
|
||||
@for (receipt of customerReceiptsResource.value(); track receipt.id) {
|
||||
@if (r.id !== receipt.id) {
|
||||
<oms-feature-return-details-lazy
|
||||
|
||||
@@ -2,35 +2,34 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
resource,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { z } from 'zod';
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { z } from "zod";
|
||||
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaActionChevronLeft } from '@isa/icons';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import { injectActivatedProcessId } from '@isa/core/process';
|
||||
import { Location } from '@angular/common';
|
||||
import { ExpandableDirectives } from '@isa/ui/expandable';
|
||||
import { ProgressBarComponent } from '@isa/ui/progress-bar';
|
||||
import { NgIconComponent, provideIcons } from "@ng-icons/core";
|
||||
import { isaActionChevronLeft } from "@isa/icons";
|
||||
import { ButtonComponent } from "@isa/ui/buttons";
|
||||
import { injectActivatedProcessId } from "@isa/core/process";
|
||||
import { Location } from "@angular/common";
|
||||
import { ExpandableDirectives } from "@isa/ui/expandable";
|
||||
import { ProgressBarComponent } from "@isa/ui/progress-bar";
|
||||
import {
|
||||
ReturnDetailsService,
|
||||
ReturnProcessStore,
|
||||
ReturnDetailsStore,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ReturnDetailsStaticComponent } from './return-details-static/return-details-static.component';
|
||||
import { ReturnDetailsLazyComponent } from './return-details-lazy/return-details-lazy.component';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { groupBy } from 'lodash';
|
||||
} from "@isa/oms/data-access";
|
||||
import { ReturnDetailsStaticComponent } from "./return-details-static/return-details-static.component";
|
||||
import { ReturnDetailsLazyComponent } from "./return-details-lazy/return-details-lazy.component";
|
||||
import { logger } from "@isa/core/logging";
|
||||
import { groupBy } from "lodash";
|
||||
|
||||
@Component({
|
||||
selector: 'oms-feature-return-details',
|
||||
templateUrl: './return-details.component.html',
|
||||
styleUrls: ['./return-details.component.scss'],
|
||||
selector: "oms-feature-return-details",
|
||||
templateUrl: "./return-details.component.html",
|
||||
styleUrls: ["./return-details.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ReturnDetailsStaticComponent,
|
||||
@@ -40,11 +39,11 @@ import { groupBy } from 'lodash';
|
||||
ExpandableDirectives,
|
||||
ProgressBarComponent,
|
||||
],
|
||||
providers: [provideIcons({ isaActionChevronLeft })],
|
||||
providers: [provideIcons({ isaActionChevronLeft }), ReturnDetailsStore],
|
||||
})
|
||||
export class ReturnDetailsComponent {
|
||||
#logger = logger(() => ({
|
||||
component: ReturnDetailsComponent.name,
|
||||
component: "ReturnDetailsComponent",
|
||||
itemId: this.receiptId(),
|
||||
processId: this.processId(),
|
||||
params: this.params(),
|
||||
@@ -66,29 +65,30 @@ export class ReturnDetailsComponent {
|
||||
receiptId = computed<number>(() => {
|
||||
const params = this.params();
|
||||
if (params) {
|
||||
return z.coerce.number().parse(params['receiptId']);
|
||||
return z.coerce.number().parse(params["receiptId"]);
|
||||
}
|
||||
throw new Error('No receiptId found in route params');
|
||||
throw new Error("No receiptId found in route params");
|
||||
});
|
||||
|
||||
// Effect resets the Store's state when the receiptId changes
|
||||
// This ensures that the store is always in sync with the current receiptId
|
||||
receiptIdEffect = effect(() => this.#store.selectStorage(this.receiptId()));
|
||||
|
||||
receiptResource = this.#store.receiptResource(this.receiptId);
|
||||
|
||||
customerReceiptsResource = resource({
|
||||
request: this.receiptResource.value,
|
||||
loader: async ({ request, abortSignal }) => {
|
||||
console.log('Fetching customer receipts for:', request);
|
||||
const email = request?.buyer?.communicationDetails?.email;
|
||||
if (!email) {
|
||||
return [];
|
||||
}
|
||||
return await this.#returnDetailsService.fetchReceiptsByEmail(
|
||||
{ email },
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
try {
|
||||
return await this.#returnDetailsService.fetchReceiptsByEmail(
|
||||
{ email },
|
||||
abortSignal,
|
||||
);
|
||||
} catch (error) {
|
||||
this.#logger.error("Failed to fetch customer receipts", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -101,15 +101,17 @@ export class ReturnDetailsComponent {
|
||||
startProcess() {
|
||||
if (!this.canStartProcess()) {
|
||||
this.#logger.warn(
|
||||
'Cannot start process: No items selected or no process ID',
|
||||
"Cannot start process: No items selected or no process ID",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const processId = this.processId();
|
||||
const selectedItems = this.#store.selectedItems();
|
||||
const selectedQuantites = this.#store.selectedQuantityMap();
|
||||
const selectedProductCategories = this.#store.itemCategoryMap();
|
||||
|
||||
this.#logger.info('Starting return process', () => ({
|
||||
this.#logger.info("Starting return process", () => ({
|
||||
processId: processId,
|
||||
selectedItems: selectedItems.map((item) => item.id),
|
||||
}));
|
||||
@@ -127,11 +129,18 @@ export class ReturnDetailsComponent {
|
||||
const returns = Object.entries(itemsGrouptByReceiptId).map(
|
||||
([receiptId, items]) => ({
|
||||
receipt: receipts[Number(receiptId)],
|
||||
items,
|
||||
items: items.map((item) => {
|
||||
const receiptItem = item;
|
||||
return {
|
||||
receiptItem,
|
||||
quantity: selectedQuantites[receiptItem.id],
|
||||
category: selectedProductCategories[receiptItem.id],
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
this.#logger.info('Starting return process with returns', () => ({
|
||||
this.#logger.info("Starting return process with returns", () => ({
|
||||
processId,
|
||||
returns,
|
||||
}));
|
||||
@@ -141,7 +150,7 @@ export class ReturnDetailsComponent {
|
||||
returns,
|
||||
});
|
||||
|
||||
this._router.navigate(['../../', 'process'], {
|
||||
this._router.navigate(["../../", "process"], {
|
||||
relativeTo: this._activatedRoute,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,28 +3,28 @@ import {
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CallbackResult, ListResponseArgs } from '@isa/common/data-access';
|
||||
import { injectActivatedProcessId } from '@isa/core/process';
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { CallbackResult, ListResponseArgs } from "@isa/common/data-access";
|
||||
import { injectActivatedProcessId } from "@isa/core/process";
|
||||
import {
|
||||
ReceiptListItem,
|
||||
ReturnSearchStatus,
|
||||
ReturnSearchStore,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';
|
||||
} from "@isa/oms/data-access";
|
||||
import { ReturnTaskListComponent } from "@isa/oms/shared/task-list";
|
||||
import {
|
||||
FilterService,
|
||||
SearchBarInputComponent,
|
||||
FilterInputMenuButtonComponent,
|
||||
} from '@isa/shared/filter';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { TooltipIconComponent } from '@isa/ui/tooltip';
|
||||
} from "@isa/shared/filter";
|
||||
import { IconButtonComponent } from "@isa/ui/buttons";
|
||||
import { TooltipIconComponent } from "@isa/ui/tooltip";
|
||||
|
||||
@Component({
|
||||
selector: 'oms-feature-return-search-main',
|
||||
templateUrl: './return-search-main.component.html',
|
||||
styleUrls: ['./return-search-main.component.scss'],
|
||||
selector: "oms-feature-return-search-main",
|
||||
templateUrl: "./return-search-main.component.html",
|
||||
styleUrls: ["./return-search-main.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
SearchBarInputComponent,
|
||||
@@ -55,7 +55,7 @@ export class ReturnSearchMainComponent {
|
||||
});
|
||||
|
||||
filterInputs = computed(() =>
|
||||
this._filterService.inputs().filter((input) => input.group === 'filter'),
|
||||
this._filterService.inputs().filter((input) => input.group === "filter"),
|
||||
);
|
||||
|
||||
// TODO: Suche als Provider in FilterService auslagern (+ Cancel Search, + Fetching Status)
|
||||
@@ -77,10 +77,10 @@ export class ReturnSearchMainComponent {
|
||||
}: CallbackResult<ListResponseArgs<ReceiptListItem>>) => {
|
||||
if (data) {
|
||||
if (data.result.length === 1) {
|
||||
this.navigate(['receipt', data.result[0].id]);
|
||||
} else if (data.result.length > 1) {
|
||||
this.navigate(['receipts']);
|
||||
return this.navigate(["receipt", data.result[0].id]);
|
||||
}
|
||||
|
||||
return this.navigate(["receipts"]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<ui-client-row data-what="search-result-item" [attr.data-which]="receiptNumber()">
|
||||
<ui-client-row
|
||||
data-what="search-result-item"
|
||||
[attr.data-which]="receiptNumber()"
|
||||
>
|
||||
<ui-client-row-content>
|
||||
<h3 class="isa-text-subtitle-1-regular">{{ name() }}</h3>
|
||||
</ui-client-row-content>
|
||||
@@ -7,12 +10,12 @@
|
||||
<ui-item-row-data-label>Belegdatum</ui-item-row-data-label>
|
||||
<ui-item-row-data-value>
|
||||
<span class="isa-text-body-2-bold">
|
||||
{{ receiptDate() | date: 'dd.MM.yy' }}
|
||||
{{ receiptDate() | date: "dd.MM.yy" }}
|
||||
</span>
|
||||
</ui-item-row-data-value>
|
||||
</ui-item-row-data-row>
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label>Rechnugsnr.</ui-item-row-data-label>
|
||||
<ui-item-row-data-label>Beleg-Nr.</ui-item-row-data-label>
|
||||
<ui-item-row-data-value>
|
||||
<span class="isa-text-body-2-bold"> {{ receiptNumber() }} </span>
|
||||
</ui-item-row-data-value>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
name="isaActionEdit"
|
||||
data-what="button"
|
||||
data-which="edit-return-item"
|
||||
(click)="navigateBack()"
|
||||
[disabled]="returnItemsAndPrintReciptPending()"
|
||||
(click)="location.back()"
|
||||
></ui-icon-button>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { createRoutingFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ReturnSummaryItemComponent } from './return-summary-item.component';
|
||||
import { MockComponents, MockProvider } from 'ng-mocks';
|
||||
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
|
||||
import { createRoutingFactory, Spectator } from "@ngneat/spectator/jest";
|
||||
import { ReturnSummaryItemComponent } from "./return-summary-item.component";
|
||||
import { MockComponents, MockProvider } from "ng-mocks";
|
||||
import { ReturnProductInfoComponent } from "@isa/oms/shared/product-info";
|
||||
import {
|
||||
Product,
|
||||
ReturnProcess,
|
||||
ReturnProcessQuestionKey,
|
||||
ReturnProcessService,
|
||||
} from '@isa/oms/data-access';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { Router } from '@angular/router';
|
||||
} from "@isa/oms/data-access";
|
||||
import { NgIcon } from "@ng-icons/core";
|
||||
import { IconButtonComponent } from "@isa/ui/buttons";
|
||||
import { Location } from "@angular/common";
|
||||
|
||||
/**
|
||||
* Creates a mock ReturnProcess with default values that can be overridden
|
||||
@@ -21,20 +21,20 @@ function createMockReturnProcess(
|
||||
return {
|
||||
id: 1,
|
||||
processId: 1,
|
||||
productCategory: 'Electronics',
|
||||
productCategory: "Electronics",
|
||||
answers: {},
|
||||
receiptId: 123,
|
||||
receiptItem: {
|
||||
id: 321,
|
||||
product: {
|
||||
name: 'Test Product',
|
||||
name: "Test Product",
|
||||
},
|
||||
},
|
||||
...partial,
|
||||
} as ReturnProcess;
|
||||
}
|
||||
|
||||
describe('ReturnSummaryItemComponent', () => {
|
||||
describe("ReturnSummaryItemComponent", () => {
|
||||
let spectator: Spectator<ReturnSummaryItemComponent>;
|
||||
let returnProcessService: jest.Mocked<ReturnProcessService>;
|
||||
|
||||
@@ -48,7 +48,10 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
providers: [
|
||||
MockProvider(ReturnProcessService, {
|
||||
getReturnInfo: jest.fn(),
|
||||
eligibleForReturn: jest.fn().mockReturnValue({ state: 'eligible' }),
|
||||
eligibleForReturn: jest.fn().mockReturnValue({ state: "eligible" }),
|
||||
}),
|
||||
MockProvider(Location, {
|
||||
back: jest.fn(),
|
||||
}),
|
||||
],
|
||||
shallow: true,
|
||||
@@ -64,38 +67,38 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
spectator.detectChanges();
|
||||
});
|
||||
|
||||
describe('Component Creation', () => {
|
||||
it('should create the component', () => {
|
||||
describe("Component Creation", () => {
|
||||
it("should create the component", () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Return Information Display', () => {
|
||||
describe("Return Information Display", () => {
|
||||
const mockReturnInfo = {
|
||||
itemCondition: 'itemCondition',
|
||||
returnDetails: { [ReturnProcessQuestionKey.CaseDamaged]: 'no' },
|
||||
returnReason: 'returnReason',
|
||||
itemCondition: "itemCondition",
|
||||
returnDetails: { [ReturnProcessQuestionKey.CaseDamaged]: "no" },
|
||||
returnReason: "returnReason",
|
||||
otherProduct: {
|
||||
ean: 'ean',
|
||||
ean: "ean",
|
||||
} as Product,
|
||||
comment: 'comment',
|
||||
comment: "comment",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(returnProcessService, 'getReturnInfo')
|
||||
.spyOn(returnProcessService, "getReturnInfo")
|
||||
.mockReturnValue(mockReturnInfo);
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 2 }));
|
||||
spectator.setInput("returnProcess", createMockReturnProcess({ id: 2 }));
|
||||
spectator.detectChanges();
|
||||
});
|
||||
it('should provide correct return information array', () => {
|
||||
it("should provide correct return information array", () => {
|
||||
// Arrange
|
||||
const expectedInfos = [
|
||||
'itemCondition',
|
||||
'returnReason',
|
||||
'Gehäuse beschädigt: no',
|
||||
'Geliefert wurde: ean',
|
||||
'comment',
|
||||
"itemCondition",
|
||||
"returnReason",
|
||||
"Gehäuse beschädigt: no",
|
||||
"Geliefert wurde: ean",
|
||||
"comment",
|
||||
];
|
||||
|
||||
// Act
|
||||
@@ -105,14 +108,14 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(actualInfos).toEqual(expectedInfos);
|
||||
expect(actualInfos.length).toBe(5);
|
||||
});
|
||||
it('should render return info items with correct content', () => {
|
||||
it("should render return info items with correct content", () => {
|
||||
// Arrange
|
||||
const expectedInfos = [
|
||||
'itemCondition',
|
||||
'returnReason',
|
||||
'Gehäuse beschädigt: no',
|
||||
'Geliefert wurde: ean',
|
||||
'comment',
|
||||
"itemCondition",
|
||||
"returnReason",
|
||||
"Gehäuse beschädigt: no",
|
||||
"Geliefert wurde: ean",
|
||||
"comment",
|
||||
];
|
||||
|
||||
// Act
|
||||
@@ -125,14 +128,14 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(listItems.length).toBe(expectedInfos.length);
|
||||
listItems.forEach((item, index) => {
|
||||
expect(item).toHaveText(expectedInfos[index]);
|
||||
expect(item).toHaveAttribute('data-info-index', index.toString());
|
||||
expect(item).toHaveAttribute("data-info-index", index.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined return info gracefully', () => {
|
||||
it("should handle undefined return info gracefully", () => {
|
||||
// Arrange
|
||||
returnProcessService.getReturnInfo.mockReturnValue(undefined);
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 3 }));
|
||||
spectator.setInput("returnProcess", createMockReturnProcess({ id: 3 }));
|
||||
spectator.detectChanges();
|
||||
|
||||
// Act
|
||||
@@ -146,26 +149,26 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(listItems.length).toBe(0);
|
||||
});
|
||||
|
||||
describe('returnDetails mapping', () => {
|
||||
it('should map multiple returnDetails keys to correct info strings', () => {
|
||||
describe("returnDetails mapping", () => {
|
||||
it("should map multiple returnDetails keys to correct info strings", () => {
|
||||
const expected = [
|
||||
'itemCondition',
|
||||
'returnReason',
|
||||
'Gehäuse beschädigt: Ja',
|
||||
'Display beschädigt: Nein',
|
||||
'Geliefert wurde: ean',
|
||||
'comment',
|
||||
"itemCondition",
|
||||
"returnReason",
|
||||
"Gehäuse beschädigt: Ja",
|
||||
"Display beschädigt: Nein",
|
||||
"Geliefert wurde: ean",
|
||||
"comment",
|
||||
];
|
||||
// Arrange
|
||||
const details = {
|
||||
[ReturnProcessQuestionKey.CaseDamaged]: 'Ja',
|
||||
[ReturnProcessQuestionKey.DisplayDamaged]: 'Nein',
|
||||
[ReturnProcessQuestionKey.CaseDamaged]: "Ja",
|
||||
[ReturnProcessQuestionKey.DisplayDamaged]: "Nein",
|
||||
};
|
||||
returnProcessService.getReturnInfo.mockReturnValue({
|
||||
...mockReturnInfo,
|
||||
returnDetails: details,
|
||||
});
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 4 }));
|
||||
spectator.setInput("returnProcess", createMockReturnProcess({ id: 4 }));
|
||||
spectator.detectChanges();
|
||||
|
||||
// Act
|
||||
@@ -173,31 +176,31 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(infos).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not include returnDetails if empty', () => {
|
||||
it("should not include returnDetails if empty", () => {
|
||||
// Arrange
|
||||
returnProcessService.getReturnInfo.mockReturnValue({
|
||||
...mockReturnInfo,
|
||||
returnDetails: {},
|
||||
});
|
||||
spectator.setInput('returnProcess', createMockReturnProcess({ id: 5 }));
|
||||
spectator.setInput("returnProcess", createMockReturnProcess({ id: 5 }));
|
||||
spectator.detectChanges();
|
||||
|
||||
// Act
|
||||
const infos = spectator.component.returnInfos();
|
||||
|
||||
// Assert
|
||||
expect(infos.some((info) => info.includes('Gehäuse beschädigt'))).toBe(
|
||||
expect(infos.some((info) => info.includes("Gehäuse beschädigt"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(infos.some((info) => info.includes('Zubehör fehlt'))).toBe(
|
||||
expect(infos.some((info) => info.includes("Zubehör fehlt"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navigation', () => {
|
||||
it('should render edit button with correct attributes', () => {
|
||||
describe("Navigation", () => {
|
||||
it("should render edit button with correct attributes", () => {
|
||||
// Assert
|
||||
const editButton = spectator.query(
|
||||
'[data-what="button"][data-which="edit-return-item"]',
|
||||
@@ -205,7 +208,7 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
expect(editButton).toExist();
|
||||
});
|
||||
|
||||
it('should navigate back when edit button is clicked', () => {
|
||||
it("should navigate back when edit button is clicked", () => {
|
||||
// Arrange
|
||||
const editButton = spectator.query(
|
||||
'[data-what="button"][data-which="edit-return-item"]',
|
||||
@@ -217,25 +220,20 @@ describe('ReturnSummaryItemComponent', () => {
|
||||
}
|
||||
|
||||
// Assert
|
||||
expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(
|
||||
['..'],
|
||||
expect.objectContaining({
|
||||
relativeTo: expect.anything(),
|
||||
}),
|
||||
);
|
||||
expect(spectator.inject(Location).back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the product info component', () => {
|
||||
it("should render the product info component", () => {
|
||||
const productInfo = spectator.query(ReturnProductInfoComponent);
|
||||
expect(productInfo).toExist();
|
||||
});
|
||||
|
||||
it('should compute eligibility state as eligible', () => {
|
||||
it("should compute eligibility state as eligible", () => {
|
||||
(returnProcessService.eligibleForReturn as jest.Mock).mockReturnValue({
|
||||
state: 'eligible',
|
||||
state: "eligible",
|
||||
});
|
||||
spectator.detectChanges();
|
||||
expect(spectator.component.eligibleForReturn()?.state).toBe('eligible');
|
||||
expect(spectator.component.eligibleForReturn()?.state).toBe("eligible");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
} from "@angular/core";
|
||||
import { Location } from "@angular/common";
|
||||
import {
|
||||
isaActionChevronRight,
|
||||
isaActionClose,
|
||||
isaActionEdit,
|
||||
} from '@isa/icons';
|
||||
} from "@isa/icons";
|
||||
import {
|
||||
EligibleForReturn,
|
||||
EligibleForReturnState,
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
ReturnProcessService,
|
||||
ProductCategory,
|
||||
returnDetailsMapping,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
} from "@isa/oms/data-access";
|
||||
import { ReturnProductInfoComponent } from "@isa/oms/shared/product-info";
|
||||
import { IconButtonComponent } from "@isa/ui/buttons";
|
||||
import { NgIcon, provideIcons } from "@ng-icons/core";
|
||||
|
||||
/**
|
||||
* Displays a single item in the return process summary, showing product details
|
||||
@@ -47,30 +47,34 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'oms-feature-return-summary-item',
|
||||
templateUrl: './return-summary-item.component.html',
|
||||
styleUrls: ['./return-summary-item.component.scss'],
|
||||
selector: "oms-feature-return-summary-item",
|
||||
templateUrl: "./return-summary-item.component.html",
|
||||
styleUrls: ["./return-summary-item.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ReturnProductInfoComponent, NgIcon, IconButtonComponent],
|
||||
providers: [
|
||||
provideIcons({ isaActionChevronRight, isaActionEdit, isaActionClose }),
|
||||
],
|
||||
host: {
|
||||
'data-what': 'list-item',
|
||||
'data-which': 'return-process-item',
|
||||
'[attr.data-receipt-id]': 'returnProcess()?.receiptId',
|
||||
'[attr.data-return-item-id]': 'returnProcess()?.returnItem?.id',
|
||||
"data-what": "list-item",
|
||||
"data-which": "return-process-item",
|
||||
"[attr.data-receipt-id]": "returnProcess()?.receiptId",
|
||||
"[attr.data-return-item-id]": "returnProcess()?.returnItem?.id",
|
||||
},
|
||||
})
|
||||
export class ReturnSummaryItemComponent {
|
||||
EligibleForReturnState = EligibleForReturnState;
|
||||
#returnProcessService = inject(ReturnProcessService);
|
||||
#router = inject(Router);
|
||||
#activatedRoute = inject(ActivatedRoute);
|
||||
|
||||
/** Angular Location service for navigation */
|
||||
location = inject(Location);
|
||||
|
||||
/** The return process object containing all information about the return */
|
||||
returnProcess = input.required<ReturnProcess>();
|
||||
|
||||
/** The status of the return items and print receipt operation */
|
||||
returnItemsAndPrintReciptPending = input<boolean>(false);
|
||||
|
||||
/**
|
||||
* Computes whether the current return process is eligible for return.
|
||||
*
|
||||
@@ -149,8 +153,4 @@ export class ReturnSummaryItemComponent {
|
||||
// remove duplicates
|
||||
return Array.from(new Set(result));
|
||||
});
|
||||
|
||||
navigateBack() {
|
||||
this.#router.navigate(['..'], { relativeTo: this.#activatedRoute });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
color="tertiary"
|
||||
size="small"
|
||||
class="px-[0.875rem] py-1 min-w-0 bg-white gap-1 absolute top-0 left-0"
|
||||
[disabled]="returnItemsAndPrintReciptStatusPending()"
|
||||
(click)="location.back()"
|
||||
>
|
||||
<ng-icon name="isaActionChevronLeft" size="1.5rem" class="-ml-2"></ng-icon>
|
||||
@@ -28,19 +29,22 @@
|
||||
data-which="return-process-item"
|
||||
[attr.data-item-id]="item.id"
|
||||
[attr.data-item-category]="item.productCategory"
|
||||
[returnItemsAndPrintReciptPending]="
|
||||
returnItemsAndPrintReciptStatusPending()
|
||||
"
|
||||
></oms-feature-return-summary-item>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-6 text-center">
|
||||
@if (returnItemsAndPrintReciptStatus() !== 'success') {
|
||||
@if (returnItemsAndPrintReciptStatus() !== "success") {
|
||||
<button
|
||||
type="button"
|
||||
size="large"
|
||||
uiButton
|
||||
color="brand"
|
||||
(click)="returnItemsAndPrintRecipt()"
|
||||
[pending]="returnItemsAndPrintReciptStatus() === 'pending'"
|
||||
[disabled]="returnItemsAndPrintReciptStatus() === 'pending'"
|
||||
[pending]="returnItemsAndPrintReciptStatusPending()"
|
||||
[disabled]="returnItemsAndPrintReciptStatusPending()"
|
||||
data-what="button"
|
||||
data-which="return-and-print"
|
||||
>
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { ReturnSummaryComponent } from './return-summary.component';
|
||||
import { createComponentFactory, Spectator } from "@ngneat/spectator/jest";
|
||||
import { MockComponent } from "ng-mocks";
|
||||
import { ReturnSummaryComponent } from "./return-summary.component";
|
||||
import {
|
||||
ReturnProcess,
|
||||
ReturnProcessService,
|
||||
ReturnProcessStore,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ReturnSummaryItemComponent } from './return-summary-item/return-summary-item.component';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
} from "@isa/oms/data-access";
|
||||
import { ReturnSummaryItemComponent } from "./return-summary-item/return-summary-item.component";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
jest.mock('@isa/core/process', () => ({
|
||||
jest.mock("@isa/core/process", () => ({
|
||||
injectActivatedProcessId: () => jest.fn(() => 1),
|
||||
}));
|
||||
|
||||
const MOCK_RETURN_PROCESSES: ReturnProcess[] = [
|
||||
{ id: 1, processId: 1, productCategory: 'Electronics' } as ReturnProcess,
|
||||
{ id: 2, processId: 1, productCategory: 'Books' } as ReturnProcess,
|
||||
{ id: 3, processId: 2, productCategory: 'Clothing' } as ReturnProcess,
|
||||
{ id: 4, processId: 2, productCategory: 'Home Goods' } as ReturnProcess,
|
||||
{ id: 5, processId: 3, productCategory: 'Electronics' } as ReturnProcess,
|
||||
{ id: 6, processId: 3, productCategory: 'Toys' } as ReturnProcess,
|
||||
{ id: 7, processId: 4, productCategory: 'Books' } as ReturnProcess,
|
||||
{ id: 8, processId: 4, productCategory: 'Garden' } as ReturnProcess,
|
||||
{ id: 1, processId: 1, productCategory: "Electronics" } as ReturnProcess,
|
||||
{ id: 2, processId: 1, productCategory: "Books" } as ReturnProcess,
|
||||
{ id: 3, processId: 2, productCategory: "Clothing" } as ReturnProcess,
|
||||
{ id: 4, processId: 2, productCategory: "Home Goods" } as ReturnProcess,
|
||||
{ id: 5, processId: 3, productCategory: "Electronics" } as ReturnProcess,
|
||||
{ id: 6, processId: 3, productCategory: "Toys" } as ReturnProcess,
|
||||
{ id: 7, processId: 4, productCategory: "Books" } as ReturnProcess,
|
||||
{ id: 8, processId: 4, productCategory: "Garden" } as ReturnProcess,
|
||||
];
|
||||
|
||||
describe('ReturnSummaryComponent', () => {
|
||||
describe("ReturnSummaryComponent", () => {
|
||||
let spectator: Spectator<ReturnSummaryComponent>;
|
||||
// Use createComponentFactory for standalone components
|
||||
const createComponent = createComponentFactory({
|
||||
@@ -47,17 +47,17 @@ describe('ReturnSummaryComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
it("should create the component", () => {
|
||||
// Assert: Check if the component instance was created successfully
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a defined processId', () => {
|
||||
it("should have a defined processId", () => {
|
||||
// Assert: Check if the processId is defined
|
||||
expect(spectator.component.processId()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have two return processes', () => {
|
||||
it("should have two return processes", () => {
|
||||
// Arrange:
|
||||
const mockReturnProcesses: ReturnProcess[] = [
|
||||
MOCK_RETURN_PROCESSES[0],
|
||||
@@ -72,14 +72,14 @@ describe('ReturnSummaryComponent', () => {
|
||||
expect(returnProcesses).toEqual(mockReturnProcesses);
|
||||
});
|
||||
|
||||
it('should render the return summary item component', () => {
|
||||
it("should render the return summary item component", () => {
|
||||
// Arrange:
|
||||
const mockReturnProcesses: ReturnProcess[] = [
|
||||
MOCK_RETURN_PROCESSES[0],
|
||||
MOCK_RETURN_PROCESSES[1],
|
||||
];
|
||||
jest
|
||||
.spyOn(spectator.component, 'returnProcesses')
|
||||
.spyOn(spectator.component, "returnProcesses")
|
||||
.mockReturnValue(mockReturnProcesses);
|
||||
|
||||
// Act: Trigger change detection
|
||||
@@ -91,14 +91,14 @@ describe('ReturnSummaryComponent', () => {
|
||||
expect(returnSummaryItems.length).toBe(mockReturnProcesses.length);
|
||||
});
|
||||
|
||||
it('should set the returnProcess input correctly', () => {
|
||||
it("should set the returnProcess input correctly", () => {
|
||||
// Arrange:
|
||||
const mockReturnProcesses: ReturnProcess[] = [
|
||||
MOCK_RETURN_PROCESSES[0],
|
||||
MOCK_RETURN_PROCESSES[1],
|
||||
];
|
||||
jest
|
||||
.spyOn(spectator.component, 'returnProcesses')
|
||||
.spyOn(spectator.component, "returnProcesses")
|
||||
.mockReturnValue(mockReturnProcesses);
|
||||
|
||||
// Act: Trigger change detection
|
||||
@@ -110,7 +110,7 @@ describe('ReturnSummaryComponent', () => {
|
||||
expect(returnSummaryItems[1].returnProcess).toEqual(mockReturnProcesses[1]);
|
||||
});
|
||||
|
||||
it('should have proper E2E testing attributes', () => {
|
||||
it("should have proper E2E testing attributes", () => {
|
||||
// Arrange
|
||||
const mockReturnProcesses: ReturnProcess[] = [
|
||||
MOCK_RETURN_PROCESSES[0], // id: 1, processId: 1, category: 'Electronics'
|
||||
@@ -118,7 +118,7 @@ describe('ReturnSummaryComponent', () => {
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(spectator.component, 'returnProcesses')
|
||||
.spyOn(spectator.component, "returnProcesses")
|
||||
.mockReturnValue(mockReturnProcesses);
|
||||
|
||||
// Act
|
||||
@@ -128,57 +128,57 @@ describe('ReturnSummaryComponent', () => {
|
||||
// Check heading attributes
|
||||
const heading = spectator.query('[data-what="heading"]');
|
||||
expect(heading).toBeTruthy();
|
||||
expect(heading).toHaveAttribute('data-which', 'return-summary-title');
|
||||
expect(heading).toHaveAttribute("data-which", "return-summary-title");
|
||||
|
||||
// Check container attributes
|
||||
const container = spectator.query('[data-what="container"]');
|
||||
expect(container).toBeTruthy();
|
||||
expect(container).toHaveAttribute('data-which', 'return-items-list');
|
||||
expect(container).toHaveAttribute("data-which", "return-items-list");
|
||||
|
||||
// Check list item attributes
|
||||
const listItems = spectator.queryAll('oms-feature-return-summary-item');
|
||||
const listItems = spectator.queryAll("oms-feature-return-summary-item");
|
||||
expect(listItems.length).toBe(mockReturnProcesses.length);
|
||||
|
||||
// Check attributes for the first item
|
||||
expect(listItems[0]).toHaveAttribute('data-what', 'list-item');
|
||||
expect(listItems[0]).toHaveAttribute('data-which', 'return-process-item');
|
||||
expect(listItems[0]).toHaveAttribute("data-what", "list-item");
|
||||
expect(listItems[0]).toHaveAttribute("data-which", "return-process-item");
|
||||
expect(listItems[0]).toHaveAttribute(
|
||||
'data-item-id',
|
||||
"data-item-id",
|
||||
`${mockReturnProcesses[0].id}`,
|
||||
);
|
||||
|
||||
expect(listItems[0]).toHaveAttribute(
|
||||
'data-item-category',
|
||||
"data-item-category",
|
||||
mockReturnProcesses[0].productCategory,
|
||||
);
|
||||
|
||||
// Check attributes for the second item
|
||||
expect(listItems[1]).toHaveAttribute('data-what', 'list-item');
|
||||
expect(listItems[1]).toHaveAttribute('data-which', 'return-process-item');
|
||||
expect(listItems[1]).toHaveAttribute("data-what", "list-item");
|
||||
expect(listItems[1]).toHaveAttribute("data-which", "return-process-item");
|
||||
expect(listItems[1]).toHaveAttribute(
|
||||
'data-item-id',
|
||||
"data-item-id",
|
||||
`${mockReturnProcesses[1].id}`,
|
||||
);
|
||||
expect(listItems[1]).toHaveAttribute(
|
||||
'data-item-category',
|
||||
"data-item-category",
|
||||
`${mockReturnProcesses[1].productCategory}`,
|
||||
);
|
||||
expect(listItems[1]).toHaveAttribute(
|
||||
'data-item-category',
|
||||
"data-item-category",
|
||||
mockReturnProcesses[1].productCategory,
|
||||
);
|
||||
|
||||
// Check button attributes
|
||||
const button = spectator.query('[data-what="button"]');
|
||||
expect(button).toBeTruthy();
|
||||
expect(button).toHaveAttribute('data-which', 'return-and-print');
|
||||
expect(button).toHaveAttribute("data-which", "return-and-print");
|
||||
});
|
||||
|
||||
it('should call returnItemsAndPrintRecipt when button is clicked', () => {
|
||||
it("should call returnItemsAndPrintRecipt when button is clicked", () => {
|
||||
// Arrange: Spy on the returnItemsAndPrintRecipt method
|
||||
const returnItemsAndPrintReciptSpy = jest.spyOn(
|
||||
spectator.component,
|
||||
'returnItemsAndPrintRecipt',
|
||||
"returnItemsAndPrintRecipt",
|
||||
);
|
||||
|
||||
// Act: Trigger button click
|
||||
|
||||
@@ -4,20 +4,20 @@ import {
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { ReturnSummaryItemComponent } from './return-summary-item/return-summary-item.component';
|
||||
import { injectActivatedProcessId } from '@isa/core/process';
|
||||
} from "@angular/core";
|
||||
import { ReturnSummaryItemComponent } from "./return-summary-item/return-summary-item.component";
|
||||
import { injectActivatedProcessId } from "@isa/core/process";
|
||||
import {
|
||||
ReturnProcess,
|
||||
ReturnProcessService,
|
||||
ReturnProcessStore,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { isaActionChevronLeft } from '@isa/icons';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
} from "@isa/oms/data-access";
|
||||
import { ButtonComponent } from "@isa/ui/buttons";
|
||||
import { NgIcon, provideIcons } from "@ng-icons/core";
|
||||
import { Location } from "@angular/common";
|
||||
import { isaActionChevronLeft } from "@isa/icons";
|
||||
import { logger } from "@isa/core/logging";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
/**
|
||||
* Main component for the return summary feature. Displays a list of items being returned
|
||||
@@ -35,9 +35,9 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'oms-feature-return-summary',
|
||||
templateUrl: './return-summary.component.html',
|
||||
styleUrls: ['./return-summary.component.scss'],
|
||||
selector: "oms-feature-return-summary",
|
||||
templateUrl: "./return-summary.component.html",
|
||||
styleUrls: ["./return-summary.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ReturnSummaryItemComponent, ButtonComponent, NgIcon],
|
||||
providers: [provideIcons({ isaActionChevronLeft })],
|
||||
@@ -74,9 +74,21 @@ export class ReturnSummaryComponent {
|
||||
* - 'error': Operation failed
|
||||
*/
|
||||
returnItemsAndPrintReciptStatus = signal<
|
||||
undefined | 'pending' | 'success' | 'error'
|
||||
undefined | "pending" | "success" | "error"
|
||||
>(undefined);
|
||||
|
||||
/**
|
||||
* Computed signal to determine if the return items and print receipt operation is pending.
|
||||
*
|
||||
* This signal checks the current status of the returnItemsAndPrintReciptStatus signal
|
||||
* and returns true if the status is 'pending', otherwise false.
|
||||
*
|
||||
* @returns {boolean} True if the operation is pending, false otherwise
|
||||
*/
|
||||
returnItemsAndPrintReciptStatusPending = computed(() => {
|
||||
return this.returnItemsAndPrintReciptStatus() === "pending";
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the return and print process for multiple items.
|
||||
*
|
||||
@@ -93,35 +105,35 @@ export class ReturnSummaryComponent {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async returnItemsAndPrintRecipt() {
|
||||
if (this.returnItemsAndPrintReciptStatus() === 'pending') {
|
||||
this.#logger.warn('Return process already in progress', () => ({
|
||||
function: 'returnItemsAndPrintRecipt',
|
||||
if (this.returnItemsAndPrintReciptStatus() === "pending") {
|
||||
this.#logger.warn("Return process already in progress", () => ({
|
||||
function: "returnItemsAndPrintRecipt",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.returnItemsAndPrintReciptStatus.set('pending');
|
||||
this.returnItemsAndPrintReciptStatus.set("pending");
|
||||
|
||||
const returnReceipts =
|
||||
await this.#returnProcessService.completeReturnProcess(
|
||||
this.returnProcesses(),
|
||||
);
|
||||
|
||||
this.#logger.info('Return receipts created', () => ({
|
||||
this.#logger.info("Return receipts created", () => ({
|
||||
count: returnReceipts.length,
|
||||
}));
|
||||
this.returnItemsAndPrintReciptStatus.set('success');
|
||||
this.returnItemsAndPrintReciptStatus.set("success");
|
||||
|
||||
this.#returnProcessStore.finishProcess(returnReceipts);
|
||||
await this.#router.navigate(['../', 'review'], {
|
||||
await this.#router.navigate(["../", "review"], {
|
||||
relativeTo: this.#activatedRoute,
|
||||
});
|
||||
} catch (error) {
|
||||
this.#logger.error('Error completing return process', error, () => ({
|
||||
function: 'returnItemsAndPrintRecipt',
|
||||
this.#logger.error("Error completing return process", error, () => ({
|
||||
function: "returnItemsAndPrintRecipt",
|
||||
}));
|
||||
this.returnItemsAndPrintReciptStatus.set('error');
|
||||
this.returnItemsAndPrintReciptStatus.set("error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,35 @@
|
||||
}
|
||||
|
||||
.oms-shared-return-task-list-item__review {
|
||||
@apply grid grid-cols-[1fr,1fr] desktop:grid-cols-[1fr,1fr,minmax(20rem,auto)] gap-x-6 py-6 text-isa-secondary-900 items-center border-b border-solid border-isa-neutral-300 last:pb-0 last:border-none;
|
||||
@apply grid grid-cols-[1fr,1fr] desktop:grid-cols-[1fr,1fr,minmax(20rem,auto)] gap-x-6 desktop:gap-y-6 py-6 text-isa-secondary-900 items-center border-b border-solid border-isa-neutral-300 last:pb-0 last:border-none;
|
||||
|
||||
&:has(.task-unknown-actions):has(.tolino-print-cta) {
|
||||
.tolino-print-cta {
|
||||
@apply desktop:justify-self-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
grid-template-areas:
|
||||
'product infos'
|
||||
'unknown-comment actions';
|
||||
"product infos"
|
||||
"unknown-comment actions";
|
||||
|
||||
.tolino-print-cta,
|
||||
.task-unknown-actions {
|
||||
@apply mt-6 desktop:mt-0;
|
||||
grid-area: actions;
|
||||
}
|
||||
|
||||
&:has(.task-unknown-actions):has(.tolino-print-cta) {
|
||||
.tolino-print-cta {
|
||||
@apply mt-0 self-start;
|
||||
grid-area: print;
|
||||
}
|
||||
|
||||
grid-template-areas:
|
||||
"product print"
|
||||
"unknown-comment actions";
|
||||
}
|
||||
|
||||
.product-info {
|
||||
grid-area: product;
|
||||
@@ -18,12 +41,6 @@
|
||||
grid-area: infos;
|
||||
}
|
||||
|
||||
.tolino-print-cta,
|
||||
.task-unknown-actions {
|
||||
@apply mt-6 desktop:mt-0;
|
||||
grid-area: actions;
|
||||
}
|
||||
|
||||
.processing-comment-unknown {
|
||||
@apply mt-6 desktop:mt-0;
|
||||
grid-area: unknown-comment;
|
||||
@@ -48,7 +65,7 @@
|
||||
}
|
||||
|
||||
.task-unknown-actions {
|
||||
@apply flex flex-row gap-3 h-full py-2 items-center;
|
||||
@apply flex flex-row gap-3 h-full py-2 items-center justify-self-end;
|
||||
}
|
||||
|
||||
.processing-comment {
|
||||
|
||||
@@ -17,7 +17,9 @@ class MockConfig {
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('scandit-web-datacapture-core');
|
||||
jest.mock('scandit-web-datacapture-core', () => ({
|
||||
configure: jest.fn(),
|
||||
}));
|
||||
jest.mock('scandit-web-datacapture-barcode', () => ({
|
||||
barcodeCaptureLoader: jest.fn(() => 'mockedBarcodeLoader'),
|
||||
}));
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
justify-content: space-between;
|
||||
|
||||
ng-icon {
|
||||
@apply size-6;
|
||||
@apply min-w-5 size-5 mt-[0.125rem];
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@@ -58,6 +58,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ui-dropdown__text {
|
||||
@apply overflow-hidden text-ellipsis whitespace-nowrap;
|
||||
}
|
||||
|
||||
.ui-dropdown__options {
|
||||
// Fixed typo from ui-dorpdown__options
|
||||
display: inline-flex;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<span>{{ viewLabel() }}</span>
|
||||
<span [class]="['ui-dropdown__text']">{{ viewLabel() }}</span>
|
||||
<ng-icon [name]="isOpenIcon()"></ng-icon>
|
||||
|
||||
<ng-template
|
||||
|
||||
4432
package-lock.json
generated
4432
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -124,6 +124,7 @@
|
||||
"overrides": {
|
||||
"jest-environment-jsdom": {
|
||||
"jsdom": "26.0.0"
|
||||
}
|
||||
},
|
||||
"stylus": "github:stylus/stylus#0.64.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user