Merge branch 'release/4.1' into develop

This commit is contained in:
Lorenz Hilpert
2025-09-19 10:31:50 +02:00
30 changed files with 799 additions and 463 deletions

View File

@@ -159,12 +159,12 @@ const routes: Routes = [
import('@page/goods-in').then((m) => m.GoodsInModule),
canActivate: [CanActivateGoodsInGuard],
},
{
path: 'remission',
loadChildren: () =>
import('@page/remission').then((m) => m.PageRemissionModule),
canActivate: [CanActivateRemissionGuard],
},
// {
// path: 'remission',
// loadChildren: () =>
// import('@page/remission').then((m) => m.PageRemissionModule),
// canActivate: [CanActivateRemissionGuard],
// },
{
path: 'package-inspection',
loadChildren: () =>

View File

@@ -1,10 +1,18 @@
<div class="notification-list scroll-bar">
@for (notification of notifications; track notification) {
<modal-notifications-list-item [item]="notification" (itemSelected)="itemSelected($event)"></modal-notifications-list-item>
<modal-notifications-list-item
[item]="notification"
(itemSelected)="itemSelected($event)"
></modal-notifications-list-item>
<hr />
}
</div>
<div class="actions">
<a class="cta-primary" [routerLink]="['/filiale/remission/create']" (click)="navigated.emit()">Zur Remission</a>
<a
class="cta-primary"
[routerLink]="remissionPath()"
(click)="navigated.emit()"
>Zur Remission</a
>
</div>

View File

@@ -1,8 +1,17 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
inject,
linkedSignal,
} from '@angular/core';
import { Router } from '@angular/router';
import { PickupShelfInNavigationService } from '@shared/services/navigation';
import { UiFilter } from '@ui/filter';
import { MessageBoardItemDTO } from '@hub/notifications';
import { TabService } from '@isa/core/tabs';
@Component({
selector: 'modal-notifications-remission-group',
@@ -11,7 +20,10 @@ import { MessageBoardItemDTO } from '@hub/notifications';
standalone: false,
})
export class ModalNotificationsRemissionGroupComponent {
private _pickupShelfInNavigationService = inject(PickupShelfInNavigationService);
tabService = inject(TabService);
private _pickupShelfInNavigationService = inject(
PickupShelfInNavigationService,
);
@Input()
notifications: MessageBoardItemDTO[];
@@ -19,11 +31,19 @@ export class ModalNotificationsRemissionGroupComponent {
@Output()
navigated = new EventEmitter<void>();
remissionPath = linkedSignal(() => [
'/',
this.tabService.activatedTab()?.id || this.tabService.nextId(),
'remission',
]);
constructor(private _router: Router) {}
itemSelected(item: MessageBoardItemDTO) {
const defaultNav = this._pickupShelfInNavigationService.listRoute();
const queryParams = UiFilter.getQueryParamsFromQueryTokenDTO(item.queryToken);
const queryParams = UiFilter.getQueryParamsFromQueryTokenDTO(
item.queryToken,
);
this._router.navigate(defaultNav.path, {
queryParams: {
...defaultNav.queryParams,

View File

@@ -16,13 +16,34 @@
[deltaEnd]="150"
[itemLength]="itemLength$ | async"
[containerHeight]="24.5"
>
@for (bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn; track bueryNumberGroup) {
>
@for (
bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn;
track bueryNumberGroup
) {
<shared-goods-in-out-order-group>
@for (orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn; track orderNumberGroup; let lastOrderNumber = $last) {
@for (processingStatusGroup of orderNumberGroup.items | groupBy: byProcessingStatusFn; track processingStatusGroup; let lastProcessingStatus = $last) {
@for (compartmentCodeGroup of processingStatusGroup.items | groupBy: byCompartmentCodeFn; track compartmentCodeGroup; let lastCompartmentCode = $last) {
@for (item of compartmentCodeGroup.items; track item; let firstItem = $first) {
@for (
orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn;
track orderNumberGroup;
let lastOrderNumber = $last
) {
@for (
processingStatusGroup of orderNumberGroup.items
| groupBy: byProcessingStatusFn;
track processingStatusGroup;
let lastProcessingStatus = $last
) {
@for (
compartmentCodeGroup of processingStatusGroup.items
| groupBy: byCompartmentCodeFn;
track compartmentCodeGroup;
let lastCompartmentCode = $last
) {
@for (
item of compartmentCodeGroup.items;
track item;
let firstItem = $first
) {
<shared-goods-in-out-order-group-item
[item]="item"
[showCompartmentCode]="firstItem"
@@ -49,7 +70,6 @@
<div class="empty-message">Es sind im Moment keine Artikel vorhanden</div>
}
<div class="actions">
@if (actions$ | async; as actions) {
@for (action of actions; track action) {
@@ -57,19 +77,27 @@
[disabled]="(changeActionLoader$ | async) || (loading$ | async)"
class="cta-action cta-action-primary"
(click)="handleAction(action)"
>
<ui-spinner
[show]="(changeActionLoader$ | async) || (loading$ | async)"
>{{ action.label }}</ui-spinner
>
<ui-spinner [show]="(changeActionLoader$ | async) || (loading$ | async)">{{ action.label }}</ui-spinner>
</button>
}
}
@if (listEmpty$ | async) {
<a class="cta-action cta-action-secondary" [routerLink]="['/filiale', 'goods', 'in']">
<a
class="cta-action cta-action-secondary"
[routerLink]="['/filiale', 'goods', 'in']"
>
Zur Bestellpostensuche
</a>
}
@if (listEmpty$ | async) {
<a class="cta-action cta-action-primary" [routerLink]="['/filiale', 'remission']">Zur Remission</a>
<a class="cta-action cta-action-primary" [routerLink]="remissionPath()"
>Zur Remission</a
>
}
</div>

View File

@@ -1,7 +1,18 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
OnDestroy,
OnInit,
ViewChild,
inject,
linkedSignal,
} from '@angular/core';
import { Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@generated/swagger/oms-api';
import {
KeyValueDTOOfStringAndString,
OrderItemListItemDTO,
} from '@generated/swagger/oms-api';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { UiScrollContainerComponent } from '@ui/scroll-container';
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
@@ -11,6 +22,7 @@ import { Config } from '@core/config';
import { ToasterService } from '@shared/shell';
import { PickupShelfInNavigationService } from '@shared/services/navigation';
import { CacheService } from '@core/cache';
import { TabService } from '@isa/core/tabs';
@Component({
selector: 'page-goods-in-remission-preview',
@@ -21,8 +33,12 @@ import { CacheService } from '@core/cache';
standalone: false,
})
export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
private _pickupShelfInNavigationService = inject(PickupShelfInNavigationService);
@ViewChild(UiScrollContainerComponent) scrollContainer: UiScrollContainerComponent;
tabService = inject(TabService);
private _pickupShelfInNavigationService = inject(
PickupShelfInNavigationService,
);
@ViewChild(UiScrollContainerComponent)
scrollContainer: UiScrollContainerComponent;
items$ = this._store.results$;
@@ -50,10 +66,18 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
byProcessingStatusFn = (item: OrderItemListItemDTO) => item.processingStatus;
byCompartmentCodeFn = (item: OrderItemListItemDTO) =>
item.compartmentInfo ? `${item.compartmentCode}_${item.compartmentInfo}` : item.compartmentCode;
item.compartmentInfo
? `${item.compartmentCode}_${item.compartmentInfo}`
: item.compartmentCode;
private readonly SCROLL_POSITION_TOKEN = 'REMISSION_PREVIEW_SCROLL_POSITION';
remissionPath = linkedSignal(() => [
'/',
this.tabService.activatedTab()?.id || this.tabService.nextId(),
'remission',
]);
constructor(
private _breadcrumb: BreadcrumbService,
private _store: GoodsInRemissionPreviewStore,
@@ -78,12 +102,18 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
}
private _removeScrollPositionFromCache(): void {
this._cache.delete({ processId: this._config.get('process.ids.goodsIn'), token: this.SCROLL_POSITION_TOKEN });
this._cache.delete({
processId: this._config.get('process.ids.goodsIn'),
token: this.SCROLL_POSITION_TOKEN,
});
}
private _addScrollPositionToCache(): void {
this._cache.set<number>(
{ processId: this._config.get('process.ids.goodsIn'), token: this.SCROLL_POSITION_TOKEN },
{
processId: this._config.get('process.ids.goodsIn'),
token: this.SCROLL_POSITION_TOKEN,
},
this.scrollContainer?.scrollPos,
);
}
@@ -108,7 +138,10 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
async updateBreadcrumb() {
const crumbs = await this._breadcrumb
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), ['goods-in', 'preview'])
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
'goods-in',
'preview',
])
.pipe(first())
.toPromise();
for (const crumb of crumbs) {
@@ -120,12 +153,15 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
async removeBreadcrumbs() {
let breadcrumbsToDelete = await this._breadcrumb
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), ['goods-in'])
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
'goods-in',
])
.pipe(first())
.toPromise();
breadcrumbsToDelete = breadcrumbsToDelete.filter(
(crumb) => !crumb.tags.includes('preview') && !crumb.tags.includes('main'),
(crumb) =>
!crumb.tags.includes('preview') && !crumb.tags.includes('main'),
);
breadcrumbsToDelete.forEach((crumb) => {
@@ -133,11 +169,17 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
});
const detailsCrumbs = await this._breadcrumb
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), ['goods-in', 'details'])
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
'goods-in',
'details',
])
.pipe(first())
.toPromise();
const editCrumbs = await this._breadcrumb
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), ['goods-in', 'edit'])
.getBreadcrumbsByKeyAndTags$(this._config.get('process.ids.goodsIn'), [
'goods-in',
'edit',
])
.pipe(first())
.toPromise();
@@ -152,32 +194,44 @@ export class GoodsInRemissionPreviewComponent implements OnInit, OnDestroy {
initInitialSearch() {
if (this._store.hits === 0) {
this._store.searchResult$.pipe(takeUntil(this._onDestroy$)).subscribe(async (result) => {
await this.createBreadcrumb();
this._store.searchResult$
.pipe(takeUntil(this._onDestroy$))
.subscribe(async (result) => {
await this.createBreadcrumb();
this.scrollContainer?.scrollTo((await this._getScrollPositionFromCache()) ?? 0);
this._removeScrollPositionFromCache();
});
this.scrollContainer?.scrollTo(
(await this._getScrollPositionFromCache()) ?? 0,
);
this._removeScrollPositionFromCache();
});
}
this._store.search();
}
async navigateToRemission() {
await this._router.navigate(['/filiale/remission']);
await this._router.navigate(this.remissionPath());
}
navigateToDetails(orderItem: OrderItemListItemDTO) {
const nav = this._pickupShelfInNavigationService.detailRoute({ item: orderItem, side: false });
const nav = this._pickupShelfInNavigationService.detailRoute({
item: orderItem,
side: false,
});
this._router.navigate(nav.path, { queryParams: { ...nav.queryParams, view: 'remission' } });
this._router.navigate(nav.path, {
queryParams: { ...nav.queryParams, view: 'remission' },
});
}
async handleAction(action: KeyValueDTOOfStringAndString) {
this.changeActionLoader$.next(true);
try {
const response = await this._store.createRemissionFromPreview().pipe(first()).toPromise();
const response = await this._store
.createRemissionFromPreview()
.pipe(first())
.toPromise();
if (!response?.dialog) {
this._toast.open({

View File

@@ -268,35 +268,6 @@
</div>
</div>
@if (remissionNavigation$ | async; as remissionNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="remissionNavigation.path"
[queryParams]="remissionNavigation.queryParams"
routerLinkActive="active"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="assignment-return"></shared-icon>
</span>
<span class="side-menu-group-item-label">Remission</span>
</a>
}
@if (packageInspectionNavigation$ | async; as packageInspectionNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); fetchAndOpenPackages()"
[routerLink]="packageInspectionNavigation.path"
[queryParams]="packageInspectionNavigation.queryParams"
routerLinkActive="active"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="clipboard-check-outline"></shared-icon>
</span>
<span class="side-menu-group-item-label">Wareneingang</span>
</a>
}
<div class="side-menu-group-sub-item-wrapper">
<a
class="side-menu-group-item"
@@ -353,5 +324,20 @@
</div>
}
</div>
@if (packageInspectionNavigation$ | async; as packageInspectionNavigation) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); fetchAndOpenPackages()"
[routerLink]="packageInspectionNavigation.path"
[queryParams]="packageInspectionNavigation.queryParams"
routerLinkActive="active"
>
<span class="side-menu-group-item-icon">
<shared-icon icon="clipboard-check-outline"></shared-icon>
</span>
<span class="side-menu-group-item-label">Wareneingang</span>
</a>
}
</nav>
</div>

View File

@@ -221,14 +221,6 @@ export class ShellSideMenuComponent {
// this._pickUpShelfInNavigation.listRoute()
// );
remissionNavigation$ = this.getLastNavigationByProcessId(
this.#config.get('process.ids.remission'),
{
path: ['/filiale', 'remission'],
queryParams: {},
},
);
packageInspectionNavigation$ = this.getLastNavigationByProcessId(
this.#config.get('process.ids.packageInspection'),
{

View File

@@ -12,7 +12,7 @@ variables:
value: '4'
# Minor Version einstellen
- name: 'Minor'
value: '0'
value: '1'
- name: 'Patch'
value: "$[counter(format('{0}.{1}', variables['Major'], variables['Minor']),0)]"
- name: 'BuildUniqueID'

View File

@@ -44,5 +44,8 @@ export class DataAccessError<TCode extends string, TData = void> extends Error {
public readonly data: TData,
) {
super(message);
// Set the prototype explicitly to maintain the correct prototype chain
Object.setPrototypeOf(this, new.target.prototype);
this.name = this.constructor.name;
}
}

View File

@@ -18,3 +18,4 @@ export * from './value-tuple-sting-and-integer';
export * from './create-remission';
export * from './remission-item-source';
export * from './receipt-complete-status';
export * from './remission-response-args-error-message';

View File

@@ -0,0 +1,11 @@
// #5331 - Messages kommen bis auf AlreadyRemoved aus dem Backend
export const RemissionResponseArgsErrorMessage = {
AlreadyCompleted: 'Remission wurde bereits abgeschlossen',
AlreadyRemitted: 'Artikel wurde bereits remittiert',
AlreadyRemoved: 'Artikel konnte nicht entfernt werden',
} as const;
export type RemissionResponseArgsErrorMessageKey =
keyof typeof RemissionResponseArgsErrorMessage;
export type RemissionResponseArgsErrorMessageValue =
(typeof RemissionResponseArgsErrorMessage)[RemissionResponseArgsErrorMessageKey];

View File

@@ -40,16 +40,18 @@ import {
calculateAvailableStock,
RemissionReturnReceiptService,
getStockToRemit,
RemissionListType,
RemissionResponseArgsErrorMessage,
} from '@isa/remission/data-access';
import { injectDialog } from '@isa/ui/dialog';
import { injectDialog, injectFeedbackErrorDialog } from '@isa/ui/dialog';
import { SearchItemToRemitDialogComponent } from '@isa/remission/shared/search-item-to-remit-dialog';
import { RemissionListType } from '@isa/remission/data-access';
import { RemissionReturnCardComponent } from './remission-return-card/remission-return-card.component';
import { logger } from '@isa/core/logging';
import { RemissionProcessedHintComponent } from './remission-processed-hint/remission-processed-hint.component';
import { RemissionListDepartmentElementsComponent } from './remission-list-department-elements/remission-list-department-elements.component';
import { injectTabId } from '@isa/core/tabs';
import { RemissionListEmptyStateComponent } from './remission-list-empty-state/remission-list-empty-state.component';
import { firstValueFrom } from 'rxjs';
function querySettingsFactory() {
return inject(ActivatedRoute).snapshot.data['querySettings'];
@@ -118,6 +120,7 @@ export class RemissionListComponent {
activatedTabId = injectTabId();
searchItemToRemitDialog = injectDialog(SearchItemToRemitDialogComponent);
errorDialog = injectFeedbackErrorDialog();
/**
* FilterService instance for managing filter state and queries.
@@ -391,34 +394,50 @@ export class RemissionListComponent {
});
/**
* Effect that handles the case when there are no items in the remission list after a search.
* If the search was triggered by the user, it opens a dialog to search for items to remit.
* If remission has already started, it adds the found items to the remission store and remits them.
* If not, it navigates to the default remission list.
* Effect that handles scenarios where a search yields no results.
* If the search was user-initiated and returned no hits, it opens a dialog
* to allow the user to add a new item to remit.
* If only one hit is found and a remission is started, it selects that item automatically.
* This effect runs whenever the remission or stock resource status changes,
* or when the search term changes.
* It ensures that the user is prompted appropriately based on their actions and the current state of the remission process.
* It also checks if the remission is started or if the list type is 'Abteilung' to determine navigation behavior.
* @see {@link
* https://angular.dev/guide/effects} for more information on Angular effects.
* @remarks This effect uses `untracked` to avoid unnecessary re-evaluations
* when accessing certain signals.
*/
emptySearchResultEffect = effect(() => {
const status = this.remissionResource.status();
const stockStatus = this.inStockResource.status();
const searchTerm: string | undefined = this.searchTerm();
if (status !== 'resolved') {
if (status !== 'resolved' || stockStatus !== 'resolved') {
return;
}
const hasItems = !!this.remissionResource.value()?.result?.length;
if (hasItems || !searchTerm || !this.hasValidSearchTerm()) {
return;
}
this.#store.clearSelectedItems();
untracked(() => {
if (!this.searchTriggeredByUser()) {
const hits = this.hits();
// #5338 - Select item automatically if only one hit after search
if (
!!hits ||
!searchTerm ||
!this.hasValidSearchTerm() ||
!this.searchTriggeredByUser()
) {
if (hits === 1 && this.remissionStarted()) {
this.preselectRemissionItem(this.items()[0]);
}
return;
}
this.searchItemToRemitDialog({
data: {
searchTerm,
isDepartment: this.isDepartment(),
},
}).closed.subscribe(async (result) => {
if (result) {
@@ -432,9 +451,8 @@ export class RemissionListComponent {
} else if (this.isDepartment()) {
return await this.navigateToDefaultRemissionList();
}
this.reloadListAndReturnData();
}
this.reloadListAndReturnData();
});
});
});
@@ -493,17 +511,10 @@ export class RemissionListComponent {
});
}
}
this.remitItemsState.set('success');
this.reloadListAndReturnData();
} catch (error) {
this.#logger.error('Failed to remit items', error);
this.remitItemsError.set(
error instanceof Error
? error.message
: 'Artikel konnten nicht remittiert werden',
);
this.remitItemsState.set('error');
await this.handleRemitItemsError(error);
}
this.#store.clearSelectedItems();
@@ -520,6 +531,62 @@ export class RemissionListComponent {
this.#store.reloadReturn();
}
/**
* Pre-Selects a remission item if it has available stock and can be remitted.
* Updates the remission store with the selected item.
* @param item - The ReturnItem or ReturnSuggestion to select.
* @returns void
*/
preselectRemissionItem(item: RemissionItem) {
if (!!item && item.id) {
const inStock = this.getAvailableStockForItem(item);
const stockToRemit = getStockToRemit({
remissionItem: item,
remissionListType: this.selectedRemissionListType(),
availableStock: inStock,
});
if (inStock > 0 && stockToRemit > 0) {
this.#store.selectRemissionItem(item.id, item);
}
}
}
/**
* Handles errors that occur during the remission of items.
* Logs the error, displays an error dialog, and reloads the list and return data.
* If the error indicates that the remission is already completed, it clears the remission state.
* Sets the stateful button to 'error' to indicate the failure.
* @param error - The error object caught during the remission process.
* @returns A promise that resolves when the error handling is complete.
*/
async handleRemitItemsError(error: any) {
this.#logger.error('Failed to remit items', error);
const errorMessage =
error?.error?.message ??
error?.message ??
'Artikel konnten nicht remittiert werden';
this.remitItemsError.set(errorMessage);
await firstValueFrom(
this.errorDialog({
data: {
errorMessage,
},
}).closed,
);
if (errorMessage === RemissionResponseArgsErrorMessage.AlreadyCompleted) {
this.#store.clearState();
}
this.reloadListAndReturnData();
this.remitItemsState.set('error'); // Stateful-Button auf Error setzen
}
/**
* Navigates to the default remission list based on the current activated tab ID.
* This method is used to redirect the user to the remission list after completing or starting a remission.

View File

@@ -9,6 +9,7 @@ import {
} from '@angular/core';
import {
ReceiptItem,
RemissionResponseArgsErrorMessage,
RemissionReturnReceiptService,
} from '@isa/remission/data-access';
import { ProductFormatComponent } from '@isa/shared/product-foramt';
@@ -20,6 +21,8 @@ import { IconButtonComponent } from '@isa/ui/buttons';
import { provideIcons } from '@ng-icons/core';
import { isaActionClose } from '@isa/icons';
import { logger } from '@isa/core/logging';
import { injectFeedbackErrorDialog } from '@isa/ui/dialog';
import { firstValueFrom } from 'rxjs';
/**
* Component for displaying a single receipt item within the remission return receipt details.
@@ -55,6 +58,8 @@ export class RemissionReturnReceiptDetailsItemComponent {
}));
#returnReceiptService = inject(RemissionReturnReceiptService);
errorDialog = injectFeedbackErrorDialog();
/**
* Required input for the receipt item to display.
* Contains product information and quantity details.
@@ -85,7 +90,7 @@ export class RemissionReturnReceiptDetailsItemComponent {
removing = signal(false);
removed = output<ReceiptItem>();
reloadReturn = output<void>();
async remove() {
if (this.removing()) {
@@ -98,10 +103,25 @@ export class RemissionReturnReceiptDetailsItemComponent {
returnId: this.returnId(),
receiptItemId: this.item().id,
});
this.removed.emit(this.item());
} catch (error) {
this.#logger.error('Failed to remove item', error);
await this.handleRemoveItemError(error);
}
this.reloadReturn.emit();
this.removing.set(false);
}
async handleRemoveItemError(error: any) {
this.#logger.error('Failed to remove item', error);
const errorMessage =
error?.error?.message ?? RemissionResponseArgsErrorMessage.AlreadyRemoved;
await firstValueFrom(
this.errorDialog({
data: {
errorMessage,
},
}).closed,
);
}
}

View File

@@ -55,7 +55,7 @@
[removeable]="canRemoveItems()"
[receiptId]="receiptId()"
[returnId]="returnId()"
(removed)="returnResource.reload()"
(reloadReturn)="returnResource.reload()"
></remi-remission-return-receipt-details-item>
@if (!last) {
<hr class="border-isa-neutral-300" />

View File

@@ -15,7 +15,7 @@ import {
DropdownButtonComponent,
DropdownOptionComponent,
} from '@isa/ui/input-controls';
import { QuantityAndReason } from './select-remi-quantity-and-reason.component';
import { QuantityAndReason } from './select-remi-quantity-and-reason-dialog.component';
import { ReturnValue } from '@isa/common/data-access';
import { provideIcons } from '@ng-icons/core';
import { isaActionChevronDown, isaActionChevronUp } from '@isa/icons';

View File

@@ -1,18 +1,14 @@
@if (item()) {
<remi-select-remi-quantity-and-reason></remi-select-remi-quantity-and-reason>
} @else {
<button
class="absolute top-4 right-[1.33rem]"
type="button"
uiTextButton
size="small"
color="subtle"
(click)="close(undefined)"
tabindex="-1"
data-what="button"
data-which="close-dialog"
>
Schließen
</button>
<remi-search-item-to-remit-list></remi-search-item-to-remit-list>
}
<button
class="absolute top-4 right-[1.33rem]"
type="button"
uiTextButton
size="small"
color="subtle"
(click)="close(undefined)"
tabindex="-1"
data-what="button"
data-which="close-dialog"
>
Schließen
</button>
<remi-search-item-to-remit-list></remi-search-item-to-remit-list>

View File

@@ -1,33 +1,23 @@
import {
ChangeDetectionStrategy,
Component,
effect,
isSignal,
linkedSignal,
signal,
Signal,
} from '@angular/core';
import { DialogContentDirective, NumberInputValidation } from '@isa/ui/dialog';
import { Item } from '@isa/catalogue/data-access';
import { TextButtonComponent } from '@isa/ui/buttons';
import { provideIcons } from '@ng-icons/core';
import { isaActionSearch } from '@isa/icons';
import { SearchItemToRemitListComponent } from './search-item-to-remit-list.component';
import { SelectRemiQuantityAndReasonComponent } from './select-remi-quantity-and-reason.component';
import { Validators } from '@angular/forms';
import { ReturnSuggestion, ReturnItem } from '@isa/remission/data-access';
import { ReturnItem } from '@isa/remission/data-access';
export type SearchItemToRemitDialogData = {
searchTerm: string | Signal<string>;
isDepartment: boolean;
};
export type SearchItemToRemitDialogResult =
SearchItemToRemitDialogData extends { isDepartment: infer D }
? D extends true
? ReturnSuggestion
: ReturnItem
: never;
// #5273, #4768 Fix - Nur ReturnItems sind zugelassen und dürfen zur Pflichtremission hinzugefügt werden
export type SearchItemToRemitDialogResult = ReturnItem;
@Component({
selector: 'remi-search-item-to-remit-dialog',
@@ -35,11 +25,7 @@ export type SearchItemToRemitDialogResult =
styleUrls: ['./search-item-to-remit-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
TextButtonComponent,
SearchItemToRemitListComponent,
SelectRemiQuantityAndReasonComponent,
],
imports: [TextButtonComponent, SearchItemToRemitListComponent],
providers: [provideIcons({ isaActionSearch })],
})
export class SearchItemToRemitDialogComponent extends DialogContentDirective<
@@ -51,35 +37,4 @@ export class SearchItemToRemitDialogComponent extends DialogContentDirective<
? this.data.searchTerm()
: this.data.searchTerm,
);
item = signal<Item | undefined>(undefined);
itemEffect = effect(() => {
const item = this.item();
this.dialogRef.updateSize(item ? '36rem' : 'auto');
if (item) {
this.dialog.title.set(`Dieser Artikel steht nicht auf der Remi Liste`);
} else {
this.dialog.title.set(undefined);
}
});
quantityValidators: NumberInputValidation[] = [
{
errorKey: 'required',
inputValidator: Validators.required,
errorText: 'Bitte geben Sie eine Menge an.',
},
{
errorKey: 'min',
inputValidator: Validators.min(1),
errorText: 'Die Menge muss mindestens 1 sein.',
},
{
errorKey: 'max',
inputValidator: Validators.max(1000),
errorText: 'Die Menge darf höchstens 1000 sein.',
},
];
}

View File

@@ -14,7 +14,7 @@
name="isaActionSearch"
color="brand"
(click)="triggerSearch()"
[pending]="searchResource.isLoading()"
[pending]="searchResource.isLoading() || inStockResource.isLoading()"
data-what="button"
data-which="search-submit"
></ui-icon-button>
@@ -34,24 +34,23 @@
<ng-icon size="1.5rem" name="isaOtherInfo"></ng-icon>
</button>
</p>
<div class="overflow-y-auto">
<div class="overflow-y-auto overflow-x-hidden">
@if (searchResource.value()?.result; as items) {
@for (item of items; track item.id) {
@for (item of availableSearchResults(); track item.id) {
@defer {
@let inStock = getAvailableStockForItem(item);
@if (inStock > 0) {
<remi-search-item-to-remit
[item]="item"
[inStock]="inStock"
data-what="list-item"
data-which="search-result"
[attr.data-item-id]="item.id"
></remi-search-item-to-remit>
}
<remi-search-item-to-remit
[item]="item"
[inStock]="getAvailableStockForItem(item)"
data-what="list-item"
data-which="search-result"
[attr.data-item-id]="item.id"
></remi-search-item-to-remit>
}
}
}
@if (!hasItems() && !searchResource.isLoading()) {
@if (
!hasItems() && !searchResource.isLoading() && !inStockResource.isLoading()
) {
<ui-empty-state
class="w-full justify-self-center"
title="Keine Suchergebnisse"

View File

@@ -57,6 +57,14 @@ export class SearchItemToRemitListComponent implements OnInit {
searchParams = signal<SearchByTermInput | undefined>(undefined);
availableSearchResults = computed(() => {
return (
this.searchResource.value()?.result?.filter((item) => {
return this.getAvailableStockForItem(item) > 0;
}) ?? []
);
});
inStockResource = createInStockResource(() => {
return {
itemIds:
@@ -69,7 +77,7 @@ export class SearchItemToRemitListComponent implements OnInit {
inStockResponseValue = computed(() => this.inStockResource.value());
hasItems = computed(() => {
return (this.searchResource.value()?.result?.length ?? 0) > 0;
return (this.availableSearchResults()?.length ?? 0) > 0;
});
stockInfoMap = computed(() => {

View File

@@ -18,7 +18,7 @@
type="button"
uiTextButton
color="strong"
(click)="host.item.set(item())"
(click)="openQuantityAndReasonDialog()"
>
Remimenge auswählen
</button>

View File

@@ -10,6 +10,9 @@ import { ProductInfoComponent } from '@isa/remission/shared/product';
import { TextButtonComponent } from '@isa/ui/buttons';
import { Breakpoint, breakpoint } from '@isa/ui/layout';
import { SearchItemToRemitDialogComponent } from './search-item-to-remit-dialog.component';
import { injectDialog } from '@isa/ui/dialog';
import { SelectRemiQuantityAndReasonDialogComponent } from './select-remi-quantity-and-reason-dialog.component';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'remi-search-item-to-remit',
@@ -20,6 +23,9 @@ import { SearchItemToRemitDialogComponent } from './search-item-to-remit-dialog.
})
export class SearchItemToRemitComponent {
host = inject(SearchItemToRemitDialogComponent);
quantityAndReasonDialog = injectDialog(
SelectRemiQuantityAndReasonDialogComponent,
);
item = input.required<Item>();
inStock = input.required<number>();
@@ -29,4 +35,22 @@ export class SearchItemToRemitComponent {
productInfoOrientation = computed(() => {
return this.desktopBreakpoint() ? 'vertical' : 'horizontal';
});
async openQuantityAndReasonDialog() {
if (this.item()) {
const dialogRef = this.quantityAndReasonDialog({
title: 'Dieser Artikel steht nicht auf der Remi Liste',
data: {
item: this.item(),
inStock: this.inStock(),
},
width: '36rem',
});
const dialogResult = await firstValueFrom(dialogRef.closed);
if (dialogResult) {
this.host.close(dialogResult);
}
}
}
}

View File

@@ -1,84 +1,94 @@
<p class="text-isa-neutral-600 isa-text-body-1-regular">
Wie viele Exemplare können remittiert werden?
</p>
<div class="flex flex-col gap-4">
@for (
quantityAndReason of quantitiesAndResons();
track $index;
let i = $index
) {
<div class="flex items-center gap-1">
<remi-quantity-and-reason-item
[position]="$index + 1"
[quantityAndReason]="quantityAndReason"
(quantityAndReasonChange)="setQuantityAndReason($index, $event)"
class="flex-1"
data-what="component"
data-which="quantity-reason-item"
[attr.data-position]="$index + 1"
></remi-quantity-and-reason-item>
@if (i > 0) {
<ui-icon-button
type="button"
(click)="removeQuantityReasonItem($index)"
data-what="button"
data-which="remove-quantity"
[attr.data-position]="$index + 1"
name="isaActionClose"
color="neutral"
></ui-icon-button>
}
</div>
}
</div>
<div>
<button
type="button"
class="flex items-center gap-2 -ml-5"
uiTextButton
color="strong"
(click)="addQuantityReasonItem()"
data-what="button"
data-which="add-quantity"
>
<ng-icon name="isaActionPlus" size="1.5rem"></ng-icon>
<div>Menge hinzufügen</div>
</button>
</div>
<div class="text-isa-accent-red isa-text-body-1-regular">
<span>
@if (canReturnErrors(); as errors) {
@for (error of errors; track $index) {
{{ error }}
}
}
</span>
</div>
<div class="grid grid-cols-2 items-center gap-2">
<button
type="button"
color="secondary"
size="large"
uiButton
(click)="host.item.set(undefined)"
data-what="button"
data-which="back"
>
Zurück
</button>
<button
type="button"
color="primary"
size="large"
uiButton
[pending]="canAddToRemiListResource.isLoading()"
[disabled]="canAddToRemiListResource.isLoading() || canReturn() === false"
(click)="addToRemiList()"
data-what="button"
data-which="save-remission"
>
Speichern
</button>
</div>
<remi-product-info
[item]="{
product: data.item.product,
retailPrice: data.item.catalogAvailability.price,
}"
></remi-product-info>
<div class="text-isa-neutral-900 flex flex-row items-center justify-end gap-8">
<span class="isa-text-body-2-regular">Aktueller Bestand</span>
<span class="isa-text-body-2-bold">{{ data.inStock }}x</span>
</div>
<p class="text-isa-neutral-600 isa-text-body-1-regular">
Wie viele Exemplare können remittiert werden?
</p>
<div class="flex flex-col gap-4">
@for (
quantityAndReason of quantitiesAndResons();
track $index;
let i = $index
) {
<div class="flex items-center gap-1">
<remi-quantity-and-reason-item
[position]="$index + 1"
[quantityAndReason]="quantityAndReason"
(quantityAndReasonChange)="setQuantityAndReason($index, $event)"
class="flex-1"
data-what="component"
data-which="quantity-reason-item"
[attr.data-position]="$index + 1"
></remi-quantity-and-reason-item>
@if (i > 0) {
<ui-icon-button
type="button"
(click)="removeQuantityReasonItem($index)"
data-what="button"
data-which="remove-quantity"
[attr.data-position]="$index + 1"
name="isaActionClose"
color="neutral"
></ui-icon-button>
}
</div>
}
</div>
<div>
<button
type="button"
class="flex items-center gap-2 -ml-5"
uiTextButton
color="strong"
(click)="addQuantityReasonItem()"
data-what="button"
data-which="add-quantity"
>
<ng-icon name="isaActionPlus" size="1.5rem"></ng-icon>
<div>Menge hinzufügen</div>
</button>
</div>
<div class="text-isa-accent-red isa-text-body-1-regular">
<span>
@if (canReturnErrors(); as errors) {
@for (error of errors; track $index) {
{{ error }}
}
}
</span>
</div>
<div class="grid grid-cols-2 items-center gap-2">
<button
type="button"
color="secondary"
size="large"
uiButton
(click)="close(undefined)"
data-what="button"
data-which="back"
>
Zurück
</button>
<button
type="button"
color="primary"
size="large"
uiButton
[pending]="canAddToRemiListResource.isLoading()"
[disabled]="canAddToRemiListResource.isLoading() || canReturn() === false"
(click)="addToRemiList()"
data-what="button"
data-which="save-remission"
>
Speichern
</button>
</div>

View File

@@ -0,0 +1,3 @@
:host {
@apply grid grid-flow-row gap-6 h-full;
}

View File

@@ -1,184 +1,196 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
model,
resource,
} from '@angular/core';
import { SearchItemToRemitDialogComponent } from './search-item-to-remit-dialog.component';
import { QuantityAndReasonItemComponent } from './quantity-and-reason-item.component';
import {
ButtonComponent,
TextButtonComponent,
IconButtonComponent,
} from '@isa/ui/buttons';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { isaActionPlus, isaActionClose } from '@isa/icons';
import {
RemissionSearchService,
RemissionStore,
ReturnItem,
ReturnSuggestion,
} from '@isa/remission/data-access';
import { injectFeedbackDialog } from '@isa/ui/dialog';
import { BatchResponseArgs } from '@isa/common/data-access';
export interface QuantityAndReason {
quantity: number;
reason: string;
}
@Component({
selector: 'remi-select-remi-quantity-and-reason',
templateUrl: './select-remi-quantity-and-reason.component.html',
styleUrls: ['./select-remi-quantity-and-reason.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
QuantityAndReasonItemComponent,
TextButtonComponent,
NgIcon,
ButtonComponent,
IconButtonComponent,
],
providers: [provideIcons({ isaActionPlus, isaActionClose })],
})
export class SelectRemiQuantityAndReasonComponent {
#remiService = inject(RemissionSearchService);
#remiStore = inject(RemissionStore);
#feedbackDialog = injectFeedbackDialog();
host = inject(SearchItemToRemitDialogComponent);
initialItem: QuantityAndReason = { quantity: 0, reason: '' };
quantitiesAndResons = model<QuantityAndReason[]>([this.initialItem]);
addQuantityReasonItem(): void {
this.quantitiesAndResons.update((items) => [...items, this.initialItem]);
}
removeQuantityReasonItem(position: number): void {
const currentItems = this.quantitiesAndResons();
if (currentItems.length > 1) {
this.quantitiesAndResons.update((items) =>
items.filter((_, index) => index !== position),
);
}
}
setQuantityAndReason(position: number, qar: QuantityAndReason): void {
this.quantitiesAndResons.update((items) => {
const newItems = [...items];
newItems[position] = qar;
return newItems;
});
}
params = computed(() => {
const items = this.quantitiesAndResons();
const item = this.host.item();
if (!item) {
return [];
}
return items.map((qar) => ({
item,
quantity: qar.quantity,
reason: qar.reason,
}));
});
canAddToRemiListResource = resource({
params: this.params,
loader: async ({ params, abortSignal }) => {
if (
!this.host.item() ||
params.some((p) => !p.reason) ||
params.some((p) => !p.quantity)
) {
return undefined;
}
const maxQuantityErrors = params.filter((p) => !(p.quantity <= 999));
if (maxQuantityErrors.length > 0) {
const errRes: BatchResponseArgs<ReturnItem> = {
completed: false,
error: true,
total: maxQuantityErrors.length,
invalidProperties: {
quantity: 'Die Menge darf maximal 999 sein.',
},
};
return errRes;
}
return this.#remiService.canAddItemToRemiList(params, abortSignal);
},
});
canReturn = computed(() => {
const results = this.canAddToRemiListResource.value();
if (!results) {
return false;
}
if (results.failed && results.failed.length > 0) {
return false;
}
if (
results.successful &&
results.successful.length === this.quantitiesAndResons().length
) {
return true;
}
return false;
});
canReturnErrors = computed(() => {
const results = this.canAddToRemiListResource.value();
if (results?.invalidProperties) {
return Object.values(results.invalidProperties);
}
if (!results?.failed) {
return [];
}
return results.failed.map((item) =>
item.invalidProperties
? Object.values(item.invalidProperties).join(', ')
: [],
) as string[];
});
async addToRemiList() {
const canAddValue = this.canAddToRemiListResource.value();
if (!canAddValue) {
return;
}
if (canAddValue.failed?.length) {
return;
}
// #5273, #4768 Fix - Items dürfen nur zur Pflichtremission hinzugefügt werden
const result: Array<ReturnItem> = await this.#remiService.addToList(
this.params(),
);
this.#feedbackDialog({
data: {
message: this.#remiStore.remissionStarted()
? 'Wurde zum Warenbegleitschein hinzugefügt'
: 'Wurde zur Remi Liste hinzugefügt',
},
});
this.host.close(result);
}
}
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
model,
resource,
} from '@angular/core';
import { QuantityAndReasonItemComponent } from './quantity-and-reason-item.component';
import {
ButtonComponent,
TextButtonComponent,
IconButtonComponent,
} from '@isa/ui/buttons';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { isaActionPlus, isaActionClose } from '@isa/icons';
import {
RemissionSearchService,
RemissionStore,
ReturnItem,
} from '@isa/remission/data-access';
import { DialogContentDirective, injectFeedbackDialog } from '@isa/ui/dialog';
import { BatchResponseArgs } from '@isa/common/data-access';
import { Item } from '@isa/catalogue/data-access';
import { ProductInfoComponent } from '@isa/remission/shared/product';
export type SelectRemiQuantityAndReasonDialogData = {
item: Item;
inStock: number;
};
export type SelectRemiQuantityAndReasonDialogResult =
| undefined
| Array<ReturnItem>;
export interface QuantityAndReason {
quantity: number;
reason: string;
}
@Component({
selector: 'remi-select-remi-quantity-and-reason-dialog',
templateUrl: './select-remi-quantity-and-reason-dialog.component.html',
styleUrls: ['./select-remi-quantity-and-reason-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
QuantityAndReasonItemComponent,
TextButtonComponent,
NgIcon,
ButtonComponent,
IconButtonComponent,
ProductInfoComponent,
],
providers: [provideIcons({ isaActionPlus, isaActionClose })],
})
export class SelectRemiQuantityAndReasonDialogComponent extends DialogContentDirective<
SelectRemiQuantityAndReasonDialogData,
SelectRemiQuantityAndReasonDialogResult
> {
#remiService = inject(RemissionSearchService);
#remiStore = inject(RemissionStore);
#feedbackDialog = injectFeedbackDialog();
initialItem: QuantityAndReason = { quantity: 0, reason: '' };
quantitiesAndResons = model<QuantityAndReason[]>([this.initialItem]);
addQuantityReasonItem(): void {
this.quantitiesAndResons.update((items) => [...items, this.initialItem]);
}
removeQuantityReasonItem(position: number): void {
const currentItems = this.quantitiesAndResons();
if (currentItems.length > 1) {
this.quantitiesAndResons.update((items) =>
items.filter((_, index) => index !== position),
);
}
}
setQuantityAndReason(position: number, qar: QuantityAndReason): void {
this.quantitiesAndResons.update((items) => {
const newItems = [...items];
newItems[position] = qar;
return newItems;
});
}
params = computed(() => {
const items = this.quantitiesAndResons();
const item = this.data.item;
if (!item) {
return [];
}
return items.map((qar) => ({
item,
quantity: qar.quantity,
reason: qar.reason,
}));
});
canAddToRemiListResource = resource({
params: this.params,
loader: async ({ params, abortSignal }) => {
if (
!this.data.item ||
params.some((p) => !p.reason) ||
params.some((p) => !p.quantity)
) {
return undefined;
}
const maxQuantityErrors = params.filter((p) => !(p.quantity <= 999));
if (maxQuantityErrors.length > 0) {
const errRes: BatchResponseArgs<ReturnItem> = {
completed: false,
error: true,
total: maxQuantityErrors.length,
invalidProperties: {
quantity: 'Die Menge darf maximal 999 sein.',
},
};
return errRes;
}
return this.#remiService.canAddItemToRemiList(params, abortSignal);
},
});
canReturn = computed(() => {
const results = this.canAddToRemiListResource.value();
if (!results) {
return false;
}
if (results.failed && results.failed.length > 0) {
return false;
}
if (
results.successful &&
results.successful.length === this.quantitiesAndResons().length
) {
return true;
}
return false;
});
canReturnErrors = computed(() => {
const results = this.canAddToRemiListResource.value();
if (results?.invalidProperties) {
return Object.values(results.invalidProperties);
}
if (!results?.failed) {
return [];
}
return results.failed.map((item) =>
item.invalidProperties
? Object.values(item.invalidProperties).join(', ')
: [],
) as string[];
});
async addToRemiList() {
const canAddValue = this.canAddToRemiListResource.value();
if (!canAddValue) {
return;
}
if (canAddValue.failed?.length) {
return;
}
// #5273, #4768 Fix - Items dürfen nur zur Pflichtremission hinzugefügt werden
const result: Array<ReturnItem> = await this.#remiService.addToList(
this.params(),
);
this.#feedbackDialog({
data: {
message: this.#remiStore.remissionStarted()
? 'Wurde zum Warenbegleitschein hinzugefügt'
: 'Wurde zur Remi Liste hinzugefügt',
},
});
this.close(result);
}
}

View File

@@ -1,3 +0,0 @@
:host {
@apply grid grid-flow-row gap-6;
}

View File

@@ -0,0 +1,17 @@
<div class="w-full flex flex-col gap-4 items-center justify-center">
<span
class="bg-isa-accent-red rounded-[6.25rem] flex flex-row items-center justify-center p-3"
>
<ng-icon
class="text-isa-white"
size="1.5rem"
name="isaActionClose"
></ng-icon>
</span>
<p
class="isa-text-body-1-bold text-isa-neutral-900"
data-what="error-message"
>
{{ data.errorMessage }}
</p>
</div>

View File

@@ -0,0 +1,56 @@
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import {
FeedbackErrorDialogComponent,
FeedbackErrorDialogData,
} from './feedback-error-dialog.component';
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
import { NgIcon } from '@ng-icons/core';
import { DialogComponent } from '../dialog.component';
// Test suite for FeedbackErrorDialogComponent
describe('FeedbackErrorDialogComponent', () => {
let spectator: Spectator<FeedbackErrorDialogComponent>;
const mockData: FeedbackErrorDialogData = {
errorMessage: 'Something went wrong',
};
const createComponent = createComponentFactory({
component: FeedbackErrorDialogComponent,
imports: [NgIcon],
providers: [
{
provide: DialogRef,
useValue: { close: jest.fn() },
},
{
provide: DIALOG_DATA,
useValue: mockData,
},
{
provide: DialogComponent,
useValue: {},
},
],
});
beforeEach(() => {
spectator = createComponent();
jest.clearAllMocks();
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
it('should display the error message passed in data', () => {
const messageElement = spectator.query('[data-what="error-message"]');
expect(messageElement).toHaveText('Something went wrong');
});
it('should render the close icon', () => {
// The icon should be present with isaActionClose
const iconElement = spectator.query('ng-icon');
expect(iconElement).toBeTruthy();
expect(iconElement).toHaveAttribute('name', 'isaActionClose');
});
});

View File

@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DialogContentDirective } from '../dialog-content.directive';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { isaActionClose } from '@isa/icons';
/**
* Input data for the error message dialog
*/
export interface FeedbackErrorDialogData {
/** The Error message text to display in the dialog */
errorMessage: string;
}
/**
* A simple feedback dialog component that displays an error message and an error icon.
*/
@Component({
selector: 'ui-feedback-error-dialog',
templateUrl: './feedback-error-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIcon],
providers: [provideIcons({ isaActionClose })],
host: {
'[class]': '["ui-feedback-error-dialog"]',
},
})
export class FeedbackErrorDialogComponent extends DialogContentDirective<
FeedbackErrorDialogData,
void
> {}

View File

@@ -7,6 +7,7 @@ import {
injectTextInputDialog,
injectNumberInputDialog,
injectConfirmationDialog,
injectFeedbackErrorDialog,
} from './injects';
import { MessageDialogComponent } from './message-dialog/message-dialog.component';
import { DialogComponent } from './dialog.component';
@@ -17,6 +18,7 @@ import { TextInputDialogComponent } from './text-input-dialog/text-input-dialog.
import { FeedbackDialogComponent } from './feedback-dialog/feedback-dialog.component';
import { NumberInputDialogComponent } from './number-input-dialog/number-input-dialog.component';
import { ConfirmationDialogComponent } from './confirmation-dialog/confirmation-dialog.component';
import { FeedbackErrorDialogComponent } from './feedback-error-dialog/feedback-error-dialog.component';
// Test component extending DialogContentDirective for testing
@Component({ template: '' })
@@ -290,4 +292,23 @@ describe('Dialog Injects', () => {
expect(injector.get(DIALOG_CONTENT)).toBe(ConfirmationDialogComponent);
});
});
describe('injectFeedbackErrorDialog', () => {
it('should create a dialog injector for FeedbackErrorDialogComponent', () => {
// Act
const openFeedbackErrorDialog = TestBed.runInInjectionContext(() =>
injectFeedbackErrorDialog(),
);
openFeedbackErrorDialog({
data: {
errorMessage: 'Test error message',
},
});
// Assert
const callOptions = mockDialogOpen.mock.calls[0][1];
const injector = callOptions.injector;
expect(injector.get(DIALOG_CONTENT)).toBe(FeedbackErrorDialogComponent);
});
});
});

View File

@@ -21,6 +21,10 @@ import {
ConfirmationDialogComponent,
ConfirmationDialogData,
} from './confirmation-dialog/confirmation-dialog.component';
import {
FeedbackErrorDialogComponent,
FeedbackErrorDialogData,
} from './feedback-error-dialog/feedback-error-dialog.component';
export interface InjectDialogOptions {
/** Optional title override for the dialog */
@@ -173,3 +177,17 @@ export const injectFeedbackDialog = (
classList: ['gap-0'],
...options,
});
/**
* Convenience function that returns a pre-configured FeedbackErrorDialog injector
* @returns A function to open a feedback error dialog
*/
export const injectFeedbackErrorDialog = (
options?: OpenDialogOptions<FeedbackErrorDialogData>,
) =>
injectDialog(FeedbackErrorDialogComponent, {
disableClose: false,
minWidth: '20rem',
classList: ['gap-0'],
...options,
});