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

View File

@ -34,6 +34,7 @@ import {
NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES, NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES,
replaceNonIntegratedProducts, replaceNonIntegratedProducts,
saveNonIntegratedProduct, saveNonIntegratedProduct,
type NonIntegratedFactoryRow,
type NonIntegratedSkuItem, type NonIntegratedSkuItem,
type NonIntegratedProductRecord, type NonIntegratedProductRecord,
} from "./nonIntegratedStorage"; } 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( function createMaterialFinish(
overrides: Partial<MaterialDTO> & Pick<MaterialDTO, "id" | "name">, overrides: Partial<MaterialDTO> & Pick<MaterialDTO, "id" | "name">,
): MaterialDTO { ): 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(); const actor = userEvent.setup();
saveNonIntegratedProduct( saveNonIntegratedProduct(
@ -4573,12 +4606,12 @@ describe("NonIntegratedDetailClient", () => {
expect(savedProduct?.factoryRows.map((row) => row.warehouse)).toEqual([ expect(savedProduct?.factoryRows.map((row) => row.warehouse)).toEqual([
"郑州仓", "郑州仓",
"郑州仓", "郑州仓",
"", "郑州仓",
]); ]);
expect(savedProduct?.factoryRows.map((row) => row.shippingMethod)).toEqual([ expect(savedProduct?.factoryRows.map((row) => row.shippingMethod)).toEqual([
"代发", "代发",
"代发", "代发",
"", "代发",
]); ]);
expect( expect(
savedProduct?.factoryRows.map((row) => row.minimumOrderQuantity), savedProduct?.factoryRows.map((row) => row.minimumOrderQuantity),
@ -4586,7 +4619,7 @@ describe("NonIntegratedDetailClient", () => {
expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([ expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([
"OEM", "OEM",
"OEM", "OEM",
"", "OEM",
]); ]);
expect( expect(
savedProduct?.factoryRows.map((row) => row.installationService), 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.supplierCategory)).toEqual([
"主力供应商", "主力供应商",
"主力供应商", "主力供应商",
"", "主力供应商",
]); ]);
expect( expect(
savedProduct?.factoryRows.map((row) => row.purchaseOrderMethod), savedProduct?.factoryRows.map((row) => row.purchaseOrderMethod),
).toEqual(["订单式", "订单式", ""]); ).toEqual(["订单式", "订单式", "订单式"]);
expect(savedProduct?.skuItems.map((item) => item.manufacturer)).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.warehouse)).toEqual([
"杭州仓", "杭州仓",
"杭州仓", "杭州仓",
"", "杭州仓",
]); ]);
expect(savedProduct?.factoryRows.map((row) => row.shippingMethod)).toEqual([ expect(savedProduct?.factoryRows.map((row) => row.shippingMethod)).toEqual([
"入仓", "入仓",
"入仓", "入仓",
"", "入仓",
]); ]);
expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([ expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([
"ODM", "ODM",
"ODM", "ODM",
"", "ODM",
]); ]);
expect(savedProduct?.skuItems.map((item) => item.manufacturer)).toEqual([ 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.shippingMethod)).toEqual([
"入仓", "入仓",
"入仓", "入仓",
"", "入仓",
]); ]);
expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([ expect(savedProduct?.factoryRows.map((row) => row.productionType)).toEqual([
"ODM", "ODM",
"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 () => { it("persists the SKU national standard compliance selection", async () => {
const actor = userEvent.setup(); const actor = userEvent.setup();
@ -6447,8 +6546,16 @@ describe("NonIntegratedDetailClient", () => {
weightKg: "18.5", 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(() => { await waitFor(() => {
const savedProduct = getNonIntegratedProductById( const savedProduct = getNonIntegratedProductById(
"SZ0230-sample-measurement", "SZ0230-sample-measurement",
@ -6458,6 +6565,7 @@ describe("NonIntegratedDetailClient", () => {
expect.any(String), expect.any(String),
); );
}); });
expect(confirmButton).toHaveTextContent("已确认");
expect( expect(
screen.getByTestId("sample-measurement-lengthCm-sku-sample-1"), screen.getByTestId("sample-measurement-lengthCm-sku-sample-1"),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import {
MODEL_SHARED_PRODUCT_FACTORY_KEYS, MODEL_SHARED_PRODUCT_FACTORY_KEYS,
PACKAGE_SHARED_PRODUCT_FACTORY_PURCHASING_KEYS, PACKAGE_SHARED_PRODUCT_FACTORY_PURCHASING_KEYS,
PRODUCT_FACTORY_REQUIRED_FIELDS, PRODUCT_FACTORY_REQUIRED_FIELDS,
SPU_SHARED_PRODUCT_FACTORY_KEYS,
} from "./skuFieldConfig"; } from "./skuFieldConfig";
import { buildAutoBarcode, shouldKeepCustomBarcode } from "./skuAutoHelpers"; import { buildAutoBarcode, shouldKeepCustomBarcode } from "./skuAutoHelpers";
import { SUPPLIER_CODE_BY_NAME } from "./supplierDirectory"; import { SUPPLIER_CODE_BY_NAME } from "./supplierDirectory";
@ -84,6 +85,8 @@ export function normalizeModelSharedProductFactoryRows(
}); });
return rows.map((row) => { return rows.map((row) => {
if (row.confirmed) return row;
const modelKey = getSkuFactoryModelKey(row); const modelKey = getSkuFactoryModelKey(row);
if (!modelKey) return 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( export function getProductFactoryMissingFieldLabels(
row: NonIntegratedFactoryRow, row: NonIntegratedFactoryRow,
) { ) {
@ -123,7 +160,7 @@ export function getProductFactoryStatusMessage({
if (rows.length === 0) return null; if (rows.length === 0) return null;
if (isRowsComplete) { if (isRowsComplete) {
return { return {
message: "产品定厂已完成", message: "资料已填完核对无误请确认。",
type: "success" as const, type: "success" as const,
}; };
} }
@ -249,7 +286,7 @@ export function buildProductFactoryRows(
); );
}); });
return normalizeModelSharedProductFactoryRows(rows); return normalizeSharedProductFactoryRows(rows);
} }
export function normalizeFactoryRowsForPayload( 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", "assemblyMode",
]); ]);
export const MODEL_SHARED_PRODUCT_FACTORY_KEYS = new Set<ProductFactoryEditableKey>([ export const MODEL_SHARED_PRODUCT_FACTORY_KEYS = new Set<ProductFactoryEditableKey>([
"installationService",
]);
export const SPU_SHARED_PRODUCT_FACTORY_KEYS = new Set<ProductFactoryEditableKey>([
"warehouse", "warehouse",
"shippingMethod", "shippingMethod",
"installMethod",
"productionType", "productionType",
"installationService",
"supplierCategory", "supplierCategory",
"purchaseOrderMethod", "purchaseOrderMethod",
]); ]);

View File

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

View File

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

View File

@ -141,14 +141,18 @@ export function ProductFactoryInfoTable({
<td className="px-2 py-2"> <td className="px-2 py-2">
<span <span
className={`inline-flex whitespace-nowrap rounded-full px-2 py-0.5 text-[11px] ${ className={`inline-flex whitespace-nowrap rounded-full px-2 py-0.5 text-[11px] ${
isProductFactoryRowComplete(row) row.confirmed
? "bg-[#dff7e8] text-[#137a38]" ? "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> </span>
</td> </td>
<td className="px-2 py-2 text-apple-label-secondary"> <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"> <td className="px-3 py-2">
<span <span
className={`inline-flex rounded-full px-2 py-0.5 text-[11px] ${ className={`inline-flex rounded-full px-2 py-0.5 text-[11px] ${
isProductFactoryRowComplete(row) row.confirmed
? "bg-[#dff7e8] text-[#137a38]" ? "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> </span>
</td> </td>
<td className="px-3 py-2">{row.model || "-"}</td> <td className="px-3 py-2">{row.model || "-"}</td>

View File

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

View File

@ -4,6 +4,7 @@ import type {
SetStateAction, SetStateAction,
} from "react"; } from "react";
import type { PersistEditedProductOverrides } from "../../nonIntegratedDetailTypes";
import type { import type {
NonIntegratedDocumentWorkspaceApprovalMap, NonIntegratedDocumentWorkspaceApprovalMap,
NonIntegratedDocumentWorkspaceConfirmationMap, NonIntegratedDocumentWorkspaceConfirmationMap,
@ -16,10 +17,15 @@ import {
type ProductFactoryEditableKey, type ProductFactoryEditableKey,
type ProductFactoryModelAutofillEntry, type ProductFactoryModelAutofillEntry,
} from "../basic-info/skuFieldConfig"; } 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 { buildSkuItemsAfterSetCountCommit } from "../basic-info/skuSetCountCommitHelpers";
import { getStyleKeyFromModel } from "../style-assets/styleFileHelpers"; import { getStyleKeyFromModel } from "../style-assets/styleFileHelpers";
import { PRODUCT_FACTORY_WORKSPACE_CATEGORY } from "../documents/documentWorkspaceConfig"; import { PRODUCT_FACTORY_WORKSPACE_CATEGORY } from "../documents/documentWorkspaceConfig";
import type { WorkflowNodeCategory } from "../documents/documentWorkspaceHelpers";
import { import {
confirmProductFactorySupplierRowsForSku, confirmProductFactorySupplierRowsForSku,
reopenProductFactoryRows, reopenProductFactoryRows,
@ -35,18 +41,30 @@ type ApplySkuSupplierName = (
value: string, value: string,
) => NonIntegratedSkuItem; ) => NonIntegratedSkuItem;
type PersistEditedProduct = (params: {
successMessage: string;
errorFallbackMessage: string;
overrides?: PersistEditedProductOverrides;
suppressSuccessMessage?: boolean;
}) => Promise<boolean>;
type UseProductFactoryRowControllerParams = { type UseProductFactoryRowControllerParams = {
applySkuSupplierName: ApplySkuSupplierName; applySkuSupplierName: ApplySkuSupplierName;
derivedSkuItems: NonIntegratedSkuItem[]; derivedSkuItems: NonIntegratedSkuItem[];
displayedProductFactoryHasPendingRows: boolean; displayedProductFactoryHasPendingRows: boolean;
displayedProductFactoryHasSystemEnteredPricingRows: boolean; displayedProductFactoryHasSystemEnteredPricingRows: boolean;
displayedProductFactoryIsConfirmed: boolean;
displayedProductFactoryRows: NonIntegratedFactoryRow[]; displayedProductFactoryRows: NonIntegratedFactoryRow[];
displayedProductFactoryRowsComplete: boolean; displayedProductFactoryRowsComplete: boolean;
documentWorkspaceApprovedAt: NonIntegratedDocumentWorkspaceApprovalMap; documentWorkspaceApprovedAt: NonIntegratedDocumentWorkspaceApprovalMap;
documentWorkspaceConfirmations: NonIntegratedDocumentWorkspaceConfirmationMap; documentWorkspaceConfirmations: NonIntegratedDocumentWorkspaceConfirmationMap;
documentWorkspaceRevisionSignatureRef: MutableRefObject<
Record<WorkflowNodeCategory, string> | null
>;
isCurrentUserAdmin: boolean; isCurrentUserAdmin: boolean;
isReadOnly: boolean; isReadOnly: boolean;
isSkuItemFactoryConfirmed: (item: NonIntegratedSkuItem) => boolean; isSkuItemFactoryConfirmed: (item: NonIntegratedSkuItem) => boolean;
persistEditedProduct: PersistEditedProduct;
productFactoryModelAutofillRef: MutableRefObject< productFactoryModelAutofillRef: MutableRefObject<
Map<string, ProductFactoryModelAutofillEntry> Map<string, ProductFactoryModelAutofillEntry>
>; >;
@ -73,13 +91,16 @@ export function useProductFactoryRowController({
derivedSkuItems, derivedSkuItems,
displayedProductFactoryHasPendingRows, displayedProductFactoryHasPendingRows,
displayedProductFactoryHasSystemEnteredPricingRows, displayedProductFactoryHasSystemEnteredPricingRows,
displayedProductFactoryIsConfirmed,
displayedProductFactoryRows, displayedProductFactoryRows,
displayedProductFactoryRowsComplete, displayedProductFactoryRowsComplete,
documentWorkspaceApprovedAt, documentWorkspaceApprovedAt,
documentWorkspaceConfirmations, documentWorkspaceConfirmations,
documentWorkspaceRevisionSignatureRef,
isCurrentUserAdmin, isCurrentUserAdmin,
isReadOnly, isReadOnly,
isSkuItemFactoryConfirmed, isSkuItemFactoryConfirmed,
persistEditedProduct,
productFactoryModelAutofillRef, productFactoryModelAutofillRef,
productFactoryReopenField, productFactoryReopenField,
productFactoryRows, productFactoryRows,
@ -156,32 +177,32 @@ export function useProductFactoryRowController({
if (key === "installMethod") { if (key === "installMethod") {
setSkuItems((current) => { setSkuItems((current) => {
const targetItem = workflowSkuItems.find((item) => item.id === skuId); const targetSkuIds = getProductFactoryTargetSkuIds(
if (!targetItem) return current; 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 (targetStyleKeys.size > 0) {
if (targetStyleKey) { setStyleDetails((currentDetails) => {
const targetSkuIds = new Set( let nextDetails = currentDetails;
workflowSkuItems targetStyleKeys.forEach((styleKey) => {
.filter((item) => getStyleKeyFromModel(item.model) === targetStyleKey) nextDetails = updateStyleDetailEntry(nextDetails, styleKey, {
.map((item) => item.id), installMethod: value,
); });
});
setStyleDetails((currentDetails) => return nextDetails;
updateStyleDetailEntry(currentDetails, targetStyleKey, { });
installMethod: value,
}),
);
return current.map((item) =>
targetSkuIds.has(item.id)
? { ...item, installMethod: value }
: item,
);
} }
return current.map((item) => 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"); setSaveMessageType("success");
} }
function handleProductFactoryStatusClick() { async function handleProductFactoryStatusClick() {
const statusMessage = getProductFactoryStatusMessage({ const statusMessage = getProductFactoryStatusMessage({
isRowsComplete: displayedProductFactoryRowsComplete, isRowsComplete: displayedProductFactoryRowsComplete,
rows: displayedProductFactoryRows, rows: displayedProductFactoryRows,
}); });
if (!statusMessage) return; 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); setSaveMessage(statusMessage.message);
setSaveMessageType(statusMessage.type); setSaveMessageType(statusMessage.type);
} }

View File

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

View File

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

View File

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

View File

@ -1,37 +1,77 @@
import type { import type {
Dispatch, Dispatch,
MutableRefObject,
SetStateAction, SetStateAction,
} from "react"; } 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 { import {
getFinishedSampleShipmentStatusMessage, getFinishedSampleShipmentStatusMessage,
getSampleMeasurementStatusMessage, getSampleMeasurementStatusMessage,
normalizeSampleMeasurementRowsForPayload,
type ProductFinishedSampleShipmentDisplayRow, type ProductFinishedSampleShipmentDisplayRow,
type ProductSampleMeasurementDisplayRow, type ProductSampleMeasurementDisplayRow,
} from "./sampleWorkflowHelpers"; } from "./sampleWorkflowHelpers";
type SaveMessageType = "success" | "error"; type SaveMessageType = "success" | "error";
type PersistEditedProduct = (params: {
successMessage: string;
errorFallbackMessage: string;
overrides?: PersistEditedProductOverrides;
suppressSuccessMessage?: boolean;
}) => Promise<boolean>;
type UseSampleWorkflowStatusControllerParams = { type UseSampleWorkflowStatusControllerParams = {
documentWorkspaceApprovedAt: NonIntegratedDocumentWorkspaceApprovalMap;
documentWorkspaceConfirmations: NonIntegratedDocumentWorkspaceConfirmationMap;
displayedFinishedSampleShipmentRows: ProductFinishedSampleShipmentDisplayRow[]; displayedFinishedSampleShipmentRows: ProductFinishedSampleShipmentDisplayRow[];
displayedFinishedSampleShipmentRowsComplete: boolean; displayedFinishedSampleShipmentRowsComplete: boolean;
displayedProductFactoryIsConfirmed: boolean; displayedProductFactoryIsConfirmed: boolean;
displayedProductSampleMeasurementRows: ProductSampleMeasurementDisplayRow[]; displayedProductSampleMeasurementRows: ProductSampleMeasurementDisplayRow[];
displayedSampleMeasurementIsConfirmed: boolean;
displayedSampleMeasurementRowsComplete: 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>>; setSaveMessage: Dispatch<SetStateAction<string>>;
setSaveMessageType: Dispatch<SetStateAction<SaveMessageType>>; setSaveMessageType: Dispatch<SetStateAction<SaveMessageType>>;
}; };
export function useSampleWorkflowStatusController({ export function useSampleWorkflowStatusController({
documentWorkspaceApprovedAt,
documentWorkspaceConfirmations,
displayedFinishedSampleShipmentRows, displayedFinishedSampleShipmentRows,
displayedFinishedSampleShipmentRowsComplete, displayedFinishedSampleShipmentRowsComplete,
displayedProductFactoryIsConfirmed, displayedProductFactoryIsConfirmed,
displayedProductSampleMeasurementRows, displayedProductSampleMeasurementRows,
displayedSampleMeasurementIsConfirmed,
displayedSampleMeasurementRowsComplete, displayedSampleMeasurementRowsComplete,
documentWorkspaceRevisionSignatureRef,
isReadOnly,
persistEditedProduct,
productSampleMeasurementRows,
setDocumentWorkspaceApprovedAt,
setDocumentWorkspaceConfirmations,
setSaveMessage, setSaveMessage,
setSaveMessageType, setSaveMessageType,
}: UseSampleWorkflowStatusControllerParams) { }: UseSampleWorkflowStatusControllerParams) {
function handleSampleMeasurementStatusClick() { async function handleSampleMeasurementStatusClick() {
const statusMessage = getSampleMeasurementStatusMessage({ const statusMessage = getSampleMeasurementStatusMessage({
isFactoryConfirmed: displayedProductFactoryIsConfirmed, isFactoryConfirmed: displayedProductFactoryIsConfirmed,
isRowsComplete: displayedSampleMeasurementRowsComplete, isRowsComplete: displayedSampleMeasurementRowsComplete,
@ -39,6 +79,47 @@ export function useSampleWorkflowStatusController({
}); });
if (!statusMessage) return; 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); setSaveMessage(statusMessage.message);
setSaveMessageType(statusMessage.type); setSaveMessageType(statusMessage.type);
} }

View File

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

View File

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

View File

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