merge: 解决冲突,保留使用 SharedAssetFamilyDTO 的类型修复

This commit is contained in:
WuYongJie 2026-06-12 17:30:49 +08:00
commit e7d2c8626c
24 changed files with 523 additions and 188 deletions

View File

@ -18,8 +18,7 @@ interface VariantDTO {
}
interface AssemblyWorkspaceSnapshot {
assetSeriesId?: string
[key: string]: unknown
assetSeriesId?: string | null
}
type InspectorTabKey = 'assembly' | 'ai'
@ -86,7 +85,7 @@ export default function AssistedAssemblyMobileView({
</div>
{/* 系列列表(未选择时显示) */}
{!selectedSeriesId && assetSeries.length > 0 && (
{!selectedAssetSeriesId && assetSeries.length > 0 && (
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
<div className="space-y-2 px-4 py-2">
{assetSeries.map((series) => (

View File

@ -34,6 +34,7 @@ import {
NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES,
replaceNonIntegratedProducts,
saveNonIntegratedProduct,
type NonIntegratedFactoryRow,
type NonIntegratedSkuItem,
type NonIntegratedProductRecord,
} from "./nonIntegratedStorage";
@ -454,6 +455,38 @@ function createSkuItem(
};
}
function createFactoryRow(
overrides: Partial<NonIntegratedFactoryRow> = {},
): NonIntegratedFactoryRow {
return {
skuId: "sku-test-1",
confirmed: false,
model: "SZ0230K01",
skuCode: "SZ023001",
setCount: "1",
barcode: "SZ023001N1280V01",
specName: "款式一 140/60/75",
size: "140/60/75",
color: "胡桃色",
material: "橡胶木",
installMethod: "拆装",
supplierName: "兰考华兰家具",
supplierCode: "N1280",
warehouse: "郑州仓",
shippingMethod: "入仓",
minimumOrderQuantity: "50",
taxIncludedUnitPrice: "",
leadTime: "30天",
assemblyMode: "",
productionType: "OEM",
installationService: "送货",
supplierCategory: "主力供应商",
purchaseOrderMethod: "订单式",
executeStandard: "GB/T 3324",
...overrides,
};
}
function createMaterialFinish(
overrides: Partial<MaterialDTO> & Pick<MaterialDTO, "id" | "name">,
): MaterialDTO {
@ -4446,7 +4479,7 @@ describe("NonIntegratedDetailClient", () => {
});
});
it("shares product factory selection fields by model without changing SKU content", async () => {
it("shares product factory selection fields by SPU without changing SKU content", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
@ -4573,12 +4606,12 @@ describe("NonIntegratedDetailClient", () => {
expect(savedProduct?.factoryRows.map((row) => row.warehouse)).toEqual([
"郑州仓",
"郑州仓",
"",
"郑州仓",
]);
expect(savedProduct?.factoryRows.map((row) => row.shippingMethod)).toEqual([
"代发",
"代发",
"",
"代发",
]);
expect(
savedProduct?.factoryRows.map((row) => row.minimumOrderQuantity),
@ -4586,7 +4619,7 @@ describe("NonIntegratedDetailClient", () => {
expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([
"OEM",
"OEM",
"",
"OEM",
]);
expect(
savedProduct?.factoryRows.map((row) => row.installationService),
@ -4594,11 +4627,11 @@ describe("NonIntegratedDetailClient", () => {
expect(savedProduct?.factoryRows.map((row) => row.supplierCategory)).toEqual([
"主力供应商",
"主力供应商",
"",
"主力供应商",
]);
expect(
savedProduct?.factoryRows.map((row) => row.purchaseOrderMethod),
).toEqual(["订单式", "订单式", ""]);
).toEqual(["订单式", "订单式", "订单式"]);
expect(savedProduct?.skuItems.map((item) => item.manufacturer)).toEqual([
"曹县嘉欢",
"曹县嘉欢",
@ -4640,17 +4673,17 @@ describe("NonIntegratedDetailClient", () => {
expect(savedProduct?.factoryRows.map((row) => row.warehouse)).toEqual([
"杭州仓",
"杭州仓",
"",
"杭州仓",
]);
expect(savedProduct?.factoryRows.map((row) => row.shippingMethod)).toEqual([
"入仓",
"入仓",
"",
"入仓",
]);
expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([
"ODM",
"ODM",
"",
"ODM",
]);
expect(savedProduct?.skuItems.map((item) => item.manufacturer)).toEqual([
"曹县嘉欢",
@ -4679,16 +4712,82 @@ describe("NonIntegratedDetailClient", () => {
expect(savedProduct?.factoryRows.map((row) => row.shippingMethod)).toEqual([
"入仓",
"入仓",
"",
"入仓",
]);
expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([
"ODM",
"ODM",
"",
"ODM",
]);
});
});
it("requires product factory rows to be manually confirmed after all fields are filled", async () => {
const actor = userEvent.setup();
const productId = "SZ0230-product-factory-manual-confirm";
saveNonIntegratedProduct(
createProductRecord({
id: productId,
spu: "SZ0230",
skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT,
skuItems: [
createSkuItem({
id: "sku-factory-confirm-1",
model: "SZ0230K01",
skuCode: "SZ023001",
barcode: "SZ023001N1280V01",
manufacturer: "兰考华兰家具",
manufacturerCode: "N1280",
installMethod: "拆装",
executeStandard: "GB/T 3324",
}),
],
factoryRows: [
createFactoryRow({
skuId: "sku-factory-confirm-1",
confirmed: false,
model: "SZ0230K01",
skuCode: "SZ023001",
barcode: "SZ023001N1280V01",
}),
],
documentWorkspaceConfirmations:
createDefaultDocumentWorkspaceConfirmations(false),
}),
);
render(
<NonIntegratedDetailClient mode="edit" productId={productId} />,
);
await actor.click(screen.getByTestId("document-workspace-trigger-产品定厂"));
const confirmButton = screen.getByTestId("product-factory-confirm");
expect(confirmButton).toHaveTextContent("资料已填完核对无误请确认。");
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(productId);
expect(savedProduct?.factoryRows?.[0]?.confirmed).toBe(false);
expect(savedProduct?.documentWorkspaceConfirmations?.产品定厂).not.toBe(
true,
);
expect(savedProduct?.documentWorkspaceApprovedAt?.产品定厂).toBeUndefined();
});
await actor.click(confirmButton);
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(productId);
expect(savedProduct?.factoryRows?.[0]?.confirmed).toBe(true);
expect(savedProduct?.documentWorkspaceConfirmations?.产品定厂).toBe(true);
expect(savedProduct?.documentWorkspaceApprovedAt?.产品定厂).toEqual(
expect.any(String),
);
});
expect(confirmButton).toHaveTextContent("已确认");
});
it("persists the SKU national standard compliance selection", async () => {
const actor = userEvent.setup();
@ -6447,8 +6546,16 @@ describe("NonIntegratedDetailClient", () => {
weightKg: "18.5",
},
]);
expect(savedProduct?.documentWorkspaceConfirmations?.样包测量).not.toBe(
true,
);
expect(savedProduct?.documentWorkspaceApprovedAt?.样包测量).toBeUndefined();
});
const confirmButton = screen.getByTestId("sample-measurement-confirm");
expect(confirmButton).toHaveTextContent("资料已填完核对无误请确认。");
await actor.click(confirmButton);
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-sample-measurement",
@ -6458,6 +6565,7 @@ describe("NonIntegratedDetailClient", () => {
expect.any(String),
);
});
expect(confirmButton).toHaveTextContent("已确认");
expect(
screen.getByTestId("sample-measurement-lengthCm-sku-sample-1"),

View File

@ -1370,7 +1370,9 @@ export default function NonIntegratedDetailClient({
isReadOnly || isSeriesSkuComponentAggregateMode;
const {
allFinishedSampleShipmentRowsComplete,
allProductFactoryRowsConfirmed,
allProductFactoryRowsComplete,
allSampleMeasurementRowsConfirmedByPricing,
allSampleMeasurementRowsComplete,
confirmedProductFactoryRowCount,
displayedFinishedSampleShipmentRowsComplete,
@ -1383,6 +1385,7 @@ export default function NonIntegratedDetailClient({
displayedProductPricingRowsAllSystemEntered,
displayedProductPricingRowsComplete,
displayedProductPricingRowsReadyForPush,
displayedSampleMeasurementRowsConfirmedByPricing,
displayedSampleMeasurementRowsComplete,
isSampleMeasurementSkuLocked,
planningLayoutHasContent,
@ -1460,6 +1463,9 @@ export default function NonIntegratedDetailClient({
const activeDocumentWorkspaceHasContent =
(documentWorkspaceCountByCategory[activeDocumentWorkspaceCategory] ?? 0) >
0;
const displayedSampleMeasurementHasPendingRows =
displayedProductSampleMeasurementRows.length > 0 &&
!displayedSampleMeasurementRowsComplete;
const {
basicInfoDisplayCount,
basicInfoIsApproved,
@ -1476,19 +1482,19 @@ export default function NonIntegratedDetailClient({
isWorkflowNodeConfirmed,
} = useNonIntegratedWorkflowConfirmationState({
allFinishedSampleShipmentRowsComplete,
allProductFactoryRowsComplete,
allSampleMeasurementRowsComplete,
allProductFactoryRowsConfirmed,
allSampleMeasurementRowsConfirmedByPricing,
basicInfoHasContent,
displayedFinishedSampleShipmentRowsComplete,
displayedFirstQualityRowsComplete,
displayedProductFactoryIsConfirmed,
displayedProductLimitRowsComplete,
displayedProductSampleMeasurementRows,
displayedSampleMeasurementRowsConfirmedByPricing,
displayedSampleMeasurementRowsComplete,
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
documentWorkspaceRevisionSignatures,
isSampleMeasurementSkuLocked,
previousDocumentWorkspaceRevisionSignatures:
documentWorkspaceRevisionSignatureRef.current,
productPricingCanBeConfirmed,
@ -1498,20 +1504,13 @@ export default function NonIntegratedDetailClient({
});
useNonIntegratedWorkflowAutoConfirmationEffects({
allFinishedSampleShipmentRowsComplete,
allProductFactoryRowsComplete,
allSampleMeasurementRowsComplete,
existingProductCreatedAt,
factoryRows,
finishedSampleShipmentRows,
isReadOnly,
mode,
productFactoryRows,
productId,
sampleMeasurementRows,
setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations,
setFactoryRows,
supplierCodeByName,
});
useNonIntegratedWorkflowRevisionResetEffect({
documentWorkspaceRevisionSignatureRef,
@ -1803,6 +1802,7 @@ export default function NonIntegratedDetailClient({
documentWorkspaceConfirmations;
const nextDocumentWorkspaceApprovedAt =
overrides?.documentWorkspaceApprovedAt ?? documentWorkspaceApprovedAt;
const nextFactoryRows = overrides?.factoryRows ?? productFactoryRows;
const nextReviewRounds = overrides?.reviewRounds ?? reviewRounds;
const nextFinishedSampleShipmentRows =
overrides?.finishedSampleShipmentRows ?? productFinishedSampleShipmentRows;
@ -1810,6 +1810,8 @@ export default function NonIntegratedDetailClient({
overrides?.openingReference ?? openingReferenceRef.current;
const nextSkuItems = overrides?.skuItems ?? skuItems;
const nextPricingRows = overrides?.pricingRows ?? productPricingRows;
const nextSampleMeasurementRows =
overrides?.sampleMeasurementRows ?? productSampleMeasurementRows;
const nextSkuBatchCount = overrides?.skuBatchCount ?? skuBatchCount;
const nextSkuBatchConfirmedAt =
overrides?.skuBatchConfirmedAt ?? skuBatchConfirmedAt;
@ -1874,11 +1876,11 @@ export default function NonIntegratedDetailClient({
bomDetails,
nextReviewRounds,
qualityInspectionSheet,
productFactoryRows,
nextFactoryRows,
nextPricingRows,
nextPlanningLayoutRows,
productLimitRows,
productSampleMeasurementRows,
nextSampleMeasurementRows,
nextFinishedSampleShipmentRows,
productFirstQualityRows,
uploadFiles,
@ -2585,13 +2587,16 @@ export default function NonIntegratedDetailClient({
derivedSkuItems,
displayedProductFactoryHasPendingRows,
displayedProductFactoryHasSystemEnteredPricingRows,
displayedProductFactoryIsConfirmed,
displayedProductFactoryRows,
displayedProductFactoryRowsComplete,
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
documentWorkspaceRevisionSignatureRef,
isCurrentUserAdmin,
isReadOnly,
isSkuItemFactoryConfirmed,
persistEditedProduct,
productFactoryModelAutofillRef,
productFactoryReopenField,
productFactoryRows,
@ -2631,7 +2636,7 @@ export default function NonIntegratedDetailClient({
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
documentWorkspaceRevisionSignatureRef,
isProductFactorySkuComplete,
isProductFactorySkuConfirmed: isSkuItemFactoryConfirmed,
isProductLimitNodeLocked,
isProductPricingNodeLocked,
isReadOnly,
@ -2643,6 +2648,8 @@ export default function NonIntegratedDetailClient({
planningLayoutRowsRef,
pricingRows,
productId,
productSampleMeasurementRows,
sampleMeasurementRows,
scheduleAutoSave,
setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations,
@ -2879,11 +2886,20 @@ export default function NonIntegratedDetailClient({
handleFinishedSampleShipmentStatusClick,
handleSampleMeasurementStatusClick,
} = useSampleWorkflowStatusController({
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
documentWorkspaceRevisionSignatureRef,
displayedFinishedSampleShipmentRows,
displayedFinishedSampleShipmentRowsComplete,
displayedProductFactoryIsConfirmed,
displayedProductSampleMeasurementRows,
displayedSampleMeasurementIsConfirmed,
displayedSampleMeasurementRowsComplete,
isReadOnly,
persistEditedProduct,
productSampleMeasurementRows,
setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations,
setSaveMessage,
setSaveMessageType,
});
@ -3820,7 +3836,7 @@ export default function NonIntegratedDetailClient({
allRows: productSampleMeasurementRows,
displayedRows: displayedProductSampleMeasurementRows,
isReadOnly,
isProductFactorySkuComplete,
isProductFactorySkuConfirmed: isSkuItemFactoryConfirmed,
isSampleMeasurementSkuLocked,
onUpdateRow: updateSampleMeasurementRow,
},
@ -4320,9 +4336,12 @@ export default function NonIntegratedDetailClient({
documentWorkspaceCountByCategory
}
documentWorkspaceView={documentWorkspaceView}
isProductFactoryRowComplete={isProductFactoryRowComplete}
isSampleMeasurementConfirmed={
displayedSampleMeasurementIsConfirmed
}
isWorkflowNodeConfirmed={isWorkflowNodeConfirmed}
productFactoryRows={productFactoryRows}
sampleMeasurementRows={productSampleMeasurementRows}
visibleGroupedUploadFiles={visibleGroupedUploadFiles}
onOpenBasicInfo={() => setDocumentWorkspaceView("detail")}
onOpenDocumentWorkspace={handleOpenDocumentWorkspace}
@ -4781,6 +4800,7 @@ export default function NonIntegratedDetailClient({
displayedProductPricingRowsReadyForPush,
displayedProductSampleMeasurementRowCount:
displayedProductSampleMeasurementRows.length,
displayedSampleMeasurementHasPendingRows,
displayedSampleMeasurementIsConfirmed,
documentWorkspaceConfirmationAction:
renderDocumentWorkspaceConfirmationAction(),

View File

@ -2,6 +2,7 @@ import type { MaterialDTO } from "@/server/services";
import type {
NonIntegratedDocumentWorkspaceApprovalMap,
NonIntegratedDocumentWorkspaceConfirmationMap,
NonIntegratedFactoryRow,
NonIntegratedFinishedSampleShipmentRow,
NonIntegratedOpeningReference,
NonIntegratedProductRecord,
@ -11,6 +12,7 @@ import type {
NonIntegratedReviewIssue,
NonIntegratedReviewRound,
NonIntegratedReviewSheet,
NonIntegratedSampleMeasurementRow,
NonIntegratedSkuItem,
} from "./core/nonIntegratedStorage";
import type {
@ -58,6 +60,8 @@ export type AutoSaveNoticeType = "saving" | "success" | "error";
export type PersistEditedProductOverrides = {
documentWorkspaceConfirmations?: NonIntegratedDocumentWorkspaceConfirmationMap;
documentWorkspaceApprovedAt?: NonIntegratedDocumentWorkspaceApprovalMap;
factoryRows?: NonIntegratedFactoryRow[];
sampleMeasurementRows?: NonIntegratedSampleMeasurementRow[];
reviewRounds?: NonIntegratedReviewRound[];
finishedSampleShipmentRows?: NonIntegratedFinishedSampleShipmentRow[];
openingReference?: NonIntegratedOpeningReference;

View File

@ -1,89 +1,41 @@
import { useEffect, type Dispatch, type SetStateAction } from "react";
import {
FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY,
PRODUCT_FACTORY_WORKSPACE_CATEGORY,
SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY,
} from "./workspaces/documents/documentWorkspaceConfig";
import type { DocumentWorkspaceCategory } from "./workspaces/documents/documentWorkspaceHelpers";
import {
isProductFactoryRowComplete,
normalizeFactoryRowsForPayload,
} from "./workspaces/basic-info/productFactoryHelpers";
import type {
NonIntegratedDocumentWorkspaceApprovalMap,
NonIntegratedDocumentWorkspaceConfirmationMap,
NonIntegratedFactoryRow,
NonIntegratedFinishedSampleShipmentRow,
NonIntegratedSampleMeasurementRow,
} from "./core/nonIntegratedStorage";
type DetailMode = "create" | "edit" | "view";
type UseNonIntegratedWorkflowAutoConfirmationEffectsParams = {
allFinishedSampleShipmentRowsComplete: boolean;
allProductFactoryRowsComplete: boolean;
allSampleMeasurementRowsComplete: boolean;
existingProductCreatedAt: string;
factoryRows: NonIntegratedFactoryRow[];
finishedSampleShipmentRows: NonIntegratedFinishedSampleShipmentRow[];
isReadOnly: boolean;
mode: DetailMode;
productFactoryRows: NonIntegratedFactoryRow[];
productId?: string;
sampleMeasurementRows: NonIntegratedSampleMeasurementRow[];
setDocumentWorkspaceApprovedAt: Dispatch<
SetStateAction<NonIntegratedDocumentWorkspaceApprovalMap>
>;
setDocumentWorkspaceConfirmations: Dispatch<
SetStateAction<NonIntegratedDocumentWorkspaceConfirmationMap>
>;
setFactoryRows: Dispatch<SetStateAction<NonIntegratedFactoryRow[]>>;
supplierCodeByName: Map<string, string>;
};
export function useNonIntegratedWorkflowAutoConfirmationEffects({
allFinishedSampleShipmentRowsComplete,
allProductFactoryRowsComplete,
allSampleMeasurementRowsComplete,
existingProductCreatedAt,
factoryRows,
finishedSampleShipmentRows,
isReadOnly,
mode,
productFactoryRows,
productId,
sampleMeasurementRows,
setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations,
setFactoryRows,
supplierCodeByName,
}: UseNonIntegratedWorkflowAutoConfirmationEffectsParams) {
useEffect(() => {
if (isReadOnly || factoryRows.length === 0) return;
const normalizedRows = normalizeFactoryRowsForPayload(
productFactoryRows,
supplierCodeByName,
);
const hasConfirmationChange = normalizedRows.some(
(row) => row.confirmed !== isProductFactoryRowComplete(row),
);
if (!hasConfirmationChange) return;
setFactoryRows(
normalizedRows.map((row) => ({
...row,
confirmed: isProductFactoryRowComplete(row),
})),
);
}, [
factoryRows.length,
isReadOnly,
productFactoryRows,
setFactoryRows,
supplierCodeByName,
]);
useEffect(() => {
if (
mode !== "edit" ||
@ -99,17 +51,6 @@ export function useNonIntegratedWorkflowAutoConfirmationEffects({
touched: boolean;
complete: boolean;
}> = [
{
category: PRODUCT_FACTORY_WORKSPACE_CATEGORY,
touched: factoryRows.length > 0,
complete: allProductFactoryRowsComplete,
},
{
category: SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY,
touched: sampleMeasurementRows.length > 0,
complete:
allProductFactoryRowsComplete && allSampleMeasurementRowsComplete,
},
{
category: FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY,
touched: finishedSampleShipmentRows.length > 0,
@ -149,15 +90,11 @@ export function useNonIntegratedWorkflowAutoConfirmationEffects({
});
}, [
allFinishedSampleShipmentRowsComplete,
allProductFactoryRowsComplete,
allSampleMeasurementRowsComplete,
existingProductCreatedAt,
factoryRows.length,
finishedSampleShipmentRows.length,
isReadOnly,
mode,
productId,
sampleMeasurementRows.length,
setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations,
]);

View File

@ -56,7 +56,7 @@ export function useNonIntegratedWorkflowCompletion({
productSampleMeasurementRows: NonIntegratedSampleMeasurementRow[];
}) {
const confirmedProductFactoryRowCount = useMemo(
() => productFactoryRows.filter(isProductFactoryRowComplete).length,
() => productFactoryRows.filter((row) => row.confirmed).length,
[productFactoryRows],
);
const populatedProductPricingRowCount = useMemo(
@ -109,7 +109,10 @@ export function useNonIntegratedWorkflowCompletion({
displayedProductFactoryRows.some((row) => !isProductFactoryRowComplete(row));
const displayedProductFactoryIsConfirmed =
displayedProductFactoryRows.length > 0 &&
!displayedProductFactoryHasPendingRows;
displayedProductFactoryRows.every((row) => row.confirmed);
const allProductFactoryRowsConfirmed =
productFactoryRows.length > 0 &&
productFactoryRows.every((row) => row.confirmed);
const displayedProductFactoryRowsComplete =
displayedProductFactoryRows.length > 0 &&
displayedProductFactoryRows.every(isProductFactoryRowComplete);
@ -162,6 +165,16 @@ export function useNonIntegratedWorkflowCompletion({
),
[productPricingRows],
);
const allSampleMeasurementRowsConfirmedByPricing =
productSampleMeasurementRows.length > 0 &&
productSampleMeasurementRows.every((row) =>
productPricingSystemEnteredSkuIdSet.has(row.skuId),
);
const displayedSampleMeasurementRowsConfirmedByPricing =
displayedProductSampleMeasurementRows.length > 0 &&
displayedProductSampleMeasurementRows.every((row) =>
productPricingSystemEnteredSkuIdSet.has(row.skuId),
);
const displayedProductFactoryHasSystemEnteredPricingRows =
displayedProductFactoryRows.some((row) =>
productPricingSystemEnteredSkuIdSet.has(row.skuId),
@ -179,7 +192,10 @@ export function useNonIntegratedWorkflowCompletion({
return {
allFinishedSampleShipmentRowsComplete,
allProductFactoryRowsConfirmed,
allProductFactoryRowsComplete,
allProductPricingRowsSystemEntered,
allSampleMeasurementRowsConfirmedByPricing,
allSampleMeasurementRowsComplete,
confirmedProductFactoryRowCount,
displayedFinishedSampleShipmentRowsComplete,
@ -192,6 +208,7 @@ export function useNonIntegratedWorkflowCompletion({
displayedProductPricingRowsAllSystemEntered,
displayedProductPricingRowsComplete,
displayedProductPricingRowsReadyForPush,
displayedSampleMeasurementRowsConfirmedByPricing,
displayedSampleMeasurementRowsComplete,
isSampleMeasurementSkuLocked,
planningLayoutHasContent,

View File

@ -25,19 +25,19 @@ import {
type UseNonIntegratedWorkflowConfirmationStateParams = {
allFinishedSampleShipmentRowsComplete: boolean;
allProductFactoryRowsComplete: boolean;
allSampleMeasurementRowsComplete: boolean;
allProductFactoryRowsConfirmed: boolean;
allSampleMeasurementRowsConfirmedByPricing: boolean;
basicInfoHasContent: boolean;
displayedFinishedSampleShipmentRowsComplete: boolean;
displayedFirstQualityRowsComplete: boolean;
displayedProductFactoryIsConfirmed: boolean;
displayedProductLimitRowsComplete: boolean;
displayedProductSampleMeasurementRows: Array<{ skuId: string }>;
displayedSampleMeasurementRowsConfirmedByPricing: boolean;
displayedSampleMeasurementRowsComplete: boolean;
documentWorkspaceApprovedAt: NonIntegratedDocumentWorkspaceApprovalMap;
documentWorkspaceConfirmations: NonIntegratedDocumentWorkspaceConfirmationMap;
documentWorkspaceRevisionSignatures: Record<WorkflowNodeCategory, string>;
isSampleMeasurementSkuLocked: (skuId: string) => boolean;
previousDocumentWorkspaceRevisionSignatures:
| Record<WorkflowNodeCategory, string>
| null
@ -54,19 +54,19 @@ type UseNonIntegratedWorkflowConfirmationStateParams = {
export function useNonIntegratedWorkflowConfirmationState({
allFinishedSampleShipmentRowsComplete,
allProductFactoryRowsComplete,
allSampleMeasurementRowsComplete,
allProductFactoryRowsConfirmed,
allSampleMeasurementRowsConfirmedByPricing,
basicInfoHasContent,
displayedFinishedSampleShipmentRowsComplete,
displayedFirstQualityRowsComplete,
displayedProductFactoryIsConfirmed,
displayedProductLimitRowsComplete,
displayedProductSampleMeasurementRows,
displayedSampleMeasurementRowsConfirmedByPricing,
displayedSampleMeasurementRowsComplete,
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
documentWorkspaceRevisionSignatures,
isSampleMeasurementSkuLocked,
previousDocumentWorkspaceRevisionSignatures,
productPricingCanBeConfirmed,
projectStatus,
@ -92,13 +92,15 @@ export function useNonIntegratedWorkflowConfirmationState({
const isWorkflowNodeConfirmed = useCallback(
(category: WorkflowNodeCategory, hasContent: boolean) => {
if (category === PRODUCT_FACTORY_WORKSPACE_CATEGORY) {
return hasContent && allProductFactoryRowsComplete;
return hasContent && allProductFactoryRowsConfirmed;
}
if (category === SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY) {
if (!hasContent || !allProductFactoryRowsConfirmed) return false;
if (allSampleMeasurementRowsConfirmedByPricing) return true;
return (
hasContent &&
allProductFactoryRowsComplete &&
allSampleMeasurementRowsComplete
documentWorkspaceConfirmations[category] === true &&
Boolean(documentWorkspaceApprovedAt[category]) &&
!dirtyWorkflowNodeCategories.has(category)
);
}
if (category === FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY) {
@ -110,12 +112,6 @@ export function useNonIntegratedWorkflowConfirmationState({
) {
return false;
}
if (
category === SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY &&
!displayedProductFactoryIsConfirmed
) {
return false;
}
return (
!shouldRequireWorkflowConfirmation(category) ||
@ -127,10 +123,9 @@ export function useNonIntegratedWorkflowConfirmationState({
},
[
allFinishedSampleShipmentRowsComplete,
allProductFactoryRowsComplete,
allSampleMeasurementRowsComplete,
allProductFactoryRowsConfirmed,
allSampleMeasurementRowsConfirmedByPricing,
dirtyWorkflowNodeCategories,
displayedProductFactoryIsConfirmed,
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
productPricingCanBeConfirmed,
@ -142,11 +137,6 @@ export function useNonIntegratedWorkflowConfirmationState({
Boolean(documentWorkspaceApprovedAt[category]),
[documentWorkspaceApprovedAt, documentWorkspaceConfirmations],
);
const isSampleMeasurementNodeLocked =
displayedProductSampleMeasurementRows.length > 0 &&
displayedProductSampleMeasurementRows.every((row) =>
isSampleMeasurementSkuLocked(row.skuId),
);
const isFinishedSampleShipmentNodeLocked = isDocumentWorkspaceNodeLocked(
FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY,
);
@ -156,12 +146,20 @@ export function useNonIntegratedWorkflowConfirmationState({
const isProductLimitNodeLocked = isDocumentWorkspaceNodeLocked(
PRODUCT_LIMIT_WORKSPACE_CATEGORY,
);
const displayedSampleMeasurementIsConfirmed = isWorkflowNodeConfirmed(
SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY,
displayedSampleMeasurementRowsComplete,
);
const displayedSampleMeasurementHasContent =
displayedProductSampleMeasurementRows.length > 0;
const displayedSampleMeasurementStoredConfirmed =
documentWorkspaceConfirmations[SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY] ===
true &&
Boolean(documentWorkspaceApprovedAt[SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY]) &&
!dirtyWorkflowNodeCategories.has(SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY);
const displayedSampleMeasurementIsConfirmed =
displayedSampleMeasurementHasContent &&
displayedProductFactoryIsConfirmed &&
(displayedSampleMeasurementRowsConfirmedByPricing ||
displayedSampleMeasurementStoredConfirmed);
const isSampleMeasurementNodeApproved =
isSampleMeasurementNodeLocked &&
displayedSampleMeasurementIsConfirmed &&
!dirtyWorkflowNodeCategories.has(SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY);
const displayedFinishedSampleShipmentIsConfirmed = isWorkflowNodeConfirmed(
FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY,

View File

@ -128,7 +128,7 @@ export function useNonIntegratedWorkflowRows({
() =>
new Set(
productFactoryRows
.filter((row) => row.confirmed || isProductFactoryRowComplete(row))
.filter((row) => row.confirmed)
.map((row) => row.skuId),
),
[productFactoryRows],

View File

@ -15,7 +15,7 @@ type SampleMeasurementWorkspaceProps = {
displayedRows: ProductSampleMeasurementDisplayRow[];
inheritedSkuBatchTabs: ReactNode;
isReadOnly: boolean;
isProductFactorySkuComplete: (skuId: string) => boolean;
isProductFactorySkuConfirmed: (skuId: string) => boolean;
isSampleMeasurementSkuLocked: (skuId: string) => boolean;
onUpdateRow: (
skuId: string,
@ -50,7 +50,7 @@ export function SampleMeasurementWorkspace({
displayedRows,
inheritedSkuBatchTabs,
isReadOnly,
isProductFactorySkuComplete,
isProductFactorySkuConfirmed,
isSampleMeasurementSkuLocked,
onUpdateRow,
}: SampleMeasurementWorkspaceProps) {
@ -64,7 +64,7 @@ export function SampleMeasurementWorkspace({
placeholder={placeholder}
readOnly={
isReadOnly ||
!isProductFactorySkuComplete(row.skuId) ||
!isProductFactorySkuConfirmed(row.skuId) ||
isSampleMeasurementSkuLocked(row.skuId)
}
mode="singleLineResizable"

View File

@ -11,6 +11,7 @@ import {
MODEL_SHARED_PRODUCT_FACTORY_KEYS,
PACKAGE_SHARED_PRODUCT_FACTORY_PURCHASING_KEYS,
PRODUCT_FACTORY_REQUIRED_FIELDS,
SPU_SHARED_PRODUCT_FACTORY_KEYS,
} from "./skuFieldConfig";
import { buildAutoBarcode, shouldKeepCustomBarcode } from "./skuAutoHelpers";
import { SUPPLIER_CODE_BY_NAME } from "./supplierDirectory";
@ -84,6 +85,8 @@ export function normalizeModelSharedProductFactoryRows(
});
return rows.map((row) => {
if (row.confirmed) return row;
const modelKey = getSkuFactoryModelKey(row);
if (!modelKey) return row;
@ -101,6 +104,40 @@ export function normalizeModelSharedProductFactoryRows(
});
}
export function normalizeSpuSharedProductFactoryRows(
rows: NonIntegratedFactoryRow[],
) {
const sharedValueByKey = new Map<string, string>();
SPU_SHARED_PRODUCT_FACTORY_KEYS.forEach((key) => {
const sharedValue = rows.find((row) => row[key].trim())?.[key].trim() ?? "";
sharedValueByKey.set(key, sharedValue);
});
return rows.map((row) => {
if (row.confirmed) return row;
let nextRow = row;
SPU_SHARED_PRODUCT_FACTORY_KEYS.forEach((key) => {
const sharedValue = sharedValueByKey.get(key);
if (sharedValue === undefined || nextRow[key] === sharedValue) return;
nextRow = {
...nextRow,
[key]: sharedValue,
};
});
return nextRow;
});
}
export function normalizeSharedProductFactoryRows(
rows: NonIntegratedFactoryRow[],
) {
return normalizeSpuSharedProductFactoryRows(
normalizeModelSharedProductFactoryRows(rows),
);
}
export function getProductFactoryMissingFieldLabels(
row: NonIntegratedFactoryRow,
) {
@ -123,7 +160,7 @@ export function getProductFactoryStatusMessage({
if (rows.length === 0) return null;
if (isRowsComplete) {
return {
message: "产品定厂已完成",
message: "资料已填完核对无误请确认。",
type: "success" as const,
};
}
@ -249,7 +286,7 @@ export function buildProductFactoryRows(
);
});
return normalizeModelSharedProductFactoryRows(rows);
return normalizeSharedProductFactoryRows(rows);
}
export function normalizeFactoryRowsForPayload(
@ -294,5 +331,5 @@ export function normalizeFactoryRowsForPayload(
};
});
return normalizeModelSharedProductFactoryRows(normalizedRows);
return normalizeSharedProductFactoryRows(normalizedRows);
}

View File

@ -161,10 +161,13 @@ export const PACKAGE_SHARED_PRODUCT_FACTORY_PURCHASING_KEYS =
"assemblyMode",
]);
export const MODEL_SHARED_PRODUCT_FACTORY_KEYS = new Set<ProductFactoryEditableKey>([
"installationService",
]);
export const SPU_SHARED_PRODUCT_FACTORY_KEYS = new Set<ProductFactoryEditableKey>([
"warehouse",
"shippingMethod",
"installMethod",
"productionType",
"installationService",
"supplierCategory",
"purchaseOrderMethod",
]);

View File

@ -28,8 +28,6 @@ export const PRODUCT_LIMIT_WORKSPACE_CATEGORY: DocumentWorkspaceCategory =
export const PLANNING_LAYOUT_WORKSPACE_CATEGORY: DocumentWorkspaceCategory =
NON_INTEGRATED_PLANNING_LAYOUT_WORKSPACE_CATEGORY;
export const AUTO_COMPLETED_WORKFLOW_CATEGORIES = new Set<WorkflowNodeCategory>([
PRODUCT_FACTORY_WORKSPACE_CATEGORY,
SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY,
FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY,
]);
export const FACTORY_QUALITY_WORKSPACE_CATEGORY: DocumentWorkspaceCategory =

View File

@ -10,6 +10,7 @@ type ProductFactoryHeaderActionsProps = {
isReadOnly: boolean;
rowCount: number;
hasPendingRows: boolean;
isConfirmed: boolean;
hasSystemEnteredPricingRows: boolean;
isReopenPickerOpen: boolean;
reopenField: ProductFactoryEditableKey;
@ -26,6 +27,7 @@ export function ProductFactoryHeaderActions({
isReadOnly,
rowCount,
hasPendingRows,
isConfirmed,
hasSystemEnteredPricingRows,
isReopenPickerOpen,
reopenField,
@ -98,10 +100,15 @@ export function ProductFactoryHeaderActions({
) : null}
<Button
size="small"
data-testid="product-factory-confirm"
onClick={onStatusClick}
disabled={rowCount === 0}
>
{hasPendingRows ? "当前资料未填完" : "已完成"}
{hasPendingRows
? "当前资料未填完"
: isConfirmed
? "已确认"
: "资料已填完核对无误请确认。"}
</Button>
</div>
);

View File

@ -141,14 +141,18 @@ export function ProductFactoryInfoTable({
<td className="px-2 py-2">
<span
className={`inline-flex whitespace-nowrap rounded-full px-2 py-0.5 text-[11px] ${
isProductFactoryRowComplete(row)
row.confirmed
? "bg-[#dff7e8] text-[#137a38]"
: "bg-[#fff3df] text-[#9b6511]"
: isProductFactoryRowComplete(row)
? "bg-[#e7f1ff] text-[#1764ad]"
: "bg-[#fff3df] text-[#9b6511]"
}`}
>
{isProductFactoryRowComplete(row)
? "已完成"
: "当前资料未填完"}
{row.confirmed
? "已确认"
: isProductFactoryRowComplete(row)
? "资料已填完"
: "当前资料未填完"}
</span>
</td>
<td className="px-2 py-2 text-apple-label-secondary">

View File

@ -40,14 +40,18 @@ export function ProductFactorySkuInfoTable({
<td className="px-3 py-2">
<span
className={`inline-flex rounded-full px-2 py-0.5 text-[11px] ${
isProductFactoryRowComplete(row)
row.confirmed
? "bg-[#dff7e8] text-[#137a38]"
: "bg-[#fff3df] text-[#9b6511]"
: isProductFactoryRowComplete(row)
? "bg-[#e7f1ff] text-[#1764ad]"
: "bg-[#fff3df] text-[#9b6511]"
}`}
>
{isProductFactoryRowComplete(row)
? "已完成"
: "当前资料未填完"}
{row.confirmed
? "已确认"
: isProductFactoryRowComplete(row)
? "资料已填完"
: "当前资料未填完"}
</span>
</td>
<td className="px-3 py-2">{row.model || "-"}</td>

View File

@ -6,6 +6,7 @@ import {
MODEL_AUTOFILL_PRODUCT_FACTORY_KEYS,
MODEL_SHARED_PRODUCT_FACTORY_KEYS,
PACKAGE_SHARED_PRODUCT_FACTORY_KEYS,
SPU_SHARED_PRODUCT_FACTORY_KEYS,
type ProductFactoryEditableKey,
type ProductFactoryModelAutofillEntry,
} from "../basic-info/skuFieldConfig";
@ -48,6 +49,10 @@ export function getProductFactoryTargetSkuIds(
const targetItem = items.find((item) => item.id === targetId);
if (!targetItem) return new Set([targetId]);
if (SPU_SHARED_PRODUCT_FACTORY_KEYS.has(key)) {
return new Set(items.map((item) => item.id));
}
if (MODEL_SHARED_PRODUCT_FACTORY_KEYS.has(key)) {
const targetModelKey = getSkuFactoryModelKey(targetItem);
if (!targetModelKey) return new Set([targetId]);

View File

@ -4,6 +4,7 @@ import type {
SetStateAction,
} from "react";
import type { PersistEditedProductOverrides } from "../../nonIntegratedDetailTypes";
import type {
NonIntegratedDocumentWorkspaceApprovalMap,
NonIntegratedDocumentWorkspaceConfirmationMap,
@ -16,10 +17,15 @@ import {
type ProductFactoryEditableKey,
type ProductFactoryModelAutofillEntry,
} from "../basic-info/skuFieldConfig";
import { normalizeFactorySupplier, getProductFactoryStatusMessage } from "../basic-info/productFactoryHelpers";
import {
getProductFactoryStatusMessage,
normalizeFactoryRowsForPayload,
normalizeFactorySupplier,
} from "../basic-info/productFactoryHelpers";
import { buildSkuItemsAfterSetCountCommit } from "../basic-info/skuSetCountCommitHelpers";
import { getStyleKeyFromModel } from "../style-assets/styleFileHelpers";
import { PRODUCT_FACTORY_WORKSPACE_CATEGORY } from "../documents/documentWorkspaceConfig";
import type { WorkflowNodeCategory } from "../documents/documentWorkspaceHelpers";
import {
confirmProductFactorySupplierRowsForSku,
reopenProductFactoryRows,
@ -35,18 +41,30 @@ type ApplySkuSupplierName = (
value: string,
) => NonIntegratedSkuItem;
type PersistEditedProduct = (params: {
successMessage: string;
errorFallbackMessage: string;
overrides?: PersistEditedProductOverrides;
suppressSuccessMessage?: boolean;
}) => Promise<boolean>;
type UseProductFactoryRowControllerParams = {
applySkuSupplierName: ApplySkuSupplierName;
derivedSkuItems: NonIntegratedSkuItem[];
displayedProductFactoryHasPendingRows: boolean;
displayedProductFactoryHasSystemEnteredPricingRows: boolean;
displayedProductFactoryIsConfirmed: boolean;
displayedProductFactoryRows: NonIntegratedFactoryRow[];
displayedProductFactoryRowsComplete: boolean;
documentWorkspaceApprovedAt: NonIntegratedDocumentWorkspaceApprovalMap;
documentWorkspaceConfirmations: NonIntegratedDocumentWorkspaceConfirmationMap;
documentWorkspaceRevisionSignatureRef: MutableRefObject<
Record<WorkflowNodeCategory, string> | null
>;
isCurrentUserAdmin: boolean;
isReadOnly: boolean;
isSkuItemFactoryConfirmed: (item: NonIntegratedSkuItem) => boolean;
persistEditedProduct: PersistEditedProduct;
productFactoryModelAutofillRef: MutableRefObject<
Map<string, ProductFactoryModelAutofillEntry>
>;
@ -73,13 +91,16 @@ export function useProductFactoryRowController({
derivedSkuItems,
displayedProductFactoryHasPendingRows,
displayedProductFactoryHasSystemEnteredPricingRows,
displayedProductFactoryIsConfirmed,
displayedProductFactoryRows,
displayedProductFactoryRowsComplete,
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
documentWorkspaceRevisionSignatureRef,
isCurrentUserAdmin,
isReadOnly,
isSkuItemFactoryConfirmed,
persistEditedProduct,
productFactoryModelAutofillRef,
productFactoryReopenField,
productFactoryRows,
@ -156,32 +177,32 @@ export function useProductFactoryRowController({
if (key === "installMethod") {
setSkuItems((current) => {
const targetItem = workflowSkuItems.find((item) => item.id === skuId);
if (!targetItem) return current;
const targetSkuIds = getProductFactoryTargetSkuIds(
workflowSkuItems,
skuId,
key,
);
const targetStyleKeys = new Set(
workflowSkuItems
.filter((item) => targetSkuIds.has(item.id))
.map((item) => getStyleKeyFromModel(item.model))
.filter(Boolean),
);
const targetStyleKey = getStyleKeyFromModel(targetItem.model);
if (targetStyleKey) {
const targetSkuIds = new Set(
workflowSkuItems
.filter((item) => getStyleKeyFromModel(item.model) === targetStyleKey)
.map((item) => item.id),
);
setStyleDetails((currentDetails) =>
updateStyleDetailEntry(currentDetails, targetStyleKey, {
installMethod: value,
}),
);
return current.map((item) =>
targetSkuIds.has(item.id)
? { ...item, installMethod: value }
: item,
);
if (targetStyleKeys.size > 0) {
setStyleDetails((currentDetails) => {
let nextDetails = currentDetails;
targetStyleKeys.forEach((styleKey) => {
nextDetails = updateStyleDetailEntry(nextDetails, styleKey, {
installMethod: value,
});
});
return nextDetails;
});
}
return current.map((item) =>
item.id === skuId ? { ...item, installMethod: value } : item,
targetSkuIds.has(item.id) ? { ...item, installMethod: value } : item,
);
});
}
@ -235,13 +256,65 @@ export function useProductFactoryRowController({
setSaveMessageType("success");
}
function handleProductFactoryStatusClick() {
async function handleProductFactoryStatusClick() {
const statusMessage = getProductFactoryStatusMessage({
isRowsComplete: displayedProductFactoryRowsComplete,
rows: displayedProductFactoryRows,
});
if (!statusMessage) return;
if (
!isReadOnly &&
displayedProductFactoryRowsComplete &&
!displayedProductFactoryIsConfirmed
) {
const targetSkuIds = new Set(
displayedProductFactoryRows.map((row) => row.skuId),
);
const normalizedRows = normalizeFactoryRowsForPayload(
productFactoryRows,
supplierCodeByName,
);
const nextRows = normalizedRows.map((row) =>
targetSkuIds.has(row.skuId) ? { ...row, confirmed: true } : row,
);
const allRowsConfirmed =
nextRows.length > 0 && nextRows.every((row) => row.confirmed);
const approvedAt = new Date().toISOString();
const nextConfirmations = {
...documentWorkspaceConfirmations,
[PRODUCT_FACTORY_WORKSPACE_CATEGORY]: allRowsConfirmed,
};
const nextApprovedAt = { ...documentWorkspaceApprovedAt };
if (allRowsConfirmed) {
nextApprovedAt[PRODUCT_FACTORY_WORKSPACE_CATEGORY] = approvedAt;
} else {
delete nextApprovedAt[PRODUCT_FACTORY_WORKSPACE_CATEGORY];
}
setFactoryRows(nextRows);
setDocumentWorkspaceConfirmations(nextConfirmations);
setDocumentWorkspaceApprovedAt(nextApprovedAt);
if (documentWorkspaceRevisionSignatureRef.current) {
documentWorkspaceRevisionSignatureRef.current = {
...documentWorkspaceRevisionSignatureRef.current,
[PRODUCT_FACTORY_WORKSPACE_CATEGORY]: JSON.stringify(nextRows),
};
}
await persistEditedProduct({
successMessage: allRowsConfirmed
? "产品定厂已确认"
: "当前批次产品定厂已确认",
errorFallbackMessage: "产品定厂确认保存失败",
overrides: {
factoryRows: nextRows,
documentWorkspaceConfirmations: nextConfirmations,
documentWorkspaceApprovedAt: nextApprovedAt,
},
});
return;
}
setSaveMessage(statusMessage.message);
setSaveMessageType(statusMessage.type);
}

View File

@ -20,7 +20,10 @@ import type {
NonIntegratedSkuItem,
} from "../../nonIntegratedStorage";
import type { FinishedSampleShipmentEditableKey } from "../basic-info/skuFieldConfig";
import { PRODUCT_PRICING_WORKSPACE_CATEGORY } from "../documents/documentWorkspaceConfig";
import {
PRODUCT_PRICING_WORKSPACE_CATEGORY,
SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY,
} from "../documents/documentWorkspaceConfig";
import type { WorkflowNodeCategory } from "../documents/documentWorkspaceHelpers";
import {
updateFirstQualityRowsForSku,
@ -77,7 +80,7 @@ type UseProductWorkflowRowControllerParams = {
documentWorkspaceRevisionSignatureRef: MutableRefObject<
Record<WorkflowNodeCategory, string> | null
>;
isProductFactorySkuComplete: (skuId: string) => boolean;
isProductFactorySkuConfirmed: (skuId: string) => boolean;
isProductLimitNodeLocked: boolean;
isProductPricingNodeLocked: boolean;
isReadOnly: boolean;
@ -89,6 +92,8 @@ type UseProductWorkflowRowControllerParams = {
planningLayoutRowsRef: MutableRefObject<NonIntegratedPlanningLayoutRow[]>;
pricingRows: NonIntegratedPricingRow[];
productId?: string;
productSampleMeasurementRows: NonIntegratedSampleMeasurementRow[];
sampleMeasurementRows: NonIntegratedSampleMeasurementRow[];
scheduleAutoSave: (overrides?: PersistEditedProductOverrides) => void;
setDocumentWorkspaceApprovedAt: Dispatch<
SetStateAction<NonIntegratedDocumentWorkspaceApprovalMap>
@ -128,7 +133,7 @@ export function useProductWorkflowRowController({
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
documentWorkspaceRevisionSignatureRef,
isProductFactorySkuComplete,
isProductFactorySkuConfirmed,
isProductLimitNodeLocked,
isProductPricingNodeLocked,
isReadOnly,
@ -140,6 +145,8 @@ export function useProductWorkflowRowController({
planningLayoutRowsRef,
pricingRows,
productId,
productSampleMeasurementRows,
sampleMeasurementRows,
scheduleAutoSave,
setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations,
@ -233,16 +240,25 @@ export function useProductWorkflowRowController({
});
const nextProductPricingCanBeConfirmed =
canConfirmProductPricingRows(nextPricingRows);
const nextSampleMeasurementCanBeConfirmedByPricing =
nextProductPricingCanBeConfirmed &&
productSampleMeasurementRows.length > 0;
const nextApprovedAt = nextProductPricingCanBeConfirmed
? {
...documentWorkspaceApprovedAt,
[PRODUCT_PRICING_WORKSPACE_CATEGORY]: pushedAt,
...(nextSampleMeasurementCanBeConfirmedByPricing
? { [SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY]: pushedAt }
: {}),
}
: documentWorkspaceApprovedAt;
const nextConfirmations = nextProductPricingCanBeConfirmed
? {
...documentWorkspaceConfirmations,
[PRODUCT_PRICING_WORKSPACE_CATEGORY]: true,
...(nextSampleMeasurementCanBeConfirmedByPricing
? { [SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY]: true }
: {}),
}
: documentWorkspaceConfirmations;
const pushSuccessMessage =
@ -257,6 +273,12 @@ export function useProductWorkflowRowController({
...documentWorkspaceRevisionSignatureRef.current,
[PRODUCT_PRICING_WORKSPACE_CATEGORY]:
JSON.stringify(nextPricingRows),
...(nextSampleMeasurementCanBeConfirmedByPricing
? {
[SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY]:
JSON.stringify(sampleMeasurementRows),
}
: {}),
};
}
}
@ -456,7 +478,7 @@ export function useProductWorkflowRowController({
) {
if (
isReadOnly ||
!isProductFactorySkuComplete(skuId) ||
!isProductFactorySkuConfirmed(skuId) ||
isSampleMeasurementSkuLocked(skuId)
)
return;

View File

@ -4,6 +4,7 @@ type SampleMeasurementHeaderActionsProps = {
actionGroupClassName: string;
rowCount: number;
isProductFactoryConfirmed: boolean;
hasPendingRows: boolean;
isSampleMeasurementConfirmed: boolean;
onStatusClick: () => void;
};
@ -12,6 +13,7 @@ export function SampleMeasurementHeaderActions({
actionGroupClassName,
rowCount,
isProductFactoryConfirmed,
hasPendingRows,
isSampleMeasurementConfirmed,
onStatusClick,
}: SampleMeasurementHeaderActionsProps) {
@ -19,6 +21,7 @@ export function SampleMeasurementHeaderActions({
<div className={actionGroupClassName}>
<Button
size="small"
data-testid="sample-measurement-confirm"
onClick={onStatusClick}
disabled={rowCount === 0}
title={
@ -28,8 +31,10 @@ export function SampleMeasurementHeaderActions({
{!isProductFactoryConfirmed
? "定厂未完成"
: isSampleMeasurementConfirmed
? "已完成"
: "当前资料未填完"}
? "已确认"
: hasPendingRows
? "当前资料未填完"
: "资料已填完核对无误请确认。"}
</Button>
</div>
);

View File

@ -153,7 +153,7 @@ export function getSampleMeasurementStatusMessage({
}
return {
message: "样包测量已完成",
message: "资料已填完核对无误请确认。",
type: "success" as const,
};
}

View File

@ -1,37 +1,77 @@
import type {
Dispatch,
MutableRefObject,
SetStateAction,
} from "react";
import type { PersistEditedProductOverrides } from "../../nonIntegratedDetailTypes";
import type {
NonIntegratedDocumentWorkspaceApprovalMap,
NonIntegratedDocumentWorkspaceConfirmationMap,
} from "../../nonIntegratedStorage";
import { SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY } from "../documents/documentWorkspaceConfig";
import type { WorkflowNodeCategory } from "../documents/documentWorkspaceHelpers";
import {
getFinishedSampleShipmentStatusMessage,
getSampleMeasurementStatusMessage,
normalizeSampleMeasurementRowsForPayload,
type ProductFinishedSampleShipmentDisplayRow,
type ProductSampleMeasurementDisplayRow,
} from "./sampleWorkflowHelpers";
type SaveMessageType = "success" | "error";
type PersistEditedProduct = (params: {
successMessage: string;
errorFallbackMessage: string;
overrides?: PersistEditedProductOverrides;
suppressSuccessMessage?: boolean;
}) => Promise<boolean>;
type UseSampleWorkflowStatusControllerParams = {
documentWorkspaceApprovedAt: NonIntegratedDocumentWorkspaceApprovalMap;
documentWorkspaceConfirmations: NonIntegratedDocumentWorkspaceConfirmationMap;
displayedFinishedSampleShipmentRows: ProductFinishedSampleShipmentDisplayRow[];
displayedFinishedSampleShipmentRowsComplete: boolean;
displayedProductFactoryIsConfirmed: boolean;
displayedProductSampleMeasurementRows: ProductSampleMeasurementDisplayRow[];
displayedSampleMeasurementIsConfirmed: boolean;
displayedSampleMeasurementRowsComplete: boolean;
documentWorkspaceRevisionSignatureRef: MutableRefObject<
Record<WorkflowNodeCategory, string> | null
>;
isReadOnly: boolean;
persistEditedProduct: PersistEditedProduct;
productSampleMeasurementRows: ProductSampleMeasurementDisplayRow[];
setDocumentWorkspaceApprovedAt: Dispatch<
SetStateAction<NonIntegratedDocumentWorkspaceApprovalMap>
>;
setDocumentWorkspaceConfirmations: Dispatch<
SetStateAction<NonIntegratedDocumentWorkspaceConfirmationMap>
>;
setSaveMessage: Dispatch<SetStateAction<string>>;
setSaveMessageType: Dispatch<SetStateAction<SaveMessageType>>;
};
export function useSampleWorkflowStatusController({
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
displayedFinishedSampleShipmentRows,
displayedFinishedSampleShipmentRowsComplete,
displayedProductFactoryIsConfirmed,
displayedProductSampleMeasurementRows,
displayedSampleMeasurementIsConfirmed,
displayedSampleMeasurementRowsComplete,
documentWorkspaceRevisionSignatureRef,
isReadOnly,
persistEditedProduct,
productSampleMeasurementRows,
setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations,
setSaveMessage,
setSaveMessageType,
}: UseSampleWorkflowStatusControllerParams) {
function handleSampleMeasurementStatusClick() {
async function handleSampleMeasurementStatusClick() {
const statusMessage = getSampleMeasurementStatusMessage({
isFactoryConfirmed: displayedProductFactoryIsConfirmed,
isRowsComplete: displayedSampleMeasurementRowsComplete,
@ -39,6 +79,47 @@ export function useSampleWorkflowStatusController({
});
if (!statusMessage) return;
if (
!isReadOnly &&
displayedProductFactoryIsConfirmed &&
displayedSampleMeasurementRowsComplete &&
!displayedSampleMeasurementIsConfirmed
) {
const approvedAt = new Date().toISOString();
const nextConfirmations = {
...documentWorkspaceConfirmations,
[SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY]: true,
};
const nextApprovedAt = {
...documentWorkspaceApprovedAt,
[SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY]: approvedAt,
};
const nextSampleMeasurementRows = normalizeSampleMeasurementRowsForPayload(
productSampleMeasurementRows,
);
setDocumentWorkspaceConfirmations(nextConfirmations);
setDocumentWorkspaceApprovedAt(nextApprovedAt);
if (documentWorkspaceRevisionSignatureRef.current) {
documentWorkspaceRevisionSignatureRef.current = {
...documentWorkspaceRevisionSignatureRef.current,
[SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY]: JSON.stringify(
nextSampleMeasurementRows,
),
};
}
await persistEditedProduct({
successMessage: "样包测量已确认",
errorFallbackMessage: "样包测量确认保存失败",
overrides: {
sampleMeasurementRows: nextSampleMeasurementRows,
documentWorkspaceConfirmations: nextConfirmations,
documentWorkspaceApprovedAt: nextApprovedAt,
},
});
return;
}
setSaveMessage(statusMessage.message);
setSaveMessageType(statusMessage.type);
}

View File

@ -54,6 +54,7 @@ export function DocumentWorkspaceHeaderActionsSlot({
displayedProductPricingRowsComplete,
displayedProductPricingRowsReadyForPush,
displayedProductSampleMeasurementRowCount,
displayedSampleMeasurementHasPendingRows,
displayedSampleMeasurementIsConfirmed,
documentWorkspaceConfirmationAction,
documentWorkspaceUploadInputRef,
@ -157,6 +158,7 @@ export function DocumentWorkspaceHeaderActionsSlot({
displayedProductPricingRowsComplete: boolean;
displayedProductPricingRowsReadyForPush: boolean;
displayedProductSampleMeasurementRowCount: number;
displayedSampleMeasurementHasPendingRows: boolean;
displayedSampleMeasurementIsConfirmed: boolean;
documentWorkspaceConfirmationAction: ReactNode;
documentWorkspaceUploadInputRef: RefObject<HTMLInputElement | null>;
@ -262,6 +264,7 @@ export function DocumentWorkspaceHeaderActionsSlot({
isReadOnly={isReadOnly}
rowCount={displayedProductFactoryRowCount}
hasPendingRows={displayedProductFactoryHasPendingRows}
isConfirmed={displayedProductFactoryIsConfirmed}
hasSystemEnteredPricingRows={
displayedProductFactoryHasSystemEnteredPricingRows
}
@ -282,6 +285,7 @@ export function DocumentWorkspaceHeaderActionsSlot({
actionGroupClassName={actionGroupClassName}
rowCount={displayedProductSampleMeasurementRowCount}
isProductFactoryConfirmed={displayedProductFactoryIsConfirmed}
hasPendingRows={displayedSampleMeasurementHasPendingRows}
isSampleMeasurementConfirmed={displayedSampleMeasurementIsConfirmed}
onStatusClick={onSampleMeasurementStatusClick}
/>

View File

@ -10,8 +10,10 @@ import {
import { DocumentWorkspaceSidebarStatusIcon } from "../../components/DocumentWorkspaceControls";
import type {
NonIntegratedFactoryRow,
NonIntegratedSampleMeasurementRow,
NonIntegratedUploadFile,
} from "../../nonIntegratedStorage";
import { SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY } from "../documents/documentWorkspaceConfig";
type DocumentWorkspaceView = "detail" | "document";
@ -27,9 +29,10 @@ export function DocumentWorkspaceSidebar({
basicInfoNeedsConfirmation,
documentWorkspaceCountByCategory,
documentWorkspaceView,
isProductFactoryRowComplete,
isWorkflowNodeConfirmed,
productFactoryRows,
sampleMeasurementRows,
isSampleMeasurementConfirmed,
visibleGroupedUploadFiles,
onOpenBasicInfo,
onOpenDocumentWorkspace,
@ -42,12 +45,13 @@ export function DocumentWorkspaceSidebar({
Record<DocumentWorkspaceCategory, number>
>;
documentWorkspaceView: DocumentWorkspaceView;
isProductFactoryRowComplete: (row: NonIntegratedFactoryRow) => boolean;
isWorkflowNodeConfirmed: (
category: DocumentWorkspaceCategory,
hasContent: boolean,
) => boolean;
productFactoryRows: NonIntegratedFactoryRow[];
sampleMeasurementRows: NonIntegratedSampleMeasurementRow[];
isSampleMeasurementConfirmed: boolean;
visibleGroupedUploadFiles: GroupedWorkspaceFiles[];
onOpenBasicInfo: () => void;
onOpenDocumentWorkspace: (category: DocumentWorkspaceCategory) => void;
@ -74,13 +78,14 @@ export function DocumentWorkspaceSidebar({
const categoryHasContent =
category === PRODUCT_FACTORY_WORKSPACE_CATEGORY
? productFactoryRows.length > 0
: category === SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY
? sampleMeasurementRows.length > 0
: hasFiles;
const categoryNeedsConfirmation =
category === PRODUCT_FACTORY_WORKSPACE_CATEGORY
? !(
productFactoryRows.length > 0 &&
productFactoryRows.every(isProductFactoryRowComplete)
)
? !productFactoryRows.every((row) => row.confirmed)
: category === SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY
? !isSampleMeasurementConfirmed
: needsConfirmation;
const shouldInsertBasicInfoTrigger = index === basicInfoInsertIndex;
const basicInfoIsActive = documentWorkspaceView === "detail";

View File

@ -8,7 +8,7 @@ import Input, { Textarea } from '@/components/ui/Input'
import Modal from '@/components/ui/Modal'
import Button from '@/components/ui/Button'
import Select from '@/components/ui/Select'
import { ToastContainer } from '@/components/ui/Toast'
import { ToastContainer, type ToastData } from '@/components/ui/Toast'
import type { ProductWithSkus } from '../home/home-client.types'
import type { LucideIcon } from 'lucide-react'
@ -63,12 +63,12 @@ export interface HomeMobileViewProps {
/* 删除产品 */
deletingProduct: ProductWithSkus | null
onDeleteProduct: () => void
onDeleteProduct: () => void | Promise<void>
onCloseDeleteProduct: () => void
/* 删除 SKU */
deletingSku: { product: ProductWithSkus; sku: { skuCode: string } } | null
onDeleteSKU: () => void
onDeleteSKU: () => void | Promise<void>
onCloseDeleteSKU: () => void
/* 添加 SKU */
@ -81,7 +81,7 @@ export interface HomeMobileViewProps {
onMarkHomeReturn: () => void
/* Toast */
toasts: Array<{ id: string; message: string; type?: string }>
toasts: ToastData[]
onRemoveToast: (id: string) => void
}
@ -406,7 +406,9 @@ export default function HomeMobileView({
<DeleteConfirmDialog
isOpen={!!deletingProduct}
onClose={onCloseDeleteProduct}
onConfirm={onDeleteProduct}
onConfirm={async () => {
await onDeleteProduct()
}}
title="删除产品"
description={
deletingProduct
@ -428,7 +430,9 @@ export default function HomeMobileView({
<DeleteConfirmDialog
isOpen={!!deletingSku}
onClose={onCloseDeleteSKU}
onConfirm={onDeleteSKU}
onConfirm={async () => {
await onDeleteSKU()
}}
title="删除 SKU"
description={
deletingSku