mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merge branch 'release/4.0' into develop
This commit is contained in:
@@ -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,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,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;
|
||||
@@ -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,4 +1,4 @@
|
||||
import { computed, inject, resource, untracked } from '@angular/core';
|
||||
import { computed, inject, resource } from '@angular/core';
|
||||
import {
|
||||
CanReturn,
|
||||
ProductCategory,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
ReturnDetailsService,
|
||||
} from '@isa/oms/data-access';
|
||||
import {
|
||||
getState,
|
||||
patchState,
|
||||
signalStore,
|
||||
type,
|
||||
@@ -22,13 +21,11 @@ import {
|
||||
getReceiptItemQuantity,
|
||||
getReceiptItemProductCategory,
|
||||
receiptItemHasCategory,
|
||||
getReceiptItemReturnedQuantity,
|
||||
} from '../helpers/return-process';
|
||||
import { SessionStorageProvider } from '@isa/core/storage';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { clone } from 'lodash';
|
||||
|
||||
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: {},
|
||||
@@ -49,36 +45,11 @@ export const receiptConfig = entityConfig({
|
||||
});
|
||||
|
||||
export const ReturnDetailsStore = signalStore(
|
||||
{ providedIn: 'root' },
|
||||
withState(initialState),
|
||||
withEntities(receiptConfig),
|
||||
withProps(() => ({
|
||||
_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();
|
||||
@@ -167,8 +151,8 @@ export const ReturnDetailsStore = signalStore(
|
||||
{ receiptId: params },
|
||||
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 ({ params, abortSignal }) => {
|
||||
if (params === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const key = `${params.item.id}:${params.category}`;
|
||||
const key = `${params.receiptItemId}:${params.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();
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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']> = {
|
||||
1: {
|
||||
@@ -77,7 +78,8 @@ describe('ReturnProcessStore', () => {
|
||||
receiptItem: TEST_ITEMS[1],
|
||||
receiptDate: '',
|
||||
answers: {},
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
},
|
||||
{
|
||||
@@ -87,7 +89,8 @@ describe('ReturnProcessStore', () => {
|
||||
receiptItem: TEST_ITEMS[2],
|
||||
receiptDate: '',
|
||||
answers: {},
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
},
|
||||
{
|
||||
@@ -97,7 +100,8 @@ describe('ReturnProcessStore', () => {
|
||||
receiptItem: TEST_ITEMS[3],
|
||||
receiptDate: '',
|
||||
answers: {},
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
},
|
||||
] as ReturnProcess[]),
|
||||
@@ -122,7 +126,8 @@ describe('ReturnProcessStore', () => {
|
||||
receiptItem: TEST_ITEMS[1],
|
||||
receiptDate: '',
|
||||
answers: {},
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
},
|
||||
] as ReturnProcess[]),
|
||||
@@ -148,7 +153,8 @@ describe('ReturnProcessStore', () => {
|
||||
receiptDate: new Date().toJSON(),
|
||||
receiptItem: TEST_ITEMS[1],
|
||||
receiptId: 123,
|
||||
productCategory: undefined,
|
||||
productCategory: ProductCategory.BookCalendar,
|
||||
quantity: 1,
|
||||
returnReceipt: undefined,
|
||||
} as ReturnProcess),
|
||||
);
|
||||
@@ -173,7 +179,13 @@ describe('ReturnProcessStore', () => {
|
||||
items: [],
|
||||
buyer: { buyerNumber: '' },
|
||||
},
|
||||
items: [TEST_ITEMS[1]],
|
||||
items: [
|
||||
{
|
||||
receiptItem: TEST_ITEMS[1],
|
||||
quantity: 1,
|
||||
category: ProductCategory.BookCalendar,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
receipt: {
|
||||
@@ -182,12 +194,22 @@ describe('ReturnProcessStore', () => {
|
||||
items: [],
|
||||
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', () => {
|
||||
@@ -205,7 +227,13 @@ describe('ReturnProcessStore', () => {
|
||||
items: [],
|
||||
buyer: { buyerNumber: '' },
|
||||
},
|
||||
items: [TEST_ITEMS[2]], // Non-returnable item
|
||||
items: [
|
||||
{
|
||||
receiptItem: TEST_ITEMS[2], // Non-returnable item
|
||||
quantity: 1,
|
||||
category: ProductCategory.BookCalendar,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -227,7 +255,23 @@ describe('ReturnProcessStore', () => {
|
||||
items: [],
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -21,13 +21,21 @@ import {
|
||||
} 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;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -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];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
resource,
|
||||
} from '@angular/core';
|
||||
@@ -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(),
|
||||
@@ -71,10 +70,6 @@ export class ReturnDetailsComponent {
|
||||
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({
|
||||
@@ -107,6 +102,8 @@ export class ReturnDetailsComponent {
|
||||
|
||||
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', () => ({
|
||||
processId: processId,
|
||||
@@ -126,7 +123,14 @@ 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],
|
||||
};
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user