fix(intake): support stock spu sku continuation
This commit is contained in:
parent
ac24f0546c
commit
9bdc6ff0bd
@ -17,6 +17,7 @@ import {
|
||||
cleanupSharedNonIntegratedAiWhiteBackgroundManifest,
|
||||
fetchSharedNonIntegratedAiWhiteBackgroundCapabilities,
|
||||
generateSharedNonIntegratedAiWhiteBackgroundSession,
|
||||
listSharedNonIntegratedProductPage,
|
||||
previewSharedNonIntegratedDocumentImages,
|
||||
saveSharedNonIntegratedProduct,
|
||||
uploadSharedNonIntegratedAiWhiteBackgroundSession,
|
||||
@ -181,6 +182,15 @@ jest.mock("../../../lib/model-preview-zip", () => ({
|
||||
}));
|
||||
|
||||
jest.mock("./nonIntegratedApi", () => ({
|
||||
listSharedNonIntegratedProductPage: jest.fn(async () => ({
|
||||
products: [],
|
||||
pageInfo: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 1,
|
||||
},
|
||||
})),
|
||||
previewSharedNonIntegratedDocumentImages: jest.fn(),
|
||||
saveSharedNonIntegratedProduct: jest.fn(async (product) => product),
|
||||
fetchSharedNonIntegratedAiWhiteBackgroundCapabilities: jest.fn(),
|
||||
@ -217,6 +227,9 @@ jest.mock("@/app/approval-workflows/approvalWorkflowApi", () => ({
|
||||
const mockedPreviewSharedNonIntegratedDocumentImages = jest.mocked(
|
||||
previewSharedNonIntegratedDocumentImages,
|
||||
);
|
||||
const mockedListSharedNonIntegratedProductPage = jest.mocked(
|
||||
listSharedNonIntegratedProductPage,
|
||||
);
|
||||
const mockedFetchSharedNonIntegratedAiWhiteBackgroundCapabilities = jest.mocked(
|
||||
fetchSharedNonIntegratedAiWhiteBackgroundCapabilities,
|
||||
);
|
||||
@ -584,6 +597,16 @@ describe("NonIntegratedDetailClient", () => {
|
||||
(parseZipFile as jest.Mock).mockReset();
|
||||
(extractPreviewModelFromZip as jest.Mock).mockReset();
|
||||
mockedPreviewSharedNonIntegratedDocumentImages.mockReset();
|
||||
mockedListSharedNonIntegratedProductPage.mockReset();
|
||||
mockedListSharedNonIntegratedProductPage.mockResolvedValue({
|
||||
products: [],
|
||||
pageInfo: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 1,
|
||||
},
|
||||
});
|
||||
mockedPreviewSharedNonIntegratedDocumentImages.mockResolvedValue({
|
||||
imageUrls: ["/api/intake/non-integrated/assets/document-previews/test/page-1.jpg"],
|
||||
});
|
||||
@ -1175,6 +1198,12 @@ describe("NonIntegratedDetailClient", () => {
|
||||
productCategory: "书桌",
|
||||
}),
|
||||
);
|
||||
expect(saveCall[0].skuItems[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
skuSequence: 10,
|
||||
skuCode: "SZ023010",
|
||||
}),
|
||||
);
|
||||
expect(saveCall[0].openingReference).toEqual(
|
||||
expect.objectContaining({
|
||||
salesCategory: "桌类",
|
||||
@ -1189,6 +1218,92 @@ describe("NonIntegratedDetailClient", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("searches stock product details when selecting an existing SPU for a new SKU flow", async () => {
|
||||
jest.useFakeTimers();
|
||||
const actor = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
(
|
||||
globalThis as NonIntegratedRemoteAssetStorageTestGlobal
|
||||
).__NON_INTEGRATED_REMOTE_ASSET_STORAGE_TEST_OVERRIDE__ = true;
|
||||
const stockProduct = createProductRecord({
|
||||
id: "jst-stock-dnz0384",
|
||||
spu: "DNZ0384",
|
||||
productCode: "DNZ0384",
|
||||
formalSpuAssigned: true,
|
||||
metadata: {
|
||||
source: "jst_sku",
|
||||
sourceSpu: "DNZ0384",
|
||||
},
|
||||
productCategory: "涔︽",
|
||||
openingReference: {
|
||||
...createEmptyNonIntegratedOpeningReference(),
|
||||
productName: "DNZ0384瀛橀噺鍟嗗搧",
|
||||
productCategory: "涔︽",
|
||||
},
|
||||
skuItems: [
|
||||
createSkuItem({
|
||||
skuSequence: 16,
|
||||
skuCode: "JST-OLD-SKU",
|
||||
barcode: "JST-OLD-BARCODE",
|
||||
}),
|
||||
],
|
||||
});
|
||||
mockedListSharedNonIntegratedProductPage.mockResolvedValue({
|
||||
products: [stockProduct],
|
||||
pageInfo: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
render(
|
||||
<NonIntegratedDetailClient
|
||||
mode="create"
|
||||
existingSpuProducts={[]}
|
||||
currentUserRole="USER"
|
||||
currentUserBusinessRoles={["DESIGN"]}
|
||||
currentUserId="creator-user-id"
|
||||
currentUserName="Creator"
|
||||
/>,
|
||||
);
|
||||
|
||||
await actor.click(screen.getByTestId("spu-create-mode-existing"));
|
||||
const existingSpuSelect = await screen.findByTestId(
|
||||
"existing-spu-create-select",
|
||||
);
|
||||
await actor.click(existingSpuSelect);
|
||||
await actor.type(existingSpuSelect, "DNZ0384");
|
||||
jest.advanceTimersByTime(300);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedListSharedNonIntegratedProductPage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pageSize: 10,
|
||||
productKind: "stock",
|
||||
spu: "DNZ0384",
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const listboxes = await screen.findAllByRole("listbox");
|
||||
const activeListbox = listboxes[listboxes.length - 1]!;
|
||||
const stockOption = Array.from(
|
||||
activeListbox.querySelectorAll<HTMLButtonElement>('[role="option"]'),
|
||||
).find((option) => option.value === "DNZ0384");
|
||||
expect(stockOption).toBeDefined();
|
||||
await actor.click(stockOption!);
|
||||
|
||||
expect(
|
||||
(screen.getByTestId("existing-spu-create-select") as HTMLInputElement)
|
||||
.value,
|
||||
).toBe("DNZ0384");
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("shows series component details from the barcode magnifier without leaking them into attribute columns", async () => {
|
||||
const actor = userEvent.setup();
|
||||
const legacyComponentCodes = "SPJ01/SPJ07/SPJ09/SPJ10";
|
||||
|
||||
@ -186,6 +186,7 @@ import {
|
||||
export { buildNonIntegratedBomDraftStorageKey } from "./workspaces/bom/bomDetailStateHelpers";
|
||||
import { syncProductPatentRecordFromProduct } from "./workspaces/patent/productPatentSyncHelpers";
|
||||
import {
|
||||
listSharedNonIntegratedProductPage,
|
||||
saveSharedNonIntegratedProduct,
|
||||
} from "./nonIntegratedApi";
|
||||
import { buildNonIntegratedBasicInfoOptions } from "./nonIntegratedBasicInfoOptions";
|
||||
@ -432,6 +433,10 @@ export default function NonIntegratedDetailClient({
|
||||
const [spuCreateMode, setSpuCreateMode] =
|
||||
useState<SpuCreateMode>("auto");
|
||||
const [selectedExistingSpu, setSelectedExistingSpu] = useState("");
|
||||
const [existingSpuRemoteQuery, setExistingSpuRemoteQuery] = useState("");
|
||||
const [remoteExistingSpuProducts, setRemoteExistingSpuProducts] = useState<
|
||||
NonIntegratedProductRecord[]
|
||||
>([]);
|
||||
const [stockProductSpu, setStockProductSpu] = useState("");
|
||||
const [formValues, setFormValues] = useState(() =>
|
||||
createFormValuesFromProduct(existingProduct),
|
||||
@ -1012,6 +1017,54 @@ export default function NonIntegratedDetailClient({
|
||||
seriesDraftSkuOptions,
|
||||
seriesNameOptions,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (
|
||||
mode !== "create" ||
|
||||
!canUseExistingSpuCreateMode ||
|
||||
spuCreateMode !== "existing" ||
|
||||
!shouldUseRemoteAssetStorage()
|
||||
) {
|
||||
setRemoteExistingSpuProducts([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedQuery = existingSpuRemoteQuery.trim();
|
||||
if (normalizedQuery.length < 2) {
|
||||
setRemoteExistingSpuProducts([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
listSharedNonIntegratedProductPage({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
productDevelopmentKind: "product",
|
||||
productKind: "stock",
|
||||
spu: normalizedQuery,
|
||||
})
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setRemoteExistingSpuProducts(data.products);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setRemoteExistingSpuProducts([]);
|
||||
}
|
||||
});
|
||||
}, 250);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [
|
||||
canUseExistingSpuCreateMode,
|
||||
existingSpuRemoteQuery,
|
||||
mode,
|
||||
spuCreateMode,
|
||||
]);
|
||||
const {
|
||||
existingSpuCreateOptions,
|
||||
existingSpuSelectOptions,
|
||||
@ -1021,6 +1074,7 @@ export default function NonIntegratedDetailClient({
|
||||
} = useExistingSpuCreateState({
|
||||
canUseExistingSpuCreateMode,
|
||||
existingSpuProducts,
|
||||
remoteExistingSpuProducts,
|
||||
selectedExistingSpu,
|
||||
spuCreateMode,
|
||||
spuEstimateRefreshToken,
|
||||
@ -4593,6 +4647,7 @@ export default function NonIntegratedDetailClient({
|
||||
selectedExistingSpuOption={
|
||||
Boolean(selectedExistingSpuOption)
|
||||
}
|
||||
onExistingSpuQueryChange={setExistingSpuRemoteQuery}
|
||||
seriesCreateEstimatedSpuLabel={
|
||||
seriesCreateEstimatedSpuLabel
|
||||
}
|
||||
|
||||
@ -488,6 +488,8 @@ export function SearchableInlineSelect({
|
||||
optionClassName = "flex w-full items-center rounded-apple-md px-2.5 py-1.5 text-left text-xs transition",
|
||||
includeEmptyOption,
|
||||
emptyOptionDisabled = false,
|
||||
maxVisibleOptions,
|
||||
onQueryChange,
|
||||
onChange,
|
||||
}: {
|
||||
ariaLabel: string;
|
||||
@ -502,6 +504,8 @@ export function SearchableInlineSelect({
|
||||
optionClassName?: string;
|
||||
includeEmptyOption?: boolean;
|
||||
emptyOptionDisabled?: boolean;
|
||||
maxVisibleOptions?: number;
|
||||
onQueryChange?: (value: string) => void;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const listId = useId();
|
||||
@ -528,15 +532,27 @@ export function SearchableInlineSelect({
|
||||
const normalizedDisplayValue = displayValue.trim().toLowerCase();
|
||||
const shouldFilterOptions =
|
||||
normalizedQuery.length > 0 && normalizedQuery !== normalizedDisplayValue;
|
||||
const visibleOptions = selectOptions.filter((option) => {
|
||||
const filteredOptions = selectOptions.filter((option) => {
|
||||
if (!shouldFilterOptions) return true;
|
||||
return (
|
||||
option.label.toLowerCase().includes(normalizedQuery) ||
|
||||
option.value.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
});
|
||||
const hasOptions = !readOnly && selectOptions.length > 0;
|
||||
const floatingPosition = useFloatingPanelPosition(isOpen && hasOptions, containerRef);
|
||||
const normalizedMaxVisibleOptions =
|
||||
typeof maxVisibleOptions === "number" && Number.isFinite(maxVisibleOptions)
|
||||
? Math.max(1, Math.floor(maxVisibleOptions))
|
||||
: null;
|
||||
const visibleOptions = normalizedMaxVisibleOptions
|
||||
? filteredOptions.slice(0, normalizedMaxVisibleOptions)
|
||||
: filteredOptions;
|
||||
const hasOptions =
|
||||
!readOnly && (selectOptions.length > 0 || Boolean(onQueryChange));
|
||||
const hasVisiblePanel = hasOptions && selectOptions.length > 0;
|
||||
const floatingPosition = useFloatingPanelPosition(
|
||||
isOpen && hasVisiblePanel,
|
||||
containerRef,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) return;
|
||||
@ -597,7 +613,9 @@ export function SearchableInlineSelect({
|
||||
onFocus={openPanel}
|
||||
onChange={(event) => {
|
||||
if (!hasOptions) return;
|
||||
setQuery(event.target.value);
|
||||
const nextQuery = event.target.value;
|
||||
setQuery(nextQuery);
|
||||
onQueryChange?.(nextQuery);
|
||||
setIsOpen(true);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
@ -641,7 +659,7 @@ export function SearchableInlineSelect({
|
||||
<ChevronDown className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
) : null}
|
||||
{hasOptions && isOpen && floatingPosition
|
||||
{hasVisiblePanel && isOpen && floatingPosition
|
||||
? createPortal(
|
||||
<div
|
||||
ref={panelRef}
|
||||
|
||||
@ -52,6 +52,7 @@ export function BasicInfoCreateSection({
|
||||
onOpeningReferenceFieldChange,
|
||||
onPreviewImageChange,
|
||||
onPreviewProductImage,
|
||||
onExistingSpuQueryChange,
|
||||
onSpuCreateModeChange,
|
||||
onStockProductSpuChange,
|
||||
onUpdateField,
|
||||
@ -89,6 +90,7 @@ export function BasicInfoCreateSection({
|
||||
) => void;
|
||||
onPreviewImageChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onPreviewProductImage: () => void;
|
||||
onExistingSpuQueryChange?: (value: string) => void;
|
||||
onSpuCreateModeChange: (value: SpuCreateMode) => void;
|
||||
onStockProductSpuChange: (value: string) => void;
|
||||
onUpdateField: (key: keyof FormValues, value: string) => void;
|
||||
@ -347,6 +349,8 @@ export function BasicInfoCreateSection({
|
||||
}
|
||||
readOnly={isReadOnly}
|
||||
onChange={onExistingSpuSelection}
|
||||
onQueryChange={onExistingSpuQueryChange}
|
||||
maxVisibleOptions={10}
|
||||
inputClassName={`h-8 w-full rounded-apple-md border border-apple-separator bg-apple-bg-secondary px-2 pr-8 text-xs text-apple-label-primary outline-none ${
|
||||
!selectedExistingSpuOption
|
||||
? "text-apple-label-secondary"
|
||||
|
||||
@ -0,0 +1,216 @@
|
||||
import {
|
||||
buildExistingSpuCreateOptions,
|
||||
buildExistingSpuCreateSkuItems,
|
||||
getMaxSkuSequenceFromProduct,
|
||||
} from "./existingSpuCreateOptions";
|
||||
import {
|
||||
createEmptyNonIntegratedOpeningReference,
|
||||
type NonIntegratedProductRecord,
|
||||
type NonIntegratedSkuItem,
|
||||
} from "../../core/nonIntegratedStorage";
|
||||
|
||||
function createSkuItem(
|
||||
overrides: Partial<NonIntegratedSkuItem> = {},
|
||||
): NonIntegratedSkuItem {
|
||||
return {
|
||||
id: "sku-1",
|
||||
skuSequence: 1,
|
||||
batchNo: 1,
|
||||
packageGroupId: "",
|
||||
packageSuffix: "",
|
||||
image: "",
|
||||
imageName: "",
|
||||
model: "",
|
||||
skuCode: "",
|
||||
setCount: "1",
|
||||
specName: "",
|
||||
barcode: "",
|
||||
size: "",
|
||||
color: "",
|
||||
material: "",
|
||||
boardMaterialType: "",
|
||||
versionNo: "V01",
|
||||
attributeCode: "",
|
||||
drawingId: "",
|
||||
installMethod: "",
|
||||
executeStandard: "",
|
||||
nationalStandardCompliant: "",
|
||||
manufacturer: "",
|
||||
manufacturerCode: "",
|
||||
minimumOrderQuantity: "",
|
||||
salesStatus: "",
|
||||
tagMaterial: "",
|
||||
model3dFileName: "",
|
||||
model3dDataUrl: "",
|
||||
componentDetails: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createProduct(
|
||||
overrides: Partial<NonIntegratedProductRecord> = {},
|
||||
): NonIntegratedProductRecord {
|
||||
return {
|
||||
id: "stock-product",
|
||||
productDevelopmentKind: "product",
|
||||
spu: "DNZ0384",
|
||||
projectStatus: "approved",
|
||||
projectReview: {
|
||||
reviewDate: "",
|
||||
reviewers: "",
|
||||
designerReviewer: "",
|
||||
purchaserReviewer: "",
|
||||
planningReviewer: "",
|
||||
operatorReviewer: "",
|
||||
conclusion: "",
|
||||
revisionNotes: "",
|
||||
decidedAt: "",
|
||||
},
|
||||
openingReference: createEmptyNonIntegratedOpeningReference(),
|
||||
year: "",
|
||||
demandFlowNo: "",
|
||||
processTemplate: "存量商品",
|
||||
productCategory: "书桌",
|
||||
salesCategory: "桌类",
|
||||
productPlanning: "",
|
||||
brandPlanning: "",
|
||||
demandDepartment: "",
|
||||
isSelfDevelopedPatentProduct: "",
|
||||
patentOwner: "",
|
||||
spuOwner: "",
|
||||
needsClearanceHandling: "",
|
||||
operationsOwner: "",
|
||||
profitCenter: "",
|
||||
salesChannel: "",
|
||||
salesPlatform: "",
|
||||
purchaseOwner: "",
|
||||
goodsType: "",
|
||||
materialType: "",
|
||||
productionType: "",
|
||||
manualProducer: "",
|
||||
shippingMode: "",
|
||||
visualDirector: "",
|
||||
itemId: "",
|
||||
priceBand: "",
|
||||
outputRequirementType: "",
|
||||
visualPlanning: "",
|
||||
visualPlanningType: "",
|
||||
photographer: "",
|
||||
graphicDesigner: "",
|
||||
designer3d: "",
|
||||
sampleRequired: "",
|
||||
flatOutputPath: "",
|
||||
output3dPath: "",
|
||||
brand: "其他品牌-普通大货",
|
||||
adaptableBrands: ["其他品牌-普通大货"],
|
||||
designer: "",
|
||||
operator: "",
|
||||
purchaser: "",
|
||||
previewImage: "",
|
||||
previewImageName: "",
|
||||
productCode: "DNZ0384",
|
||||
size: "",
|
||||
color: "",
|
||||
installMethod: "",
|
||||
installSteps: "",
|
||||
tagMaterial: "",
|
||||
packageDetail: "",
|
||||
craftDetail: "",
|
||||
styleDetails: [],
|
||||
bomDetails: [],
|
||||
reviewSheet: {
|
||||
version: "",
|
||||
factory: "",
|
||||
reviewDate: "",
|
||||
reviewMethod: "",
|
||||
productName: "",
|
||||
productImage: "",
|
||||
productImageName: "",
|
||||
size: "",
|
||||
material: "",
|
||||
barcode: "",
|
||||
reviewResult: "",
|
||||
color: "",
|
||||
issues: [],
|
||||
},
|
||||
qualityInspectionSheet: {
|
||||
rows: [],
|
||||
},
|
||||
skuItems: [],
|
||||
factoryRows: [],
|
||||
pricingRows: [],
|
||||
planningLayoutRows: [],
|
||||
productLimitRows: [],
|
||||
sampleMeasurementRows: [],
|
||||
finishedSampleShipmentRows: [],
|
||||
firstQualityRows: [],
|
||||
uploadFiles: [],
|
||||
documentWorkspaceConfirmations: {},
|
||||
formalSpuAssigned: true,
|
||||
createdAt: "2026-06-17T00:00:00.000Z",
|
||||
updatedAt: "2026-06-17T00:00:00.000Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("existingSpuCreateOptions", () => {
|
||||
it("includes stock product SPUs and keeps their max sku sequence", () => {
|
||||
const options = buildExistingSpuCreateOptions([
|
||||
createProduct({
|
||||
id: "jst-stock-dnz0384",
|
||||
spu: "DNZ0384",
|
||||
productCode: "DNZ0384",
|
||||
metadata: {
|
||||
source: "jst_sku",
|
||||
sourceSpu: "DNZ0384",
|
||||
},
|
||||
skuItems: [
|
||||
createSkuItem({
|
||||
skuSequence: 7,
|
||||
skuCode: "DNZ038407",
|
||||
}),
|
||||
createSkuItem({
|
||||
skuSequence: 12,
|
||||
skuCode: "GBPM-DNZ038412",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(options).toEqual([
|
||||
expect.objectContaining({
|
||||
value: "DNZ0384",
|
||||
maxSkuSequence: 12,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses structured skuSequence when imported stock codes do not match the SPU prefix", () => {
|
||||
const maxSequence = getMaxSkuSequenceFromProduct(
|
||||
createProduct({
|
||||
skuItems: [
|
||||
createSkuItem({
|
||||
skuSequence: 16,
|
||||
skuCode: "JST-OLD-SKU",
|
||||
barcode: "JST-OLD-BARCODE",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
"DNZ0384",
|
||||
);
|
||||
|
||||
expect(maxSequence).toBe(16);
|
||||
});
|
||||
|
||||
it("starts new SKU rows after the selected existing SPU max sequence", () => {
|
||||
const rows = buildExistingSpuCreateSkuItems(
|
||||
[
|
||||
createSkuItem({ id: "sku-a", skuSequence: 1 }),
|
||||
createSkuItem({ id: "sku-b", skuSequence: 2 }),
|
||||
],
|
||||
16,
|
||||
);
|
||||
|
||||
expect(rows.map((row) => row.skuSequence)).toEqual([17, 18]);
|
||||
});
|
||||
});
|
||||
@ -3,6 +3,7 @@ import {
|
||||
getNonIntegratedProductDisplayName,
|
||||
NON_INTEGRATED_DEFAULT_BRAND,
|
||||
normalizeNonIntegratedBrand,
|
||||
stripNonIntegratedBrandPrefix,
|
||||
type NonIntegratedProductRecord,
|
||||
} from "../../core/nonIntegratedStorage";
|
||||
|
||||
@ -25,11 +26,13 @@ export const EMPTY_EXISTING_SPU_CREATE_OPTIONS: ExistingSpuCreateOption[] = [];
|
||||
export function getSkuSequenceFromCode(skuCode: string, spu: string) {
|
||||
const normalizedSkuCode = skuCode.trim().toUpperCase();
|
||||
const normalizedSpu = spu.trim().toUpperCase();
|
||||
if (!normalizedSkuCode || !normalizedSpu) return 0;
|
||||
if (!normalizedSkuCode.startsWith(normalizedSpu)) return 0;
|
||||
const normalizedSkuCodeBase = stripNonIntegratedBrandPrefix(normalizedSkuCode);
|
||||
const normalizedSpuBase = stripNonIntegratedBrandPrefix(normalizedSpu);
|
||||
if (!normalizedSkuCodeBase || !normalizedSpuBase) return 0;
|
||||
if (!normalizedSkuCodeBase.startsWith(normalizedSpuBase)) return 0;
|
||||
|
||||
const sequenceMatch = normalizedSkuCode
|
||||
.slice(normalizedSpu.length)
|
||||
const sequenceMatch = normalizedSkuCodeBase
|
||||
.slice(normalizedSpuBase.length)
|
||||
.match(/^(\d+)/);
|
||||
if (!sequenceMatch) return 0;
|
||||
|
||||
@ -46,8 +49,6 @@ export function getMaxSkuSequenceFromProduct(
|
||||
getSkuSequenceFromCode(item.skuCode, spu),
|
||||
getSkuSequenceFromCode(item.barcode, spu),
|
||||
);
|
||||
if (parsedSequence <= 0) return maxSequence;
|
||||
|
||||
const structuredSequence =
|
||||
Number.isFinite(item.skuSequence) && item.skuSequence > 0
|
||||
? Math.trunc(item.skuSequence)
|
||||
@ -131,3 +132,30 @@ export function findExistingSpuCreateOption(
|
||||
|
||||
return options.find((option) => option.value === normalizedValue) ?? null;
|
||||
}
|
||||
|
||||
export function buildExistingSpuCreateSkuItems(
|
||||
items: NonIntegratedProductRecord["skuItems"],
|
||||
maxExistingSkuSequence: number,
|
||||
) {
|
||||
const startSequence =
|
||||
Number.isFinite(maxExistingSkuSequence) && maxExistingSkuSequence > 0
|
||||
? Math.trunc(maxExistingSkuSequence) + 1
|
||||
: 1;
|
||||
const groupSequenceMap = new Map<string, number>();
|
||||
let nextSequence = startSequence;
|
||||
|
||||
return items.map((item, index) => {
|
||||
const groupKey = item.packageGroupId.trim() || item.id || String(index);
|
||||
let sequence = groupSequenceMap.get(groupKey);
|
||||
if (!sequence) {
|
||||
sequence = nextSequence;
|
||||
groupSequenceMap.set(groupKey, sequence);
|
||||
nextSequence += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
skuSequence: sequence,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import type {
|
||||
ExistingSpuCreateOption,
|
||||
SpuCreateMode,
|
||||
} from "./existingSpuCreateOptions";
|
||||
import { buildExistingSpuCreateSkuItems } from "./existingSpuCreateOptions";
|
||||
import { BRAND_OPTIONS } from "./basicInfoConfig";
|
||||
import type { FormValues } from "./productFormValueHelpers";
|
||||
import { syncSkuItemsBrandPrefix } from "./skuBrandHelpers";
|
||||
@ -235,6 +236,9 @@ export function useBasicInfoFieldController({
|
||||
salesCategory: option.salesCategory || current.salesCategory,
|
||||
productCategory: option.productCategory || current.productCategory,
|
||||
}));
|
||||
setSkuItems((current) =>
|
||||
buildExistingSpuCreateSkuItems(current, option.maxSkuSequence),
|
||||
);
|
||||
setSaveMessage("");
|
||||
setSaveMessageType("success");
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
type UseExistingSpuCreateStateParams = {
|
||||
canUseExistingSpuCreateMode: boolean;
|
||||
existingSpuProducts: NonIntegratedProductRecord[];
|
||||
remoteExistingSpuProducts?: NonIntegratedProductRecord[];
|
||||
selectedExistingSpu: string;
|
||||
spuCreateMode: SpuCreateMode;
|
||||
spuEstimateRefreshToken: number;
|
||||
@ -21,6 +22,7 @@ type UseExistingSpuCreateStateParams = {
|
||||
export function useExistingSpuCreateState({
|
||||
canUseExistingSpuCreateMode,
|
||||
existingSpuProducts,
|
||||
remoteExistingSpuProducts = [],
|
||||
selectedExistingSpu,
|
||||
spuCreateMode,
|
||||
spuEstimateRefreshToken,
|
||||
@ -28,13 +30,15 @@ export function useExistingSpuCreateState({
|
||||
const existingSpuCreateOptions = useMemo(() => {
|
||||
if (!canUseExistingSpuCreateMode) return EMPTY_EXISTING_SPU_CREATE_OPTIONS;
|
||||
|
||||
const sourceProducts = existingSpuProducts.length
|
||||
? existingSpuProducts
|
||||
: listNonIntegratedProducts();
|
||||
const sourceProducts =
|
||||
existingSpuProducts.length || remoteExistingSpuProducts.length
|
||||
? [...existingSpuProducts, ...remoteExistingSpuProducts]
|
||||
: listNonIntegratedProducts();
|
||||
return buildExistingSpuCreateOptions(sourceProducts);
|
||||
}, [
|
||||
canUseExistingSpuCreateMode,
|
||||
existingSpuProducts,
|
||||
remoteExistingSpuProducts,
|
||||
spuEstimateRefreshToken,
|
||||
]);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user