Merged PR 1884: #5213

- feat(dialog-feedback-dialog, remission-list-item): add feedback dialog and remission list item components
- feat(remission-list-item): implement remission list item component
Refs: #5213
This commit is contained in:
Nino Righi
2025-07-15 11:26:03 +00:00
committed by Lorenz Hilpert
parent 40c9d51dfc
commit 65ab3bfc0a
10 changed files with 190 additions and 32 deletions

View File

@@ -19,7 +19,7 @@
<ui-item-row-data>
<remi-product-stock-info
[availableStock]="availableStock()"
[stockToRemit]="stockToRemit()"
[stockToRemit]="selectedStockToRemit() ?? stockToRemit()"
[targetStock]="targetStock()"
[zob]="stock()?.minStockCategoryManagement ?? 0"
></remi-product-stock-info>

View File

@@ -22,7 +22,7 @@ import {
ProductStockInfoComponent,
} from '@isa/remission/shared/product';
import { TextButtonComponent } from '@isa/ui/buttons';
import { injectTextInputDialog } from '@isa/ui/dialog';
import { injectFeedbackDialog, injectTextInputDialog } from '@isa/ui/dialog';
import { ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
import { firstValueFrom } from 'rxjs';
import { Breakpoint, breakpoint } from '@isa/ui/layout';
@@ -63,6 +63,12 @@ export class RemissionListItemComponent {
*/
#dialog = injectTextInputDialog();
/**
* Dialog service for providing feedback to the user.
* @private
*/
#feedbackDialog = injectFeedbackDialog();
/**
* Store for managing selected remission quantities.
* @private
@@ -139,48 +145,53 @@ export class RemissionListItemComponent {
* Computes the available stock for the item using stock and removedFromStock.
* @returns The calculated available stock.
*/
availableStock = computed(() => {
return calculateAvailableStock({
availableStock = computed(() =>
calculateAvailableStock({
stock: this.stock()?.inStock,
removedFromStock: this.stock()?.removedFromStock,
});
});
}),
);
/**
* Computes the quantity to remit for the current item.
* - Uses the selected quantity from the store if available.
* - Otherwise, calculates based on available stock, predefined return quantity, and remaining quantity.
* @returns The quantity to remit.
* Computes the selected stock quantity to remit for the current item.
* Uses the store's selected quantity for the item's ID.
*/
stockToRemit = computed(() => {
const remissionItemId = this.item()?.id;
return (
this.#store.selectedQuantity()?.[remissionItemId!] ??
calculateStockToRemit({
availableStock: this.availableStock(),
predefinedReturnQuantity: this.predefinedReturnQuantity(),
remainingQuantityInStock: this.remainingQuantityInStock(),
})
);
});
selectedStockToRemit = computed(
() => this.#store.selectedQuantity()?.[this.item().id!],
);
/**
* Computes the stock to remit based on available stock, predefined return quantity,
* and remaining quantity in stock.
*
* @returns The calculated stock to remit.
*/
stockToRemit = computed(() =>
calculateStockToRemit({
availableStock: this.availableStock(),
predefinedReturnQuantity: this.predefinedReturnQuantity(),
remainingQuantityInStock: this.remainingQuantityInStock(),
}),
);
/**
* Computes the target stock after remission.
* @returns The calculated target stock.
*/
targetStock = computed(() => {
return calculateTargetStock({
targetStock = computed(() =>
calculateTargetStock({
availableStock: this.availableStock(),
stockToRemit: this.stockToRemit(),
remainingQuantityInStock: this.remainingQuantityInStock(),
});
});
}),
);
/**
* Opens a dialog to allow the user to change the remission quantity for the item.
* Validates the input and updates the remission quantity in the store if valid.
* Opens a dialog to change the remission quantity for the current item.
* Prompts the user for a new quantity and updates the store if valid.
* Displays feedback dialog upon successful update.
*
* @returns Promise<void>
* @returns A promise that resolves when the dialog is closed.
*/
async openRemissionQuantityDialog(): Promise<void> {
const dialogRef = this.#dialog({
@@ -209,6 +220,9 @@ export class RemissionListItemComponent {
if (itemId && quantity > 0) {
this.#store.updateRemissionQuantity(itemId, this.item(), quantity);
this.#feedbackDialog({
data: { message: 'Remi-Menge wurde geändert' },
});
}
}
}

View File

@@ -1,6 +1,6 @@
.ui-dialog {
@apply bg-isa-white p-8 grid gap-8 items-start rounded-[2rem] grid-flow-row text-isa-neutral-900 relative;
@apply max-h-[90vh] max-w-[90vw] overflow-hidden;
@apply max-h-[90vh] overflow-hidden;
grid-template-rows: auto 1fr;
.ui-dialog-title {
@@ -13,4 +13,8 @@
@apply overflow-y-auto overflow-x-hidden;
@apply min-h-0;
}
&:has(ui-feedback-dialog) {
@apply gap-0;
}
}

View File

@@ -4,4 +4,5 @@ export * from './lib/injects';
export * from './lib/message-dialog/message-dialog.component';
export * from './lib/confirmation-dialog/confirmation-dialog.component';
export * from './lib/text-input-dialog/text-input-dialog.component';
export * from './lib/feedback-dialog/feedback-dialog.component';
export * from './lib/tokens';

View File

@@ -1,5 +1,7 @@
<h2 class="ui-dialog-title" data-what="title">
{{ title() }}
</h2>
@if (title()) {
<h2 class="ui-dialog-title" data-what="title">
{{ title() }}
</h2>
}
<ng-container *ngComponentOutlet="component"> </ng-container>

View File

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

View File

@@ -0,0 +1,58 @@
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import {
FeedbackDialogComponent,
FeedbackDialogData,
} from './feedback-dialog.component';
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
import { NgIcon } from '@ng-icons/core';
import { DialogComponent } from '../dialog.component';
// Test suite for FeedbackDialogComponent
describe('FeedbackDialogComponent', () => {
let spectator: Spectator<FeedbackDialogComponent>;
let mockDialogRef: DialogRef<void>;
const mockData: FeedbackDialogData = {
message: 'Feedback message',
};
const createComponent = createComponentFactory({
component: FeedbackDialogComponent,
imports: [NgIcon],
providers: [
{
provide: DialogRef,
useValue: { close: jest.fn() },
},
{
provide: DIALOG_DATA,
useValue: mockData,
},
{
provide: DialogComponent,
useValue: {},
},
],
});
beforeEach(() => {
spectator = createComponent();
mockDialogRef = spectator.inject(DialogRef);
jest.clearAllMocks();
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
it('should display the feedback message passed in data', () => {
// Adjust selector if template uses a different data-what value
const messageElement = spectator.query('[data-what="message"]');
expect(messageElement).toHaveText('Feedback message');
});
it('should render the check icon', () => {
// The icon should be present if the template uses NgIcon with isaActionCheck
const iconElement = spectator.query('ng-icon');
expect(iconElement).toBeTruthy();
});
});

View File

@@ -0,0 +1,33 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DialogContentDirective } from '../dialog-content.directive';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { isaActionCheck } from '@isa/icons';
/**
* Input data for the message dialog
*/
export interface FeedbackDialogData {
/** The message text to display in the dialog */
message: string;
}
/**
* Simple message dialog component
* Used for displaying informational messages to the user
* Returns void when closed (no result)
*/
@Component({
selector: 'ui-feedback-dialog',
templateUrl: './feedback-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIcon],
providers: [provideIcons({ isaActionCheck })],
host: {
'[class]': '["ui-feedback-dialog"]',
},
})
export class FeedbackDialogComponent extends DialogContentDirective<
FeedbackDialogData,
void
> {}

View File

@@ -2,6 +2,7 @@ import { Dialog, DialogRef } from '@angular/cdk/dialog';
import { TestBed } from '@angular/core/testing';
import {
injectDialog,
injectFeedbackDialog,
injectMessageDialog,
injectTextInputDialog,
} from './injects';
@@ -11,6 +12,7 @@ import { DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
import { Component } from '@angular/core';
import { DialogContentDirective } from './dialog-content.directive';
import { TextInputDialogComponent } from './text-input-dialog/text-input-dialog.component';
import { FeedbackDialogComponent } from './feedback-dialog/feedback-dialog.component';
// Test component extending DialogContentDirective for testing
@Component({ template: '' })
@@ -174,4 +176,23 @@ describe('Dialog Injects', () => {
expect(injector.get(DIALOG_CONTENT)).toBe(TextInputDialogComponent);
});
});
describe('injectFeedbackDialog', () => {
it('should create a dialog injector for FeedbackDialogComponent', () => {
// Act
const openFeedbackDialog = TestBed.runInInjectionContext(() =>
injectFeedbackDialog(),
);
openFeedbackDialog({
data: {
message: 'Test message',
},
});
// Assert
const callOptions = mockDialogOpen.mock.calls[0][1];
const injector = callOptions.injector;
expect(injector.get(DIALOG_CONTENT)).toBe(FeedbackDialogComponent);
});
});
});

View File

@@ -7,6 +7,7 @@ import { DialogComponent } from './dialog.component';
import { DIALOG_CONTENT, DIALOG_TITLE } from './tokens';
import { MessageDialogComponent } from './message-dialog/message-dialog.component';
import { TextInputDialogComponent } from './text-input-dialog/text-input-dialog.component';
import { FeedbackDialogComponent } from './feedback-dialog/feedback-dialog.component';
export interface InjectDialogOptions {
/** Optional title override for the dialog */
@@ -114,3 +115,13 @@ export const injectMessageDialog = () => injectDialog(MessageDialogComponent);
*/
export const injectTextInputDialog = () =>
injectDialog(TextInputDialogComponent);
/**
* Convenience function that returns a pre-configured FeedbackDialog injector
* @returns A function to open a feedback dialog
*/
export const injectFeedbackDialog = () =>
injectDialog(FeedbackDialogComponent, {
disableClose: false,
minWidth: '20rem',
});