Compare commits
1 Commits
main
...
codex/wu-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15c082a712 |
@ -501,6 +501,7 @@ export function SearchableInlineSelect({
|
|||||||
emptyOptionDisabled = false,
|
emptyOptionDisabled = false,
|
||||||
maxVisibleOptions,
|
maxVisibleOptions,
|
||||||
onQueryChange,
|
onQueryChange,
|
||||||
|
clearValueOnUnselectedQuery = false,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
@ -517,6 +518,7 @@ export function SearchableInlineSelect({
|
|||||||
emptyOptionDisabled?: boolean;
|
emptyOptionDisabled?: boolean;
|
||||||
maxVisibleOptions?: number;
|
maxVisibleOptions?: number;
|
||||||
onQueryChange?: (value: string) => void;
|
onQueryChange?: (value: string) => void;
|
||||||
|
clearValueOnUnselectedQuery?: boolean;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const listId = useId();
|
const listId = useId();
|
||||||
@ -525,6 +527,7 @@ export function SearchableInlineSelect({
|
|||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const [hasUnselectedQuery, setHasUnselectedQuery] = useState(false);
|
||||||
const normalizedOptions = normalizeInlineSelectOptions(options);
|
const normalizedOptions = normalizeInlineSelectOptions(options);
|
||||||
const shouldIncludeEmptyOption = includeEmptyOption ?? false;
|
const shouldIncludeEmptyOption = includeEmptyOption ?? false;
|
||||||
const selectOptions = shouldIncludeEmptyOption
|
const selectOptions = shouldIncludeEmptyOption
|
||||||
@ -595,6 +598,7 @@ export function SearchableInlineSelect({
|
|||||||
const openPanel = useCallback(() => {
|
const openPanel = useCallback(() => {
|
||||||
if (!hasOptions) return;
|
if (!hasOptions) return;
|
||||||
setQuery(displayValue);
|
setQuery(displayValue);
|
||||||
|
setHasUnselectedQuery(false);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
window.setTimeout(() => inputRef.current?.select(), 0);
|
window.setTimeout(() => inputRef.current?.select(), 0);
|
||||||
}, [displayValue, hasOptions]);
|
}, [displayValue, hasOptions]);
|
||||||
@ -604,6 +608,7 @@ export function SearchableInlineSelect({
|
|||||||
if (option.disabled) return;
|
if (option.disabled) return;
|
||||||
onChange(option.value);
|
onChange(option.value);
|
||||||
setQuery(option.label);
|
setQuery(option.label);
|
||||||
|
setHasUnselectedQuery(false);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
},
|
},
|
||||||
[onChange],
|
[onChange],
|
||||||
@ -629,11 +634,13 @@ export function SearchableInlineSelect({
|
|||||||
if (!hasOptions) return;
|
if (!hasOptions) return;
|
||||||
const nextQuery = event.target.value;
|
const nextQuery = event.target.value;
|
||||||
setQuery(nextQuery);
|
setQuery(nextQuery);
|
||||||
|
setHasUnselectedQuery(true);
|
||||||
onQueryChange?.(nextQuery);
|
onQueryChange?.(nextQuery);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
setHasUnselectedQuery(false);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -650,6 +657,10 @@ export function SearchableInlineSelect({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
|
if (clearValueOnUnselectedQuery && hasUnselectedQuery) {
|
||||||
|
onChange("");
|
||||||
|
}
|
||||||
|
setHasUnselectedQuery(false);
|
||||||
setQuery(displayValue);
|
setQuery(displayValue);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { SUPPLIER_DIRECTORY } from "../supplierDirectory";
|
|||||||
import {
|
import {
|
||||||
InlineSelect,
|
InlineSelect,
|
||||||
InlineSuggestionInput,
|
InlineSuggestionInput,
|
||||||
|
SearchableInlineSelect,
|
||||||
} from "./InlineControls";
|
} from "./InlineControls";
|
||||||
import { SkuColumnResizableFrame } from "./SkuTableFields";
|
import { SkuColumnResizableFrame } from "./SkuTableFields";
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ export function SkuSupplierNameField({
|
|||||||
options = SUPPLIER_DIRECTORY.map((entry) => entry.name),
|
options = SUPPLIER_DIRECTORY.map((entry) => entry.name),
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
columnWidth = SKU_SUPPLIER_COLUMN_WIDTH,
|
columnWidth = SKU_SUPPLIER_COLUMN_WIDTH,
|
||||||
|
requireDirectorySelection = false,
|
||||||
onChange,
|
onChange,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onColumnResize,
|
onColumnResize,
|
||||||
@ -80,6 +82,7 @@ export function SkuSupplierNameField({
|
|||||||
options?: readonly string[];
|
options?: readonly string[];
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
columnWidth?: number;
|
columnWidth?: number;
|
||||||
|
requireDirectorySelection?: boolean;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onColumnResize?: (width: number) => void;
|
onColumnResize?: (width: number) => void;
|
||||||
@ -93,20 +96,37 @@ export function SkuSupplierNameField({
|
|||||||
readOnly ? "bg-apple-bg-primary" : "bg-apple-bg-secondary"
|
readOnly ? "bg-apple-bg-primary" : "bg-apple-bg-secondary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<InlineSuggestionInput
|
{requireDirectorySelection ? (
|
||||||
ariaLabel={"\u4f9b\u5e94\u5546\u540d\u79f0"}
|
<SearchableInlineSelect
|
||||||
value={value}
|
ariaLabel={"\u4f9b\u5e94\u5546\u540d\u79f0"}
|
||||||
options={options}
|
value={value}
|
||||||
placeholder={"\u641c\u7d22\u4f9b\u5e94\u5546\u540d\u79f0"}
|
options={options}
|
||||||
readOnly={readOnly}
|
placeholder={"\u641c\u7d22\u4f9b\u5e94\u5546\u540d\u79f0"}
|
||||||
filterOptions
|
readOnly={readOnly}
|
||||||
onChange={onChange}
|
maxVisibleOptions={10}
|
||||||
onCommit={onConfirm}
|
clearValueOnUnselectedQuery
|
||||||
inputClassName={`h-8 w-full border-0 bg-transparent px-2 pr-8 text-xs text-apple-label-primary outline-none ${
|
onChange={onChange}
|
||||||
readOnly ? "text-apple-label-secondary" : ""
|
inputClassName={`h-8 w-full border-0 bg-transparent px-2 pr-8 text-xs text-apple-label-primary outline-none ${
|
||||||
}`}
|
readOnly ? "text-apple-label-secondary" : ""
|
||||||
panelClassName="absolute left-0 top-[calc(100%+4px)] z-50 max-h-56 min-w-full overflow-auto rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-1 shadow-apple-md"
|
}`}
|
||||||
/>
|
panelClassName="absolute left-0 top-[calc(100%+4px)] z-50 max-h-56 min-w-full overflow-auto rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-1 shadow-apple-md"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<InlineSuggestionInput
|
||||||
|
ariaLabel={"\u4f9b\u5e94\u5546\u540d\u79f0"}
|
||||||
|
value={value}
|
||||||
|
options={options}
|
||||||
|
placeholder={"\u641c\u7d22\u4f9b\u5e94\u5546\u540d\u79f0"}
|
||||||
|
readOnly={readOnly}
|
||||||
|
filterOptions
|
||||||
|
onChange={onChange}
|
||||||
|
onCommit={onConfirm}
|
||||||
|
inputClassName={`h-8 w-full border-0 bg-transparent px-2 pr-8 text-xs text-apple-label-primary outline-none ${
|
||||||
|
readOnly ? "text-apple-label-secondary" : ""
|
||||||
|
}`}
|
||||||
|
panelClassName="absolute left-0 top-[calc(100%+4px)] z-50 max-h-56 min-w-full overflow-auto rounded-apple-lg border border-apple-separator bg-apple-bg-primary p-1 shadow-apple-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SkuColumnResizableFrame>
|
</SkuColumnResizableFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -124,31 +144,6 @@ export function FactoryWarehouseField({
|
|||||||
columnWidth?: number;
|
columnWidth?: number;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const handleWarehouseChange = (input: string) => {
|
|
||||||
const trimmed = input.trim();
|
|
||||||
|
|
||||||
// 精确匹配就直接用
|
|
||||||
if (options.includes(trimmed)) {
|
|
||||||
onChange(trimmed);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动纠正:如果输入不完整,尝试匹配唯一包含的选项
|
|
||||||
// 例如输入"河北源沃代发仓",自动匹配为"工厂-河北源沃代发仓"
|
|
||||||
const normalizedInput = trimmed.toLowerCase();
|
|
||||||
const containsMatches = options.filter((option) =>
|
|
||||||
option.toLowerCase().includes(normalizedInput),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (containsMatches.length === 1) {
|
|
||||||
onChange(containsMatches[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 没匹配到就原样传,让用户自己处理
|
|
||||||
onChange(trimmed);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkuColumnResizableFrame
|
<SkuColumnResizableFrame
|
||||||
columnWidth={columnWidth}
|
columnWidth={columnWidth}
|
||||||
@ -157,14 +152,15 @@ export function FactoryWarehouseField({
|
|||||||
readOnly ? "bg-apple-bg-primary" : "bg-apple-bg-secondary"
|
readOnly ? "bg-apple-bg-primary" : "bg-apple-bg-secondary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<InlineSuggestionInput
|
<SearchableInlineSelect
|
||||||
ariaLabel={"\u4ed3\u5e93"}
|
ariaLabel={"\u4ed3\u5e93"}
|
||||||
value={value}
|
value={value}
|
||||||
options={options}
|
options={options}
|
||||||
placeholder={"\u641c\u7d22\u4ed3\u5e93"}
|
placeholder={"\u641c\u7d22\u4ed3\u5e93"}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
filterOptions
|
maxVisibleOptions={10}
|
||||||
onChange={handleWarehouseChange}
|
clearValueOnUnselectedQuery
|
||||||
|
onChange={onChange}
|
||||||
inputClassName={`h-8 w-full border-0 bg-transparent px-2 pr-8 text-xs text-apple-label-primary outline-none ${
|
inputClassName={`h-8 w-full border-0 bg-transparent px-2 pr-8 text-xs text-apple-label-primary outline-none ${
|
||||||
readOnly ? "text-apple-label-secondary" : ""
|
readOnly ? "text-apple-label-secondary" : ""
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@ -432,6 +432,7 @@ export function SkuInfoRow({
|
|||||||
options={supplierNameOptions}
|
options={supplierNameOptions}
|
||||||
readOnly={skuRowReadOnly}
|
readOnly={skuRowReadOnly}
|
||||||
columnWidth={skuColumnWidths.supplierName}
|
columnWidth={skuColumnWidths.supplierName}
|
||||||
|
requireDirectorySelection
|
||||||
onChange={(value) => onUpdateSkuSupplierName(item.id, value)}
|
onChange={(value) => onUpdateSkuSupplierName(item.id, value)}
|
||||||
onConfirm={() => void onConfirmSkuSupplierName(item.id)}
|
onConfirm={() => void onConfirmSkuSupplierName(item.id)}
|
||||||
onColumnResize={(width) => onColumnResize("supplierName", width)}
|
onColumnResize={(width) => onColumnResize("supplierName", width)}
|
||||||
|
|||||||
@ -36,6 +36,22 @@ export function normalizeFactorySupplier(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeProductFactorySupplier(
|
||||||
|
value: string,
|
||||||
|
supplierCodeByName: ReadonlyMap<string, string> = SUPPLIER_CODE_BY_NAME,
|
||||||
|
) {
|
||||||
|
const normalizedName = value.trim();
|
||||||
|
const matchedSupplier = resolveSupplierEntryByName(
|
||||||
|
normalizedName,
|
||||||
|
supplierCodeByName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: matchedSupplier?.name ?? "",
|
||||||
|
code: matchedSupplier?.code?.trim().toUpperCase() ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function hasPackageSharedFactoryRowContent(
|
export function hasPackageSharedFactoryRowContent(
|
||||||
row: NonIntegratedFactoryRow,
|
row: NonIntegratedFactoryRow,
|
||||||
) {
|
) {
|
||||||
@ -207,13 +223,15 @@ export function createProductFactoryRowFromSku(
|
|||||||
const savedSupplierCode =
|
const savedSupplierCode =
|
||||||
existingRow?.supplierCode?.trim().toUpperCase() ?? "";
|
existingRow?.supplierCode?.trim().toUpperCase() ?? "";
|
||||||
const supplierSource = sku.manufacturer || savedSupplierName;
|
const supplierSource = sku.manufacturer || savedSupplierName;
|
||||||
const supplier = normalizeFactorySupplier(supplierSource, supplierCodeByName);
|
const supplier = normalizeProductFactorySupplier(
|
||||||
|
supplierSource,
|
||||||
|
supplierCodeByName,
|
||||||
|
);
|
||||||
const supplierCode =
|
const supplierCode =
|
||||||
supplier.code ||
|
supplier.code ||
|
||||||
sku.manufacturerCode.trim().toUpperCase() ||
|
sku.manufacturerCode.trim().toUpperCase() ||
|
||||||
savedSupplierCode;
|
savedSupplierCode;
|
||||||
const supplierName =
|
const supplierName = supplier.name;
|
||||||
supplierSource.trim() || supplier.name || sku.manufacturer;
|
|
||||||
const skuCode = sku.skuCode.trim();
|
const skuCode = sku.skuCode.trim();
|
||||||
const fallbackBarcode =
|
const fallbackBarcode =
|
||||||
sku.barcode.trim() || buildAutoBarcode(skuCode, supplierCode, sku.versionNo);
|
sku.barcode.trim() || buildAutoBarcode(skuCode, supplierCode, sku.versionNo);
|
||||||
@ -297,13 +315,12 @@ export function normalizeFactoryRowsForPayload(
|
|||||||
supplierCodeByName: ReadonlyMap<string, string> = SUPPLIER_CODE_BY_NAME,
|
supplierCodeByName: ReadonlyMap<string, string> = SUPPLIER_CODE_BY_NAME,
|
||||||
): NonIntegratedFactoryRow[] {
|
): NonIntegratedFactoryRow[] {
|
||||||
const normalizedRows = rows.map((row) => {
|
const normalizedRows = rows.map((row) => {
|
||||||
const supplier = normalizeFactorySupplier(
|
const supplier = normalizeProductFactorySupplier(
|
||||||
row.supplierName,
|
row.supplierName,
|
||||||
supplierCodeByName,
|
supplierCodeByName,
|
||||||
);
|
);
|
||||||
const supplierName = supplier.name || row.supplierName.trim();
|
const supplierName = supplier.name;
|
||||||
const supplierCode =
|
const supplierCode = supplier.code;
|
||||||
row.supplierCode.trim().toUpperCase() || supplier.code;
|
|
||||||
const skuCode = row.skuCode.trim();
|
const skuCode = row.skuCode.trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -186,6 +186,7 @@ export function ProductFactoryInfoTable({
|
|||||||
value={row.supplierName}
|
value={row.supplierName}
|
||||||
options={supplierNameOptions}
|
options={supplierNameOptions}
|
||||||
readOnly={isFactoryFieldReadOnly(row, "supplierName")}
|
readOnly={isFactoryFieldReadOnly(row, "supplierName")}
|
||||||
|
requireDirectorySelection
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
onUpdateRow(row.skuId, "supplierName", value)
|
onUpdateRow(row.skuId, "supplierName", value)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,15 @@ import { buildAutoBarcode } from "../basic-info/skuAutoHelpers";
|
|||||||
import {
|
import {
|
||||||
buildProductFactoryRows,
|
buildProductFactoryRows,
|
||||||
normalizeFactoryRowsForPayload,
|
normalizeFactoryRowsForPayload,
|
||||||
normalizeFactorySupplier,
|
normalizeProductFactorySupplier,
|
||||||
resolveAutoAwareBarcode,
|
resolveAutoAwareBarcode,
|
||||||
} from "../basic-info/productFactoryHelpers";
|
} from "../basic-info/productFactoryHelpers";
|
||||||
import { getProductFactoryModelAutofillKey } from "../basic-info/productFactoryKeyHelpers";
|
import { getProductFactoryModelAutofillKey } from "../basic-info/productFactoryKeyHelpers";
|
||||||
import { getProductFactoryTargetSkuIds } from "./productFactoryTargetSkuHelpers";
|
import { getProductFactoryTargetSkuIds } from "./productFactoryTargetSkuHelpers";
|
||||||
|
|
||||||
|
export const resolveProductFactoryDirectorySupplier =
|
||||||
|
normalizeProductFactorySupplier;
|
||||||
|
|
||||||
export function updateProductFactoryRowsForSku({
|
export function updateProductFactoryRowsForSku({
|
||||||
autofillEntries,
|
autofillEntries,
|
||||||
currentRows,
|
currentRows,
|
||||||
@ -55,8 +58,11 @@ export function updateProductFactoryRowsForSku({
|
|||||||
const versionNo = derivedSkuItem?.versionNo ?? "V01";
|
const versionNo = derivedSkuItem?.versionNo ?? "V01";
|
||||||
|
|
||||||
if (key === "supplierName") {
|
if (key === "supplierName") {
|
||||||
const supplier = normalizeFactorySupplier(value, supplierCodeByName);
|
const supplier = resolveProductFactoryDirectorySupplier(
|
||||||
const supplierName = supplier.name || value;
|
value,
|
||||||
|
supplierCodeByName,
|
||||||
|
);
|
||||||
|
const supplierName = supplier.name;
|
||||||
const supplierCode = supplier.code;
|
const supplierCode = supplier.code;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -121,13 +127,12 @@ export function confirmProductFactorySupplierRowsForSku({
|
|||||||
(item) => item.id === row.skuId,
|
(item) => item.id === row.skuId,
|
||||||
);
|
);
|
||||||
const versionNo = derivedSkuItem?.versionNo ?? "V01";
|
const versionNo = derivedSkuItem?.versionNo ?? "V01";
|
||||||
const supplier = normalizeFactorySupplier(
|
const supplier = resolveProductFactoryDirectorySupplier(
|
||||||
row.supplierName,
|
row.supplierName,
|
||||||
supplierCodeByName,
|
supplierCodeByName,
|
||||||
);
|
);
|
||||||
const supplierName = supplier.name || row.supplierName.trim();
|
const supplierName = supplier.name;
|
||||||
const supplierCode =
|
const supplierCode = supplier.code;
|
||||||
row.supplierCode.trim().toUpperCase() || supplier.code;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
getProductFactoryStatusMessage,
|
getProductFactoryStatusMessage,
|
||||||
normalizeFactoryRowsForPayload,
|
normalizeFactoryRowsForPayload,
|
||||||
normalizeFactorySupplier,
|
|
||||||
} from "../basic-info/productFactoryHelpers";
|
} from "../basic-info/productFactoryHelpers";
|
||||||
import { buildSkuItemsAfterSetCountCommit } from "../basic-info/skuSetCountCommitHelpers";
|
import { buildSkuItemsAfterSetCountCommit } from "../basic-info/skuSetCountCommitHelpers";
|
||||||
import { getStyleKeyFromModel } from "../style-assets/styleFileHelpers";
|
import { getStyleKeyFromModel } from "../style-assets/styleFileHelpers";
|
||||||
@ -29,6 +28,7 @@ import type { WorkflowNodeCategory } from "../documents/documentWorkspaceHelpers
|
|||||||
import {
|
import {
|
||||||
confirmProductFactorySupplierRowsForSku,
|
confirmProductFactorySupplierRowsForSku,
|
||||||
reopenProductFactoryRows,
|
reopenProductFactoryRows,
|
||||||
|
resolveProductFactoryDirectorySupplier,
|
||||||
updateProductFactoryRowsForSku,
|
updateProductFactoryRowsForSku,
|
||||||
} from "./productFactoryRowUpdateHelpers";
|
} from "./productFactoryRowUpdateHelpers";
|
||||||
import { getProductFactoryTargetSkuIds } from "./productFactoryTargetSkuHelpers";
|
import { getProductFactoryTargetSkuIds } from "./productFactoryTargetSkuHelpers";
|
||||||
@ -129,8 +129,11 @@ export function useProductFactoryRowController({
|
|||||||
if (currentFactoryRow?.confirmed) return;
|
if (currentFactoryRow?.confirmed) return;
|
||||||
|
|
||||||
if (key === "supplierName") {
|
if (key === "supplierName") {
|
||||||
const supplier = normalizeFactorySupplier(value, supplierCodeByName);
|
const supplier = resolveProductFactoryDirectorySupplier(
|
||||||
const supplierName = supplier.name || value;
|
value,
|
||||||
|
supplierCodeByName,
|
||||||
|
);
|
||||||
|
const supplierName = supplier.name;
|
||||||
|
|
||||||
setSkuItems((current) => {
|
setSkuItems((current) => {
|
||||||
const targetSkuIds = getProductFactoryTargetSkuIds(
|
const targetSkuIds = getProductFactoryTargetSkuIds(
|
||||||
@ -228,12 +231,11 @@ export function useProductFactoryRowController({
|
|||||||
if (isReadOnly) return;
|
if (isReadOnly) return;
|
||||||
const currentRow = productFactoryRows.find((row) => row.skuId === skuId);
|
const currentRow = productFactoryRows.find((row) => row.skuId === skuId);
|
||||||
if (currentRow?.confirmed) return;
|
if (currentRow?.confirmed) return;
|
||||||
const supplier = normalizeFactorySupplier(
|
const supplier = resolveProductFactoryDirectorySupplier(
|
||||||
currentRow?.supplierName ?? "",
|
currentRow?.supplierName ?? "",
|
||||||
supplierCodeByName,
|
supplierCodeByName,
|
||||||
);
|
);
|
||||||
const supplierName =
|
const supplierName = supplier.name;
|
||||||
supplier.name || currentRow?.supplierName.trim() || "";
|
|
||||||
|
|
||||||
if (supplierName) {
|
if (supplierName) {
|
||||||
setSkuItems((current) =>
|
setSkuItems((current) =>
|
||||||
|
|||||||
@ -138,6 +138,10 @@ jest.mock('../intake-non-integrated-operation-log.service', () => ({
|
|||||||
mockRecordIntakeWorkspaceConfirmationChange(...args),
|
mockRecordIntakeWorkspaceConfirmationChange(...args),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
jest.mock('../intake-directory-selection.service', () => ({
|
||||||
|
normalizeIntakeDirectorySelectionsForSave: jest.fn(async (product) => product),
|
||||||
|
}))
|
||||||
|
|
||||||
import { intakeNonIntegratedProductRepository } from '../../repositories/intake-non-integrated-product.repository'
|
import { intakeNonIntegratedProductRepository } from '../../repositories/intake-non-integrated-product.repository'
|
||||||
import { userRepository } from '../../repositories/user.repository'
|
import { userRepository } from '../../repositories/user.repository'
|
||||||
import { assetSkuGroupRepository, assetVariantRepository } from '../../repositories/asset-center.repository'
|
import { assetSkuGroupRepository, assetVariantRepository } from '../../repositories/asset-center.repository'
|
||||||
|
|||||||
@ -0,0 +1,142 @@
|
|||||||
|
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
|
||||||
|
|
||||||
|
import { normalizeIntakeDirectorySelections } from "./intake-directory-selection.service";
|
||||||
|
|
||||||
|
function createProduct(
|
||||||
|
overrides: Partial<NonIntegratedProductRecord> = {},
|
||||||
|
): NonIntegratedProductRecord {
|
||||||
|
return {
|
||||||
|
skuItems: [
|
||||||
|
{
|
||||||
|
id: "sku-1",
|
||||||
|
skuSequence: 1,
|
||||||
|
batchNo: 1,
|
||||||
|
packageGroupId: "",
|
||||||
|
packageSuffix: "",
|
||||||
|
image: "",
|
||||||
|
imageName: "",
|
||||||
|
model: "",
|
||||||
|
skuCode: "SKU-1",
|
||||||
|
setCount: "1",
|
||||||
|
specName: "",
|
||||||
|
barcode: "",
|
||||||
|
size: "",
|
||||||
|
color: "",
|
||||||
|
material: "",
|
||||||
|
versionNo: "V01",
|
||||||
|
attributeCode: "",
|
||||||
|
drawingId: "",
|
||||||
|
installMethod: "",
|
||||||
|
executeStandard: "",
|
||||||
|
nationalStandardCompliant: "",
|
||||||
|
manufacturer: "",
|
||||||
|
manufacturerCode: "",
|
||||||
|
salesStatus: "",
|
||||||
|
tagMaterial: "",
|
||||||
|
model3dFileName: "",
|
||||||
|
model3dDataUrl: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
factoryRows: [
|
||||||
|
{
|
||||||
|
skuId: "sku-1",
|
||||||
|
confirmed: false,
|
||||||
|
model: "",
|
||||||
|
skuCode: "SKU-1",
|
||||||
|
setCount: "1",
|
||||||
|
barcode: "",
|
||||||
|
specName: "",
|
||||||
|
size: "",
|
||||||
|
color: "",
|
||||||
|
material: "",
|
||||||
|
installMethod: "",
|
||||||
|
supplierName: "",
|
||||||
|
supplierCode: "",
|
||||||
|
warehouse: "",
|
||||||
|
shippingMethod: "",
|
||||||
|
minimumOrderQuantity: "",
|
||||||
|
taxIncludedUnitPrice: "",
|
||||||
|
leadTime: "",
|
||||||
|
assemblyMode: "",
|
||||||
|
productionType: "",
|
||||||
|
installationService: "",
|
||||||
|
supplierCategory: "",
|
||||||
|
purchaseOrderMethod: "",
|
||||||
|
executeStandard: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as NonIntegratedProductRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("intake directory selections", () => {
|
||||||
|
const directory = {
|
||||||
|
suppliers: [{ name: "阿凡达商贸", code: "N123" }],
|
||||||
|
warehouses: ["成品代发仓"],
|
||||||
|
};
|
||||||
|
|
||||||
|
it("only keeps an explicitly directory-backed supplier and warehouse", () => {
|
||||||
|
const product = createProduct({
|
||||||
|
skuItems: [
|
||||||
|
{
|
||||||
|
...createProduct().skuItems[0],
|
||||||
|
manufacturer: "阿",
|
||||||
|
manufacturerCode: "OLD",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
factoryRows: [
|
||||||
|
{
|
||||||
|
...createProduct().factoryRows[0],
|
||||||
|
supplierName: "阿",
|
||||||
|
supplierCode: "OLD",
|
||||||
|
warehouse: "不存在的仓库",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = normalizeIntakeDirectorySelections(product, directory);
|
||||||
|
|
||||||
|
expect(result.skuItems[0]).toEqual(
|
||||||
|
expect.objectContaining({ manufacturer: "", manufacturerCode: "" }),
|
||||||
|
);
|
||||||
|
expect(result.factoryRows[0]?.warehouse).toBe("");
|
||||||
|
expect(result.factoryRows[0]).toEqual(
|
||||||
|
expect.objectContaining({ supplierName: "", supplierCode: "" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores the selected directory value and its supplier code", () => {
|
||||||
|
const product = createProduct({
|
||||||
|
skuItems: [
|
||||||
|
{
|
||||||
|
...createProduct().skuItems[0],
|
||||||
|
manufacturer: "阿凡达商贸",
|
||||||
|
manufacturerCode: "OLD",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
factoryRows: [
|
||||||
|
{
|
||||||
|
...createProduct().factoryRows[0],
|
||||||
|
supplierName: "阿凡达商贸",
|
||||||
|
supplierCode: "OLD",
|
||||||
|
warehouse: "成品代发仓",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = normalizeIntakeDirectorySelections(product, directory);
|
||||||
|
|
||||||
|
expect(result.skuItems[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
manufacturer: "阿凡达商贸",
|
||||||
|
manufacturerCode: "N123",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result.factoryRows[0]?.warehouse).toBe("成品代发仓");
|
||||||
|
expect(result.factoryRows[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
supplierName: "阿凡达商贸",
|
||||||
|
supplierCode: "N123",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
|
||||||
|
|
||||||
|
import {
|
||||||
|
jstPlanningDirectoryService,
|
||||||
|
type JstPlanningDirectoryOptions,
|
||||||
|
} from "./jst-planning-directory.service";
|
||||||
|
|
||||||
|
function normalizeDirectoryKey(value: string) {
|
||||||
|
return value.trim().toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDirectoryBackedValues(product: NonIntegratedProductRecord) {
|
||||||
|
return (
|
||||||
|
product.skuItems.some(
|
||||||
|
(item) => item.manufacturer.trim() || item.manufacturerCode.trim(),
|
||||||
|
) ||
|
||||||
|
product.factoryRows.some(
|
||||||
|
(row) =>
|
||||||
|
row.supplierName.trim() ||
|
||||||
|
row.supplierCode.trim() ||
|
||||||
|
row.warehouse.trim(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeIntakeDirectorySelections<
|
||||||
|
TProduct extends NonIntegratedProductRecord,
|
||||||
|
>(product: TProduct, directory: JstPlanningDirectoryOptions): TProduct {
|
||||||
|
const suppliersByName = new Map(
|
||||||
|
directory.suppliers.map((supplier) => [
|
||||||
|
normalizeDirectoryKey(supplier.name),
|
||||||
|
{
|
||||||
|
name: supplier.name.trim(),
|
||||||
|
code: supplier.code.trim().toUpperCase(),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const warehousesByName = new Map(
|
||||||
|
directory.warehouses.map((warehouse) => [
|
||||||
|
normalizeDirectoryKey(warehouse),
|
||||||
|
warehouse.trim(),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...product,
|
||||||
|
skuItems: product.skuItems.map((item) => {
|
||||||
|
const supplier = suppliersByName.get(
|
||||||
|
normalizeDirectoryKey(item.manufacturer),
|
||||||
|
);
|
||||||
|
|
||||||
|
return supplier
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
manufacturer: supplier.name,
|
||||||
|
manufacturerCode: supplier.code,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...item,
|
||||||
|
manufacturer: "",
|
||||||
|
manufacturerCode: "",
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
factoryRows: product.factoryRows.map((row) => {
|
||||||
|
const supplier = suppliersByName.get(
|
||||||
|
normalizeDirectoryKey(row.supplierName),
|
||||||
|
);
|
||||||
|
const warehouse = warehousesByName.get(
|
||||||
|
normalizeDirectoryKey(row.warehouse),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
supplierName: supplier?.name ?? "",
|
||||||
|
supplierCode: supplier?.code ?? "",
|
||||||
|
warehouse: warehouse ?? "",
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function normalizeIntakeDirectorySelectionsForSave<
|
||||||
|
TProduct extends NonIntegratedProductRecord,
|
||||||
|
>(product: TProduct): Promise<TProduct> {
|
||||||
|
if (!hasDirectoryBackedValues(product)) return product;
|
||||||
|
|
||||||
|
const directory = await jstPlanningDirectoryService.listOptions();
|
||||||
|
return normalizeIntakeDirectorySelections(product, directory);
|
||||||
|
}
|
||||||
@ -95,6 +95,7 @@ import { skuQualityInspectionService } from './sku-quality-inspection.service'
|
|||||||
import { spuNumberReservationService } from './spu-number-reservation.service'
|
import { spuNumberReservationService } from './spu-number-reservation.service'
|
||||||
import { supplyChainProductArchiveSyncService } from './supply-chain-product-archive-sync.service'
|
import { supplyChainProductArchiveSyncService } from './supply-chain-product-archive-sync.service'
|
||||||
import { archiveChangeActorFromUser } from './supply-chain-product-archive-change-log.service'
|
import { archiveChangeActorFromUser } from './supply-chain-product-archive-change-log.service'
|
||||||
|
import { normalizeIntakeDirectorySelectionsForSave } from './intake-directory-selection.service'
|
||||||
|
|
||||||
type NonIntegratedUploadPresentationPreview = {
|
type NonIntegratedUploadPresentationPreview = {
|
||||||
pdfUrl: string
|
pdfUrl: string
|
||||||
@ -6620,7 +6621,7 @@ class IntakeNonIntegratedProductService {
|
|||||||
useExactFormalSpu: Boolean(existingFormalSpu),
|
useExactFormalSpu: Boolean(existingFormalSpu),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const normalizedProduct = buildPersistedProductPayload(
|
let normalizedProduct = buildPersistedProductPayload(
|
||||||
assignedProductInput,
|
assignedProductInput,
|
||||||
existingRecord?.payload,
|
existingRecord?.payload,
|
||||||
existingRecord?.createdAt.toISOString(),
|
existingRecord?.createdAt.toISOString(),
|
||||||
@ -6628,6 +6629,7 @@ class IntakeNonIntegratedProductService {
|
|||||||
if (!normalizedProduct) {
|
if (!normalizedProduct) {
|
||||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '非集成产品数据不完整,无法保存')
|
throw new AppError(ErrorCode.VALIDATION_FAILED, '非集成产品数据不完整,无法保存')
|
||||||
}
|
}
|
||||||
|
normalizedProduct = await normalizeIntakeDirectorySelectionsForSave(normalizedProduct)
|
||||||
assertNonIntegratedProductMasterDataReadyForFormalUse(normalizedProduct, {
|
assertNonIntegratedProductMasterDataReadyForFormalUse(normalizedProduct, {
|
||||||
allowedMissingFields:
|
allowedMissingFields:
|
||||||
normalizedProduct.skuItems.length === 1 ? ['spec_name'] : [],
|
normalizedProduct.skuItems.length === 1 ? ['spec_name'] : [],
|
||||||
@ -6966,7 +6968,7 @@ class IntakeNonIntegratedProductService {
|
|||||||
throw new AppError(ErrorCode.PRODUCT_NOT_FOUND, '非集成产品不存在')
|
throw new AppError(ErrorCode.PRODUCT_NOT_FOUND, '非集成产品不存在')
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedProduct = buildPersistedProductPayload(
|
let normalizedProduct = buildPersistedProductPayload(
|
||||||
productInput,
|
productInput,
|
||||||
existingRecordCandidate?.payload,
|
existingRecordCandidate?.payload,
|
||||||
existingRecordCandidate?.createdAt.toISOString(),
|
existingRecordCandidate?.createdAt.toISOString(),
|
||||||
@ -6974,6 +6976,7 @@ class IntakeNonIntegratedProductService {
|
|||||||
if (!normalizedProduct) {
|
if (!normalizedProduct) {
|
||||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '非集成产品数据不完整,无法保存')
|
throw new AppError(ErrorCode.VALIDATION_FAILED, '非集成产品数据不完整,无法保存')
|
||||||
}
|
}
|
||||||
|
normalizedProduct = await normalizeIntakeDirectorySelectionsForSave(normalizedProduct)
|
||||||
|
|
||||||
const normalizedSpu = resolveNonIntegratedPersistedSpu(normalizedProduct)
|
const normalizedSpu = resolveNonIntegratedPersistedSpu(normalizedProduct)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user