feat(oms-task-list): implement task action types and specialized UI handling

Enhance the ReturnTaskListComponent and ReturnTaskListItemComponent to:
- Use properly typed TaskActionTypes enum (OK, NOK, PRINT, UNKNOWN) instead of string literals
- Add specialized UI components for different action types
- Implement conditional rendering for task actions based on type
- Improve styling for different task types
- Filter out completed tasks in main view

feat(oms-data-access): add Zod schema validation for return receipts

- Add ReturnReceiptValuesSchema for validation of API payloads
- Implement proper type safety for task action types
- Use schema validation in ReturnProcessService before API calls

Ref: #4942
This commit is contained in:
Nino
2025-05-05 17:45:48 +02:00
parent 82d991fcbc
commit 7edbe11c65
11 changed files with 177 additions and 41 deletions

View File

@@ -1,4 +1,14 @@
export const TaskActionTypes = {
OK: 'OK',
NOK: 'NOK',
PRINT: 'PRINT',
UNKNOWN: 'UNKNOWN',
} as const;
export type TaskActionTypeType =
(typeof TaskActionTypes)[keyof typeof TaskActionTypes];
export interface TaskActionType {
type: 'complete' | 'damaged' | 'resell' | 'print';
type: TaskActionTypeType;
taskId: number;
}

View File

@@ -11,7 +11,10 @@ import {
} from './models';
import { CategoryQuestions } from './questions';
import { KeyValue } from '@angular/common';
import { ReturnProcessChecklistAnswerSchema } from './schemas';
import {
ReturnProcessChecklistAnswerSchema,
ReturnReceiptValuesSchema,
} from './schemas';
import { logger } from '@isa/core/logging';
import {
PropertyIsEmptyError,
@@ -31,6 +34,7 @@ import {
} from '@generated/swagger/oms-api';
import { firstValueFrom } from 'rxjs';
import { ReturnPrintReceiptsService } from './return-print-receipts.service';
import { z } from 'zod';
/**
* Service responsible for managing the return process workflow.
@@ -286,19 +290,26 @@ export class ReturnProcessService {
}
return {
quantity: process.receiptItem.quantity.quantity,
quantity: process.receiptItem.quantity.quantity, // TODO: Teilmenge handling implementieren - Aktuell wird die gesamte Quantity genommen
comment: returnInfo.comment,
itemCondition: returnInfo.itemCondition,
otherProduct: returnInfo.otherProduct,
returnDetails: returnInfo.returnDetails,
returnReason: returnInfo.returnReason,
receiptItem: process.receiptItem,
receiptItem: { id: process.receiptItem.id },
};
});
const parsedPayload = z.array(ReturnReceiptValuesSchema).safeParse(payload);
if (!parsedPayload.success) {
this.#logger.error('Payload validation failed', parsedPayload.error);
return [];
}
const response = await firstValueFrom(
this.#receiptService.ReceiptCreateReturnReceipt({
payload,
payload: parsedPayload.data as ReturnReceiptValuesDTO[],
}),
);

View File

@@ -1,3 +1,4 @@
export * from './fetch-return-details.schema';
export * from './query-token.schema';
export * from './return-process-question-answer.schema';
export * from './return-receipt-values.schema';

View File

@@ -0,0 +1,19 @@
import { z } from 'zod';
import { Product } from '../models/product';
const ReceiptItemSchema = z.object({
id: z.number(),
});
export const ReturnReceiptValuesSchema = z.object({
quantity: z.number(),
comment: z.string().optional(),
itemCondition: z.string().optional(),
returnDetails: z.string().optional(),
returnReason: z.string().optional(),
receiptItem: ReceiptItemSchema,
});
export type ReturnReceiptValues = z.infer<typeof ReturnReceiptValuesSchema> & {
otherProduct: Product;
};

View File

@@ -35,9 +35,6 @@
</div>
</div>
<div class="flex flex-col items-center gap-6 w-[24.25rem] justify-self-center">
<span class="isa-text-subtitle-2-bold self-start">OFFENE AUFGABEN</span>
<oms-shared-return-task-list
[appearance]="'main'"
></oms-shared-return-task-list>
</div>
<oms-shared-return-task-list
[appearance]="'main'"
></oms-shared-return-task-list>

View File

@@ -4,27 +4,33 @@
data-which="return-product-info"
></oms-shared-return-product-info>
<div class="p-4 flex flex-col gap-4 rounded-lg bg-isa-secondary-100">
@let taskActionType = type();
<div
class="h-full p-4 flex flex-col gap-4 rounded-lg bg-isa-secondary-100"
[class.unkown-type]="taskActionType === 'UNKNOWN'"
>
<div
data-what="review-list"
data-which="processing-comment"
class="isa-text-body-2-bold"
class="processing-comment"
>
{{ processingComment() }}
</div>
@if (!item()?.completed) {
<button
class="flex items-center gap-2 self-end"
type="button"
uiButton
color="primary"
(click)="onActionClick({ type: 'complete' })"
data-what="button"
data-which="complete"
>
<ng-icon name="isaActionCheck" uiButtonIcon></ng-icon>
Als erledigt markieren
</button>
@if (taskActionType !== 'UNKNOWN') {
<button
class="flex items-center gap-2 self-end"
type="button"
uiButton
color="primary"
(click)="onActionClick(taskActionType)"
data-what="button"
data-which="complete"
>
<ng-icon name="isaActionCheck" uiButtonIcon></ng-icon>
Als erledigt markieren
</button>
}
} @else {
<span
class="flex items-center gap-2 text-isa-accent-green isa-text-body-2-bold self-end"
@@ -36,3 +42,43 @@
</span>
}
</div>
@if (taskActionType === 'UNKNOWN' && !item()?.completed) {
<div class="flex flex-row gap-3 h-full py-2">
<button
class="flex items-center"
type="button"
uiButton
color="secondary"
(click)="onActionClick(taskActionType)"
data-what="button"
data-which="resellable"
>
Ja verkaufsfähig
</button>
<button
class="flex items-center"
type="button"
uiButton
color="secondary"
(click)="onActionClick(taskActionType)"
data-what="button"
data-which="damaged"
>
Nein, defekt
</button>
</div>
}
@if (taskActionType === 'PRINT') {
<button
data-what="button"
data-which="print-receipt"
class="self-start"
(click)="onActionClick(taskActionType)"
uiInfoButton
>
<span uiInfoButtonLabel>Retourenschein drucken</span>
<ng-icon name="isaActionPrinter" uiInfoButtonIcon></ng-icon>
</button>
}

View File

@@ -7,5 +7,17 @@
}
.oms-shared-return-task-list-item__main {
@apply bg-white rounded-2xl p-6 flex flex-col gap-6;
@apply bg-white rounded-2xl p-6 flex flex-col gap-6 w-[24.25rem];
}
.processing-comment {
@apply isa-text-body-2-bold;
}
.unkown-type {
@apply rounded-none bg-isa-white;
.processing-comment {
@apply isa-text-body-1-regular;
}
}

View File

@@ -4,16 +4,18 @@ import {
computed,
input,
output,
Signal,
ViewEncapsulation,
} from '@angular/core';
import { isaActionCheck } from '@isa/icons';
import { isaActionCheck, isaActionPrinter } from '@isa/icons';
import {
Product,
ReceiptItemTaskListItem,
TaskActionType,
TaskActionTypeType,
} from '@isa/oms/data-access';
import { ReturnProductInfoComponent } from '@isa/oms/shared/product-info';
import { ButtonComponent } from '@isa/ui/buttons';
import { ButtonComponent, InfoButtonComponent } from '@isa/ui/buttons';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
@Component({
@@ -22,8 +24,13 @@ import { NgIconComponent, provideIcons } from '@ng-icons/core';
styleUrl: './return-task-list-item.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ReturnProductInfoComponent, ButtonComponent, NgIconComponent],
providers: [provideIcons({ isaActionCheck })],
imports: [
ReturnProductInfoComponent,
ButtonComponent,
NgIconComponent,
InfoButtonComponent,
],
providers: [provideIcons({ isaActionCheck, isaActionPrinter })],
host: {
'[class]': "['oms-shared-return-task-list-item', appearanceClass()]",
},
@@ -60,9 +67,17 @@ export class ReturnTaskListItemComponent {
return undefined;
});
onActionClick(type: Omit<TaskActionType, 'taskId'>) {
type: Signal<TaskActionTypeType> = computed(() => {
const item = this.item();
const mappedType: Omit<TaskActionType, 'taskId'> = {
type: item.type as TaskActionTypeType,
};
return mappedType.type;
});
onActionClick(type: TaskActionTypeType) {
const taskId = this.item().id;
const actionType = { ...type, taskId };
const actionType = { type, taskId };
this.action.emit(actionType);
}
}

View File

@@ -1,8 +1,12 @@
@let taskList = taskListItems();
@if (taskList?.length !== 0) {
@if (appearance() === 'main') {
<span class="isa-text-subtitle-2-bold self-start">OFFENE AUFGABEN</span>
}
<div
class="flex flex-col w-full items-center justify-center"
[class.list-gap]="appearance() === 'main'"
[class.return-search-main-task-list-styles]="appearance() === 'main'"
>
@for (item of taskList; track item.id) {
@defer (on viewport) {

View File

@@ -1,3 +1,7 @@
.list-gap {
@apply gap-6;
.oms-shared-return-task-list__main {
@apply flex flex-col items-center gap-6 justify-self-center;
}
.return-search-main-task-list-styles {
@apply gap-6 desktop:overflow-y-scroll desktop:max-h-[calc(100vh-13rem)] justify-start;
}

View File

@@ -46,10 +46,24 @@ export class ReturnTaskListComponent {
taskListItems = computed(() => {
const processId = this.processId();
const appearance = this.appearance();
if (!processId) {
return [];
}
return this.#returnReviewStore.entityMap()[processId].data ?? [];
const taskListItems = this.#returnReviewStore.entityMap()[processId].data;
if (!taskListItems || taskListItems?.length === 0) {
return [];
}
// Filter out completed tasks if the appearance is 'main'
if (appearance === 'main') {
return taskListItems.filter((item) => !item.completed);
}
return taskListItems;
});
constructor() {
@@ -69,11 +83,14 @@ export class ReturnTaskListComponent {
}
async handleAction(action: TaskActionType) {
switch (action.type) {
case 'complete':
await this.completeTask(action.taskId);
break;
// TODO: Implement other action types
if (action.type === 'UNKNOWN') {
// TODO: Neuer Endpoint - updateTask
} else {
if (action.type === 'PRINT') {
// TODO: Spezieller Print request für Tolino - DIN-A4 Retourenschein Drucken
}
await this.completeTask(action.taskId);
}
}