merge: sync xuxiake mainline before platform material fix
# Conflicts: # furniture-3d-system/src/app/assisted-assembly/mobile/AssistedAssemblyMobileView.tsx # furniture-3d-system/src/app/mobile/HomeMobileView.tsx # furniture-3d-system/src/server/services/asset-center.service.ts
This commit is contained in:
commit
f802d65490
@ -939,7 +939,6 @@ model AssetFamily {
|
||||
deleter User? @relation("AssetFamilyDeleter", fields: [deletedById], references: [id], onDelete: SetNull)
|
||||
|
||||
variants AssetVariant[]
|
||||
skuGroups AssetSkuGroup[]
|
||||
compatibleSeries AssetFamilySeriesCompatibility[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
@ -958,26 +957,6 @@ model AssetFamily {
|
||||
@@index([deletedById])
|
||||
}
|
||||
|
||||
model AssetSkuGroup {
|
||||
id String @id @default(cuid())
|
||||
assetFamilyId String
|
||||
assetFamily AssetFamily @relation(fields: [assetFamilyId], references: [id], onDelete: Cascade)
|
||||
code String
|
||||
name String
|
||||
description String? @db.Text
|
||||
sortOrder Int @default(0)
|
||||
metadata Json?
|
||||
variants AssetVariant[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([assetFamilyId, code])
|
||||
@@index([assetFamilyId])
|
||||
@@index([code])
|
||||
@@index([sortOrder])
|
||||
}
|
||||
|
||||
model AssetSeries {
|
||||
id String @id @default(cuid())
|
||||
code String @unique
|
||||
@ -1025,8 +1004,6 @@ model AssetVariant {
|
||||
id String @id @default(cuid())
|
||||
assetFamilyId String
|
||||
assetFamily AssetFamily @relation(fields: [assetFamilyId], references: [id], onDelete: Cascade)
|
||||
skuGroupId String?
|
||||
skuGroup AssetSkuGroup? @relation(fields: [skuGroupId], references: [id], onDelete: SetNull)
|
||||
code String @unique
|
||||
name String
|
||||
identityKey String
|
||||
@ -1084,7 +1061,6 @@ model AssetVariant {
|
||||
|
||||
@@unique([assetFamilyId, identityKey])
|
||||
@@index([assetFamilyId])
|
||||
@@index([skuGroupId])
|
||||
@@index([status])
|
||||
@@index([barcode])
|
||||
@@index([substrateId])
|
||||
|
||||
@ -1450,6 +1450,15 @@
|
||||
animation: slideInRight 0.35s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
@keyframes nav-progress {
|
||||
0% {
|
||||
transform: translateX(-120%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(260%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 滑块 — Precision Atelier 配色 */
|
||||
.apple-slider {
|
||||
width: 100%;
|
||||
|
||||
@ -18,6 +18,7 @@ import type {
|
||||
WarehouseLocationDTO,
|
||||
} from "@/server/services";
|
||||
import type { PlatformMetricItem } from "@/components/platform/types";
|
||||
import type { NonIntegratedProductPageInfo } from "./non-integrated/core/nonIntegratedApi";
|
||||
import type { NonIntegratedProductRecord } from "./non-integrated/nonIntegratedStorage";
|
||||
|
||||
type IntakeClientUser = {
|
||||
@ -45,6 +46,11 @@ type CommonIntakeClientProps = {
|
||||
materialLibraryCategories: MaterialCategoryDTO[];
|
||||
materialLibraryDictionaries: SubstrateDictionaryBundleDTO;
|
||||
initialNonIntegratedProducts: NonIntegratedProductRecord[];
|
||||
initialNonIntegratedPageInfo?: {
|
||||
product?: NonIntegratedProductPageInfo;
|
||||
stock?: NonIntegratedProductPageInfo;
|
||||
series?: NonIntegratedProductPageInfo;
|
||||
};
|
||||
initialNodePermissionOverrides?: IntakeNodePermissionOverrideMap;
|
||||
initialActiveSection?: IntakeSection;
|
||||
initialActiveRawMaterialLibrary?: RawMaterialLibraryKey;
|
||||
|
||||
@ -118,6 +118,7 @@ export function buildIntakePrimaryDomainProps(params: {
|
||||
pendingEntryTab: IntakeEntryTab;
|
||||
skuList: IntegratedPendingSeriesRow[];
|
||||
nonIntegratedProducts: NonIntegratedProductRecord[];
|
||||
initialNonIntegratedPageInfo: IntakeClientProps["initialNonIntegratedPageInfo"];
|
||||
canDeleteNonIntegratedProducts: boolean;
|
||||
canCreateNonIntegratedProducts: boolean;
|
||||
canUseDesignControlledProductData: boolean;
|
||||
@ -162,6 +163,7 @@ export function buildIntakePrimaryDomainProps(params: {
|
||||
pendingEntryTab: params.pendingEntryTab,
|
||||
integratedRows: params.skuList,
|
||||
nonIntegratedProducts: params.nonIntegratedProducts,
|
||||
initialNonIntegratedPageInfo: params.initialNonIntegratedPageInfo,
|
||||
canDeleteProducts: params.canDeleteNonIntegratedProducts,
|
||||
canCreateProducts: params.canCreateNonIntegratedProducts,
|
||||
canUseDesignControlledProductData:
|
||||
|
||||
41
furniture-3d-system/src/app/intake/loading.tsx
Normal file
41
furniture-3d-system/src/app/intake/loading.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
const ROWS = Array.from({ length: 8 }, (_, index) => index)
|
||||
const COLUMNS = Array.from({ length: 8 }, (_, index) => index)
|
||||
|
||||
export default function IntakeLoading() {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-apple-bg-secondary p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="space-y-2">
|
||||
<div className="h-4 w-32 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
<div className="h-3 w-52 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
</div>
|
||||
<div className="h-8 w-24 animate-pulse rounded-apple-lg bg-apple-bg-tertiary" />
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary">
|
||||
<div className="flex gap-2 border-b border-apple-separator p-3">
|
||||
<div className="h-8 w-20 animate-pulse rounded-apple-lg bg-apple-bg-tertiary" />
|
||||
<div className="h-8 w-20 animate-pulse rounded-apple-lg bg-apple-bg-tertiary" />
|
||||
<div className="h-8 w-20 animate-pulse rounded-apple-lg bg-apple-bg-tertiary" />
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-hidden p-3">
|
||||
<div className="overflow-hidden rounded-apple-lg border border-apple-separator">
|
||||
<div className="grid grid-cols-8 gap-px bg-apple-separator">
|
||||
{COLUMNS.map((column) => (
|
||||
<div key={column} className="bg-apple-bg-secondary p-3">
|
||||
<div className="h-3 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
</div>
|
||||
))}
|
||||
{ROWS.flatMap((row) =>
|
||||
COLUMNS.map((column) => (
|
||||
<div key={`${row}-${column}`} className="bg-apple-bg-primary p-3">
|
||||
<div className="h-3 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
</div>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -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"),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -124,7 +124,7 @@ export function FinishedSampleShipmentWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-hidden p-3">
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-auto p-3">
|
||||
{allRows.length === 0 ? (
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
|
||||
当前还没有 SKU 信息,先维护 SKU 后再进行成品寄样。
|
||||
@ -134,9 +134,9 @@ export function FinishedSampleShipmentWorkspace({
|
||||
当前批次还没有 SKU 信息。
|
||||
</div>
|
||||
) : (
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator">
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator" style={{ overflowY: 'visible' }}>
|
||||
<table className="min-w-[1760px] w-full border-collapse text-xs">
|
||||
<thead className="bg-apple-bg-secondary/40 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{[
|
||||
"寄样",
|
||||
|
||||
@ -89,7 +89,7 @@ export function FirstQualityWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-hidden p-3">
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-auto p-3">
|
||||
{allRows.length === 0 ? (
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
|
||||
当前还没有 SKU 信息,先维护 SKU 后再进行首批质检。
|
||||
@ -99,9 +99,9 @@ export function FirstQualityWorkspace({
|
||||
当前批次还没有 SKU 信息。
|
||||
</div>
|
||||
) : (
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator">
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator" style={{ overflowY: 'visible' }}>
|
||||
<table className="min-w-[2120px] w-full border-collapse text-xs">
|
||||
<thead className="bg-apple-bg-secondary/40 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{[
|
||||
"批次",
|
||||
|
||||
@ -96,7 +96,7 @@ export function ProductLimitWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-hidden p-3">
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-auto p-3">
|
||||
{allRows.length === 0 ? (
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
|
||||
当前还没有 SKU 信息,先维护 SKU 后再填写产品限价。
|
||||
@ -106,9 +106,9 @@ export function ProductLimitWorkspace({
|
||||
当前批次还没有 SKU 信息。
|
||||
</div>
|
||||
) : (
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator">
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator" style={{ overflowY: 'visible' }}>
|
||||
<table className="min-w-[1480px] w-full border-collapse text-xs">
|
||||
<thead className="bg-apple-bg-secondary/40 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{[
|
||||
"产品图片",
|
||||
|
||||
@ -113,7 +113,7 @@ export function ProductPricingWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-hidden p-3">
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-auto p-3">
|
||||
{allRows.length === 0 ? (
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
|
||||
当前还没有 SKU 信息,先维护 SKU 后再核价。
|
||||
@ -123,9 +123,9 @@ export function ProductPricingWorkspace({
|
||||
当前批次还没有 SKU 信息。
|
||||
</div>
|
||||
) : (
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator">
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator" style={{ overflowY: 'visible' }}>
|
||||
<table className="min-w-[1280px] w-full border-collapse text-xs">
|
||||
<thead className="bg-apple-bg-secondary/40 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{[
|
||||
"型号",
|
||||
|
||||
@ -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"
|
||||
@ -85,7 +85,7 @@ export function SampleMeasurementWorkspace({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-hidden p-3">
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-auto p-3">
|
||||
{allRows.length === 0 ? (
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
|
||||
当前还没有 SKU 信息,先维护 SKU 后再进行样包测量。
|
||||
@ -95,9 +95,9 @@ export function SampleMeasurementWorkspace({
|
||||
当前批次还没有 SKU 信息。
|
||||
</div>
|
||||
) : (
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator">
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator" style={{ overflowY: 'visible' }}>
|
||||
<table className="min-w-[2120px] w-full border-collapse text-xs">
|
||||
<thead className="bg-apple-bg-secondary/40 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{[
|
||||
"型号",
|
||||
|
||||
@ -67,10 +67,9 @@ export function ComponentSkuTable({
|
||||
onSelectComponentCode: (code: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-apple-lg border border-apple-separator">
|
||||
<div className="non-integrated-table-scroll overflow-x-auto">
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll max-h-[60vh] max-w-full rounded-apple-lg border border-apple-separator">
|
||||
<table className="min-w-[1660px] w-full text-left text-xs">
|
||||
<thead className="bg-apple-bg-secondary/60 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{COMPONENT_SKU_TABLE_HEADERS.map((label) => (
|
||||
<th
|
||||
@ -165,7 +164,6 @@ export function ComponentSkuTable({
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -50,14 +50,13 @@ export function SkuInfoTableShell({
|
||||
(showActionColumn ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-apple-lg border border-apple-separator">
|
||||
<div className="non-integrated-table-scroll overflow-x-auto">
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll max-h-[60vh] max-w-full rounded-apple-lg border border-apple-separator">
|
||||
<table
|
||||
className={`${
|
||||
isSeriesComponentDetailProduct ? "min-w-[2260px]" : "min-w-[2180px]"
|
||||
} w-full text-left text-xs`}
|
||||
>
|
||||
<thead className="bg-apple-bg-secondary/60 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{headers.map((label) => (
|
||||
<th
|
||||
@ -83,7 +82,6 @@ export function SkuInfoTableShell({
|
||||
{children}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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",
|
||||
]);
|
||||
|
||||
@ -28,7 +28,7 @@ export function BomRowsTableShell({
|
||||
className="w-full min-w-full text-left text-xs"
|
||||
style={{ minWidth: `${tableMinWidth}px` }}
|
||||
>
|
||||
<thead className="bg-apple-bg-secondary/60 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -96,9 +96,9 @@ export function ProductFactoryInfoTable({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator">
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator" style={{ overflowY: 'visible' }}>
|
||||
<table className="min-w-[2160px] w-full border-collapse text-xs">
|
||||
<thead className="bg-apple-bg-secondary/40 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{[
|
||||
"定厂",
|
||||
@ -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">
|
||||
|
||||
@ -8,9 +8,9 @@ export function ProductFactorySkuInfoTable({
|
||||
rows: NonIntegratedFactoryRow[];
|
||||
}) {
|
||||
return (
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator">
|
||||
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator" style={{ overflowY: 'visible' }}>
|
||||
<table className="min-w-[980px] w-full border-collapse text-xs">
|
||||
<thead className="bg-apple-bg-secondary/40 text-apple-label-secondary">
|
||||
<thead className="sticky top-0 z-10 bg-apple-bg-primary text-apple-label-secondary">
|
||||
<tr>
|
||||
{[
|
||||
"定厂",
|
||||
@ -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>
|
||||
|
||||
@ -91,7 +91,7 @@ export function ProductFactoryWorkspace({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-hidden p-3">
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-auto p-3">
|
||||
{allRows.length === 0 ? (
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
|
||||
当前还没有 SKU 信息,先维护 SKU 后再定厂。
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -153,7 +153,7 @@ export function getSampleMeasurementStatusMessage({
|
||||
}
|
||||
|
||||
return {
|
||||
message: "样包测量已完成",
|
||||
message: "资料已填完核对无误请确认。",
|
||||
type: "success" as const,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -231,7 +231,8 @@ export default async function IntakePage({
|
||||
materialLibraryFinishes,
|
||||
materialLibraryCategories,
|
||||
materialLibraryDictionaries,
|
||||
initialNonIntegratedProductPage,
|
||||
initialProductNonIntegratedProductPage,
|
||||
initialStockNonIntegratedProductPage,
|
||||
initialSeriesNonIntegratedProductPage,
|
||||
nodePermissionSettings,
|
||||
initialOpenProduct,
|
||||
@ -247,10 +248,17 @@ export default async function IntakePage({
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
productDevelopmentKind: 'product',
|
||||
productKind: 'new',
|
||||
}),
|
||||
intakeNonIntegratedProductService.listPage(authUser, {
|
||||
page: 1,
|
||||
pageSize: 500,
|
||||
pageSize: 50,
|
||||
productDevelopmentKind: 'product',
|
||||
productKind: 'stock',
|
||||
}),
|
||||
intakeNonIntegratedProductService.listPage(authUser, {
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
productDevelopmentKind: 'series',
|
||||
}),
|
||||
intakeNodePermissionSettingsService.getSettings(),
|
||||
@ -263,11 +271,17 @@ export default async function IntakePage({
|
||||
])
|
||||
const initialNonIntegratedProducts = appendInitialOpenProduct(
|
||||
mergeInitialNonIntegratedProducts(
|
||||
initialNonIntegratedProductPage.products,
|
||||
initialProductNonIntegratedProductPage.products,
|
||||
initialStockNonIntegratedProductPage.products,
|
||||
initialSeriesNonIntegratedProductPage.products,
|
||||
),
|
||||
initialOpenProduct,
|
||||
)
|
||||
const initialNonIntegratedPageInfo = {
|
||||
product: initialProductNonIntegratedProductPage.pageInfo,
|
||||
stock: initialStockNonIntegratedProductPage.pageInfo,
|
||||
series: initialSeriesNonIntegratedProductPage.pageInfo,
|
||||
}
|
||||
const initialOpenProductWorkspace = initialOpenProduct
|
||||
? {
|
||||
product: initialOpenProduct,
|
||||
@ -292,43 +306,50 @@ export default async function IntakePage({
|
||||
? selectedDetail
|
||||
: null
|
||||
|
||||
const eligibleDraftGroupDetails = await Promise.all(
|
||||
eligibleDraftGroups.map((group) => draftGroupService.getById(group.id)),
|
||||
const eligibleDraftGroupById = new Map(
|
||||
eligibleDraftGroups.map((group) => [group.id, group]),
|
||||
)
|
||||
const eligiblePreSkus = await draftGroupService.listPreSkusByDraftGroupIds(
|
||||
eligibleDraftGroups.map((group) => group.id),
|
||||
)
|
||||
const eligiblePreSkuSequenceByDraftGroup = new Map<string, number>()
|
||||
|
||||
const eligibleSkuList = eligibleDraftGroupDetails.flatMap((detail) =>
|
||||
detail.preSkus.map((preSku, index) => {
|
||||
const skuFields = readPreSkuSkuFields(preSku)
|
||||
return {
|
||||
id: preSku.id,
|
||||
draftGroupId: detail.draftGroup.id,
|
||||
spuCode:
|
||||
readPreSkuTextField(preSku, 'spuCode') ||
|
||||
readPreSkuTextField(preSku, 'spu') ||
|
||||
detail.draftGroup.code ||
|
||||
`SPU-${String(index + 1).padStart(3, '0')}`,
|
||||
code:
|
||||
skuFields.skuCode ||
|
||||
preSku.customName ||
|
||||
preSku.defaultName,
|
||||
name: preSku.customName || preSku.defaultName,
|
||||
seriesName:
|
||||
readPreSkuTextField(preSku, 'seriesName') ||
|
||||
detail.draftGroup.assetSeriesName ||
|
||||
detail.draftGroup.name,
|
||||
draftGroupName: detail.draftGroup.name,
|
||||
status: mapWorkbenchStatus(preSku.status),
|
||||
componentCodes: preSku.componentCodes ?? [],
|
||||
componentDetails: readPreSkuComponentDetails(preSku),
|
||||
packagingPlan: preSku.packagingPlan,
|
||||
holePoints: buildSkuHolePointsFromVariantSelection(preSku),
|
||||
skuFields,
|
||||
model3dPreviewNodes: preSku.previewNodes.length
|
||||
? preSku.previewNodes
|
||||
: preSku.assemblyWorkspace?.nodes ?? [],
|
||||
}
|
||||
}),
|
||||
)
|
||||
const eligibleSkuList = eligiblePreSkus.flatMap((preSku) => {
|
||||
const draftGroup = eligibleDraftGroupById.get(preSku.draftGroupId)
|
||||
if (!draftGroup) return []
|
||||
|
||||
const index = eligiblePreSkuSequenceByDraftGroup.get(draftGroup.id) ?? 0
|
||||
eligiblePreSkuSequenceByDraftGroup.set(draftGroup.id, index + 1)
|
||||
const skuFields = readPreSkuSkuFields(preSku)
|
||||
return {
|
||||
id: preSku.id,
|
||||
draftGroupId: draftGroup.id,
|
||||
spuCode:
|
||||
readPreSkuTextField(preSku, 'spuCode') ||
|
||||
readPreSkuTextField(preSku, 'spu') ||
|
||||
draftGroup.code ||
|
||||
`SPU-${String(index + 1).padStart(3, '0')}`,
|
||||
code:
|
||||
skuFields.skuCode ||
|
||||
preSku.customName ||
|
||||
preSku.defaultName,
|
||||
name: preSku.customName || preSku.defaultName,
|
||||
seriesName:
|
||||
readPreSkuTextField(preSku, 'seriesName') ||
|
||||
draftGroup.assetSeriesName ||
|
||||
draftGroup.name,
|
||||
draftGroupName: draftGroup.name,
|
||||
status: mapWorkbenchStatus(preSku.status),
|
||||
componentCodes: preSku.componentCodes ?? [],
|
||||
componentDetails: readPreSkuComponentDetails(preSku),
|
||||
packagingPlan: preSku.packagingPlan,
|
||||
holePoints: buildSkuHolePointsFromVariantSelection(preSku),
|
||||
skuFields,
|
||||
model3dPreviewNodes: preSku.previewNodes.length
|
||||
? preSku.previewNodes
|
||||
: preSku.assemblyWorkspace?.nodes ?? [],
|
||||
}
|
||||
})
|
||||
|
||||
const sourceWorkspace =
|
||||
eligibleSelectedDetail?.preSkus.find((preSku) => preSku.assemblyWorkspace)
|
||||
@ -363,6 +384,7 @@ export default async function IntakePage({
|
||||
materialLibraryCategories={materialLibraryCategories}
|
||||
materialLibraryDictionaries={materialLibraryDictionaries}
|
||||
initialNonIntegratedProducts={initialNonIntegratedProducts}
|
||||
initialNonIntegratedPageInfo={initialNonIntegratedPageInfo}
|
||||
initialNodePermissionOverrides={nodePermissionSettings.overrides}
|
||||
initialActiveSection={initialActiveSection}
|
||||
initialActiveRawMaterialLibrary={initialActiveRawMaterialLibrary}
|
||||
@ -390,6 +412,7 @@ export default async function IntakePage({
|
||||
materialLibraryCategories={materialLibraryCategories}
|
||||
materialLibraryDictionaries={materialLibraryDictionaries}
|
||||
initialNonIntegratedProducts={initialNonIntegratedProducts}
|
||||
initialNonIntegratedPageInfo={initialNonIntegratedPageInfo}
|
||||
initialNodePermissionOverrides={nodePermissionSettings.overrides}
|
||||
initialActiveSection={initialActiveSection}
|
||||
initialActiveRawMaterialLibrary={initialActiveRawMaterialLibrary}
|
||||
|
||||
@ -4,7 +4,6 @@ import { useRouter } from "next/navigation";
|
||||
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Copy, Pencil, Plus, RotateCcw, Trash2 } from "lucide-react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import {
|
||||
CopyProductDialog,
|
||||
ImagePreviewDialog,
|
||||
@ -33,6 +32,7 @@ import {
|
||||
import type { ProductDevelopmentKind, WorkspaceProductMode } from "@/app/intake/intakeWorkspaceTypes";
|
||||
import {
|
||||
buildNonIntegratedListStateStorageKey,
|
||||
createDefaultNonIntegratedListQuery,
|
||||
getUniqueSortedStrings,
|
||||
normalizeNonIntegratedListQuery,
|
||||
normalizePersistedPage,
|
||||
@ -102,8 +102,39 @@ const TABLE_ACTION_BUTTON_PRIMARY = `${TABLE_ACTION_BUTTON_BASE} !border-zinc-95
|
||||
const INTAKE_PRIMARY_BUTTON_CLASS =
|
||||
"!border-zinc-950 !bg-zinc-950 !bg-none !text-white !shadow-none hover:!border-zinc-800 hover:!bg-zinc-800 focus:!border-zinc-600 disabled:!border-zinc-200 disabled:!bg-zinc-200 disabled:!text-zinc-400";
|
||||
|
||||
function isDefaultInitialListRequest(params: {
|
||||
activeQuickScope: NonIntegratedQuickScope;
|
||||
appliedQuery: NonIntegratedListQuery;
|
||||
currentPage: number;
|
||||
effectiveProductKind: NonIntegratedProductKindFilter;
|
||||
initialPageInfo?: NonIntegratedProductPageInfo | null;
|
||||
initialProductKind: NonIntegratedProductKindFilter;
|
||||
}) {
|
||||
const {
|
||||
activeQuickScope,
|
||||
appliedQuery,
|
||||
currentPage,
|
||||
effectiveProductKind,
|
||||
initialPageInfo,
|
||||
initialProductKind,
|
||||
} = params;
|
||||
|
||||
if (!initialPageInfo) return false;
|
||||
if (activeQuickScope !== "all") return false;
|
||||
if (currentPage !== initialPageInfo.page) return false;
|
||||
if (initialPageInfo.pageSize !== PENDING_PAGE_SIZE) return false;
|
||||
if (effectiveProductKind !== initialProductKind) return false;
|
||||
|
||||
const defaultQuery = createDefaultNonIntegratedListQuery(initialProductKind);
|
||||
return Object.entries(defaultQuery).every(
|
||||
([key, value]) =>
|
||||
appliedQuery[key as keyof NonIntegratedListQuery] === value,
|
||||
);
|
||||
}
|
||||
|
||||
export function NonIntegratedPendingDetail({
|
||||
initialProducts,
|
||||
initialPageInfo,
|
||||
canDeleteProducts,
|
||||
currentUser,
|
||||
productDevelopmentKind = "product",
|
||||
@ -117,6 +148,7 @@ export function NonIntegratedPendingDetail({
|
||||
onOpenSecondaryNewNonIntegratedProductWorkspace,
|
||||
}: {
|
||||
initialProducts: NonIntegratedProductRecord[];
|
||||
initialPageInfo?: NonIntegratedProductPageInfo | null;
|
||||
canDeleteProducts: boolean;
|
||||
currentUser: NonIntegratedListPermissionUser;
|
||||
productDevelopmentKind?: ProductDevelopmentKind;
|
||||
@ -151,7 +183,7 @@ export function NonIntegratedPendingDetail({
|
||||
String(normalizePersistedPage(initialPersistedListState?.currentPage)),
|
||||
);
|
||||
const [pageInfo, setPageInfo] =
|
||||
useState<NonIntegratedProductPageInfo | null>(null);
|
||||
useState<NonIntegratedProductPageInfo | null>(initialPageInfo ?? null);
|
||||
const [isListLoading, setIsListLoading] = useState(false);
|
||||
const [listErrorMessage, setListErrorMessage] = useState("");
|
||||
const [previewProduct, setPreviewProduct] = useState<{
|
||||
@ -187,6 +219,7 @@ export function NonIntegratedPendingDetail({
|
||||
useState(false);
|
||||
const listStateStorageKeyRef = useRef(listStateStorageKey);
|
||||
const skipNextListStatePersistRef = useRef(false);
|
||||
const initialListRequestHandledRef = useRef(false);
|
||||
const shouldUseSharedNonIntegratedApi = process.env.NODE_ENV !== "test";
|
||||
const effectiveProductKind = fixedProductKind ?? appliedQuery.productKind;
|
||||
const hasCreateAction = Boolean(
|
||||
@ -310,6 +343,22 @@ export function NonIntegratedPendingDetail({
|
||||
useEffect(() => {
|
||||
if (!shouldUseSharedNonIntegratedApi) return;
|
||||
|
||||
if (
|
||||
!initialListRequestHandledRef.current &&
|
||||
isDefaultInitialListRequest({
|
||||
activeQuickScope,
|
||||
appliedQuery,
|
||||
currentPage,
|
||||
effectiveProductKind,
|
||||
initialPageInfo,
|
||||
initialProductKind,
|
||||
})
|
||||
) {
|
||||
initialListRequestHandledRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
initialListRequestHandledRef.current = true;
|
||||
let cancelled = false;
|
||||
setIsListLoading(true);
|
||||
setListErrorMessage("");
|
||||
@ -351,6 +400,8 @@ export function NonIntegratedPendingDetail({
|
||||
appliedQuery,
|
||||
currentPage,
|
||||
effectiveProductKind,
|
||||
initialPageInfo,
|
||||
initialProductKind,
|
||||
productDevelopmentKind,
|
||||
shouldUseSharedNonIntegratedApi,
|
||||
]);
|
||||
@ -847,8 +898,8 @@ export function NonIntegratedPendingDetail({
|
||||
(isSeriesDevelopmentList ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-xs">
|
||||
<div className="border-b border-apple-separator px-4 py-3">
|
||||
<div className="flex h-full min-w-0 flex-col overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-xs">
|
||||
<div className="min-w-0 border-b border-apple-separator px-4 py-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{quickScopes.map((scope) => (
|
||||
@ -871,14 +922,14 @@ export function NonIntegratedPendingDetail({
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-end gap-2 overflow-x-auto pb-1 md:grid md:overflow-visible md:pb-0 ${filterGridColumnClass}`}
|
||||
className={`flex min-w-0 max-w-full items-end gap-2 overflow-x-auto pb-1 md:grid md:overflow-visible md:pb-0 ${filterGridColumnClass}`}
|
||||
>
|
||||
{filterFields.map((filter) => (
|
||||
<label
|
||||
key={filter.key}
|
||||
className="min-w-[112px] space-y-1.5 text-xs text-apple-label-secondary max-md:space-y-0 md:min-w-0"
|
||||
>
|
||||
<span className="max-md:sr-only">{filter.label}</span>
|
||||
<span className="max-md:hidden">{filter.label}</span>
|
||||
{filter.options ? (
|
||||
<select
|
||||
aria-label={filter.ariaLabel}
|
||||
@ -954,9 +1005,9 @@ export function NonIntegratedPendingDetail({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 p-4">
|
||||
<div className="flex h-full min-h-0 flex-col rounded-apple-lg border border-apple-separator bg-apple-bg-primary">
|
||||
<div className="min-h-0 flex-1 overflow-auto pb-3">
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-hidden p-4">
|
||||
<div className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden rounded-apple-lg border border-apple-separator bg-apple-bg-primary">
|
||||
<div className="min-h-0 min-w-0 max-w-full flex-1 overflow-x-auto overflow-y-auto pb-3">
|
||||
<table
|
||||
className={`w-full text-left text-xs ${
|
||||
isSeriesDevelopmentList ? "min-w-[3140px]" : "min-w-[3000px]"
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import type { NonIntegratedProductRecord } from "@/app/intake/non-integrated/nonIntegratedStorage";
|
||||
import type { NonIntegratedProductPageInfo } from "@/app/intake/non-integrated/core/nonIntegratedApi";
|
||||
import { NonIntegratedPendingDetail } from "@/app/intake/product-development/NonIntegratedPendingDetail";
|
||||
import { ProductDevelopmentProgressDashboard } from "@/app/intake/product-development/progress-dashboard/ProductDevelopmentProgressDashboard";
|
||||
import type {
|
||||
@ -26,6 +27,11 @@ type ProductDevelopmentDomainProps = {
|
||||
pendingEntryTab: IntakeEntryTab;
|
||||
integratedRows: IntegratedPendingSeriesRow[];
|
||||
nonIntegratedProducts: NonIntegratedProductRecord[];
|
||||
initialNonIntegratedPageInfo?: {
|
||||
product?: NonIntegratedProductPageInfo;
|
||||
stock?: NonIntegratedProductPageInfo;
|
||||
series?: NonIntegratedProductPageInfo;
|
||||
};
|
||||
canDeleteProducts: boolean;
|
||||
canCreateProducts: boolean;
|
||||
canUseDesignControlledProductData: boolean;
|
||||
@ -53,6 +59,7 @@ export function ProductDevelopmentDomain({
|
||||
pendingEntryTab,
|
||||
integratedRows,
|
||||
nonIntegratedProducts,
|
||||
initialNonIntegratedPageInfo,
|
||||
canDeleteProducts,
|
||||
canCreateProducts,
|
||||
canUseDesignControlledProductData,
|
||||
@ -89,6 +96,7 @@ export function ProductDevelopmentDomain({
|
||||
return (
|
||||
<NonIntegratedPendingDetail
|
||||
initialProducts={seriesDevelopmentProducts}
|
||||
initialPageInfo={initialNonIntegratedPageInfo?.series}
|
||||
canDeleteProducts={canDeleteProducts}
|
||||
currentUser={currentUser}
|
||||
productDevelopmentKind="series"
|
||||
@ -109,6 +117,7 @@ export function ProductDevelopmentDomain({
|
||||
return (
|
||||
<NonIntegratedPendingDetail
|
||||
initialProducts={stockDevelopmentProducts}
|
||||
initialPageInfo={initialNonIntegratedPageInfo?.stock}
|
||||
canDeleteProducts={canDeleteProducts}
|
||||
currentUser={currentUser}
|
||||
productDevelopmentKind="product"
|
||||
@ -126,6 +135,7 @@ export function ProductDevelopmentDomain({
|
||||
return (
|
||||
<NonIntegratedPendingDetail
|
||||
initialProducts={productDevelopmentProducts}
|
||||
initialPageInfo={initialNonIntegratedPageInfo?.product}
|
||||
canDeleteProducts={canDeleteProducts}
|
||||
currentUser={currentUser}
|
||||
productDevelopmentKind="product"
|
||||
|
||||
@ -214,6 +214,7 @@ export function useIntakeClientViewModel(
|
||||
pendingEntryTab,
|
||||
skuList,
|
||||
nonIntegratedProducts,
|
||||
initialNonIntegratedPageInfo: props.initialNonIntegratedPageInfo,
|
||||
canDeleteNonIntegratedProducts,
|
||||
canCreateNonIntegratedProducts,
|
||||
canUseDesignControlledProductData,
|
||||
|
||||
@ -409,7 +409,9 @@ export default function HomeMobileView({
|
||||
<DeleteConfirmDialog
|
||||
isOpen={!!deletingProduct}
|
||||
onClose={onCloseDeleteProduct}
|
||||
onConfirm={onDeleteProduct}
|
||||
onConfirm={async () => {
|
||||
await onDeleteProduct()
|
||||
}}
|
||||
title="删除产品"
|
||||
description={
|
||||
deletingProduct
|
||||
@ -431,7 +433,9 @@ export default function HomeMobileView({
|
||||
<DeleteConfirmDialog
|
||||
isOpen={!!deletingSku}
|
||||
onClose={onCloseDeleteSKU}
|
||||
onConfirm={onDeleteSKU}
|
||||
onConfirm={async () => {
|
||||
await onDeleteSKU()
|
||||
}}
|
||||
title="删除 SKU"
|
||||
description={
|
||||
deletingSku
|
||||
|
||||
@ -46,6 +46,7 @@ type SpuQualityGroup = {
|
||||
}
|
||||
|
||||
const CONCLUSION_OPTIONS = ['待检', '合格', '让步接收', '返工', '不合格'] as const
|
||||
const INITIAL_SPU_GROUP_RENDER_LIMIT = 80
|
||||
|
||||
function createEmptyForm(inspectorName: string): QualityFormState {
|
||||
return {
|
||||
@ -256,6 +257,7 @@ export default function WarehouseQualityClient({
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [showAllGroups, setShowAllGroups] = useState(false)
|
||||
|
||||
const filteredArchives = useMemo(() => {
|
||||
const keyword = search.trim().toLowerCase()
|
||||
@ -278,11 +280,32 @@ export default function WarehouseQualityClient({
|
||||
selectedGroup?.skuArchives[0] ??
|
||||
null
|
||||
|
||||
const visibleSpuGroups = useMemo(() => {
|
||||
if (showAllGroups || spuGroups.length <= INITIAL_SPU_GROUP_RENDER_LIMIT) {
|
||||
return spuGroups
|
||||
}
|
||||
|
||||
const initialGroups = spuGroups.slice(0, INITIAL_SPU_GROUP_RENDER_LIMIT)
|
||||
if (
|
||||
selectedGroup &&
|
||||
!initialGroups.some((group) => group.key === selectedGroup.key)
|
||||
) {
|
||||
return [...initialGroups, selectedGroup]
|
||||
}
|
||||
|
||||
return initialGroups
|
||||
}, [selectedGroup, showAllGroups, spuGroups])
|
||||
const hiddenSpuGroupCount = Math.max(0, spuGroups.length - visibleSpuGroups.length)
|
||||
|
||||
const records = useMemo(
|
||||
() => sortRecordsForDisplay(selectedArchive?.records ?? []),
|
||||
[selectedArchive],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setShowAllGroups(false)
|
||||
}, [search])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedGroup && spuGroups[0]) {
|
||||
setSelectedGroupKey(spuGroups[0].key)
|
||||
@ -420,7 +443,7 @@ export default function WarehouseQualityClient({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
{spuGroups.map((group) => {
|
||||
{visibleSpuGroups.map((group) => {
|
||||
const isActive = group.key === selectedGroup?.key
|
||||
return (
|
||||
<button
|
||||
@ -456,6 +479,15 @@ export default function WarehouseQualityClient({
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{hiddenSpuGroupCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllGroups(true)}
|
||||
className="w-full rounded-apple-lg border border-dashed border-apple-separator bg-apple-bg-primary px-2.5 py-2 text-center text-[11px] text-apple-label-secondary transition hover:bg-apple-bg-secondary"
|
||||
>
|
||||
显示全部 {spuGroups.length} 个 SPU
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
44
furniture-3d-system/src/app/warehouse-quality/loading.tsx
Normal file
44
furniture-3d-system/src/app/warehouse-quality/loading.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
const GROUPS = Array.from({ length: 9 }, (_, index) => index)
|
||||
const ROWS = Array.from({ length: 6 }, (_, index) => index)
|
||||
|
||||
export default function WarehouseQualityLoading() {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 gap-3 overflow-hidden bg-apple-bg-secondary p-4">
|
||||
<aside className="flex w-[270px] shrink-0 flex-col overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary">
|
||||
<div className="space-y-2 border-b border-apple-separator p-3">
|
||||
<div className="h-4 w-24 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
<div className="h-8 animate-pulse rounded-apple-lg bg-apple-bg-tertiary" />
|
||||
</div>
|
||||
<div className="space-y-2 p-2">
|
||||
{GROUPS.map((group) => (
|
||||
<div key={group} className="rounded-apple-lg border border-apple-separator p-2.5">
|
||||
<div className="h-3 w-24 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
<div className="mt-2 h-3 w-36 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
<main className="flex min-w-0 flex-1 flex-col overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary">
|
||||
<div className="space-y-2 border-b border-apple-separator p-4">
|
||||
<div className="h-5 w-36 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
<div className="h-3 w-56 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 space-y-3 overflow-hidden p-4">
|
||||
<div className="h-24 animate-pulse rounded-apple-lg bg-apple-bg-tertiary" />
|
||||
<div className="overflow-hidden rounded-apple-lg border border-apple-separator">
|
||||
{ROWS.map((row) => (
|
||||
<div key={row} className="grid grid-cols-6 gap-3 border-b border-apple-separator p-3 last:border-b-0">
|
||||
<div className="h-3 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
<div className="h-3 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
<div className="h-3 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
<div className="h-3 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
<div className="h-3 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
<div className="h-3 animate-pulse rounded-apple-sm bg-apple-bg-tertiary" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -57,6 +57,7 @@ interface AppTopNavProps {
|
||||
export function AppTopNav({ user, variant = 'default' }: AppTopNavProps) {
|
||||
const pathname = usePathname() ?? '/'
|
||||
const [mobileWorkbenchNavOpen, setMobileWorkbenchNavOpen] = useState(false)
|
||||
const [pendingHref, setPendingHref] = useState<string | null>(null)
|
||||
|
||||
if (variant === 'workbench') {
|
||||
const shouldRestrictNavigation =
|
||||
@ -68,6 +69,21 @@ export function AppTopNav({ user, variant = 'default' }: AppTopNavProps) {
|
||||
const activeNavItem =
|
||||
visibleNavItems.find((item) => matchesWorkbenchHref(pathname, item.href)) ??
|
||||
visibleNavItems[0]
|
||||
const visiblePendingHref =
|
||||
pendingHref && !matchesWorkbenchHref(pathname, pendingHref)
|
||||
? pendingHref
|
||||
: null
|
||||
const pendingNavItem = visiblePendingHref
|
||||
? visibleNavItems.find((item) => item.href === visiblePendingHref)
|
||||
: null
|
||||
|
||||
function handleNavIntent(href: string) {
|
||||
if (matchesWorkbenchHref(pathname, href)) {
|
||||
setPendingHref(null)
|
||||
return
|
||||
}
|
||||
setPendingHref(href)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="fixed left-0 right-0 top-0 z-50 flex h-16 items-center border-b border-black/[0.04] bg-white/[0.92] px-3 font-[Manrope,sans-serif] tracking-tight backdrop-blur-2xl sm:px-8">
|
||||
@ -102,10 +118,15 @@ export function AppTopNav({ user, variant = 'default' }: AppTopNavProps) {
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setMobileWorkbenchNavOpen(false)}
|
||||
onClick={() => {
|
||||
handleNavIntent(item.href)
|
||||
setMobileWorkbenchNavOpen(false)
|
||||
}}
|
||||
className={[
|
||||
'flex min-h-[44px] items-center gap-2 rounded-[10px] px-3 text-[13px] font-semibold transition',
|
||||
active
|
||||
visiblePendingHref === item.href
|
||||
? 'bg-black/[0.07] text-[#2d3335]'
|
||||
: active
|
||||
? 'bg-[#050505] text-white'
|
||||
: 'text-[#2d3335] hover:bg-black/[0.04]',
|
||||
].join(' ')}
|
||||
@ -139,20 +160,30 @@ export function AppTopNav({ user, variant = 'default' }: AppTopNavProps) {
|
||||
{visibleNavItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = matchesWorkbenchHref(pathname, item.href)
|
||||
const pending = visiblePendingHref === item.href
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => handleNavIntent(item.href)}
|
||||
className={[
|
||||
'flex h-10 items-center gap-2 rounded-[6px] px-3 text-[13px] font-semibold transition',
|
||||
active
|
||||
'relative flex h-10 items-center gap-2 overflow-hidden rounded-[6px] px-3 text-[13px] font-semibold transition',
|
||||
pending
|
||||
? 'bg-black/[0.055] text-[#2d3335]'
|
||||
: active
|
||||
? 'bg-black/[0.04] text-[#2d3335] shadow-sm'
|
||||
: 'text-[#5a6062]/70 hover:bg-black/[0.025] hover:text-[#2d3335]',
|
||||
].join(' ')}
|
||||
aria-busy={pending || undefined}
|
||||
>
|
||||
<Icon size={15} strokeWidth={1.8} />
|
||||
<span>{item.label}</span>
|
||||
{pending ? (
|
||||
<span className="absolute inset-x-2 bottom-1 h-0.5 overflow-hidden rounded-full bg-black/[0.08]">
|
||||
<span className="block h-full w-1/2 animate-[nav-progress_1s_ease-in-out_infinite] rounded-full bg-[#2d3335]/70" />
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
@ -165,6 +196,11 @@ export function AppTopNav({ user, variant = 'default' }: AppTopNavProps) {
|
||||
</button>
|
||||
<UserMenu user={user ?? null} />
|
||||
</div>
|
||||
{pendingNavItem ? (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-[2px] overflow-hidden bg-black/[0.04]">
|
||||
<div className="h-full w-1/3 animate-[nav-progress_1s_ease-in-out_infinite] rounded-full bg-[#2d3335]/70" />
|
||||
</div>
|
||||
) : null}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,178 @@
|
||||
export const LEGACY_ASSET_SKU_GROUP_KEY = 'legacySkuGroup'
|
||||
export const LEGACY_ASSET_SKU_GROUPS_KEY = 'legacySkuGroups'
|
||||
|
||||
export interface LegacyAssetSkuGroupSnapshot {
|
||||
id: string
|
||||
assetFamilyId: string | null
|
||||
code: string
|
||||
name: string
|
||||
description: string | null
|
||||
sortOrder: number
|
||||
metadata: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function readText(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
}
|
||||
|
||||
function readNullableText(value: unknown): string | null {
|
||||
const text = readText(value)
|
||||
return text || null
|
||||
}
|
||||
|
||||
function readSortOrder(value: unknown, fallback: number): number {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const parsed = Number.parseInt(value.trim(), 10)
|
||||
if (Number.isFinite(parsed)) return parsed
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
export function buildLegacyAssetSkuGroupId(assetFamilyId: string, code: string): string {
|
||||
const normalizedFamilyId = assetFamilyId.trim()
|
||||
const normalizedCode = code.trim().toUpperCase()
|
||||
return `legacy-sku-group:${normalizedFamilyId}:${normalizedCode}`
|
||||
}
|
||||
|
||||
export function normalizeLegacyAssetSkuGroupSnapshot(
|
||||
value: unknown,
|
||||
fallback: {
|
||||
assetFamilyId?: string | null
|
||||
id?: string | null
|
||||
code?: string | null
|
||||
name?: string | null
|
||||
sortOrder?: number
|
||||
metadata?: Record<string, unknown> | null
|
||||
} = {},
|
||||
): LegacyAssetSkuGroupSnapshot | null {
|
||||
const record = isPlainRecord(value) ? value : {}
|
||||
const assetFamilyId = readText(record.assetFamilyId) || fallback.assetFamilyId?.trim() || null
|
||||
const code = (readText(record.code) || fallback.code?.trim() || '').toUpperCase()
|
||||
if (!code) return null
|
||||
|
||||
const id =
|
||||
readText(record.id) ||
|
||||
fallback.id?.trim() ||
|
||||
(assetFamilyId ? buildLegacyAssetSkuGroupId(assetFamilyId, code) : `legacy-sku-group:${code}`)
|
||||
const name = readText(record.name) || fallback.name?.trim() || code
|
||||
const metadata = isPlainRecord(record.metadata)
|
||||
? { ...record.metadata }
|
||||
: fallback.metadata
|
||||
? { ...fallback.metadata }
|
||||
: null
|
||||
|
||||
return {
|
||||
id,
|
||||
assetFamilyId,
|
||||
code,
|
||||
name,
|
||||
description: readNullableText(record.description),
|
||||
sortOrder: readSortOrder(record.sortOrder, fallback.sortOrder ?? Number.MAX_SAFE_INTEGER),
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
||||
export function createLegacyAssetSkuGroupSnapshot(input: {
|
||||
assetFamilyId: string
|
||||
code: string
|
||||
name?: string | null
|
||||
description?: string | null
|
||||
sortOrder?: number
|
||||
metadata?: Record<string, unknown> | null
|
||||
id?: string | null
|
||||
}): LegacyAssetSkuGroupSnapshot {
|
||||
const assetFamilyId = input.assetFamilyId.trim()
|
||||
const code = input.code.trim().toUpperCase()
|
||||
const id = input.id?.trim() || buildLegacyAssetSkuGroupId(assetFamilyId, code)
|
||||
return {
|
||||
id,
|
||||
assetFamilyId,
|
||||
code,
|
||||
name: input.name?.trim() || code,
|
||||
description: input.description?.trim() || null,
|
||||
sortOrder: input.sortOrder ?? Number.MAX_SAFE_INTEGER,
|
||||
metadata: input.metadata ? { ...input.metadata } : null,
|
||||
}
|
||||
}
|
||||
|
||||
export function readLegacyAssetSkuGroupFromBusinessData(
|
||||
businessData: unknown,
|
||||
fallback: Parameters<typeof normalizeLegacyAssetSkuGroupSnapshot>[1] = {},
|
||||
): LegacyAssetSkuGroupSnapshot | null {
|
||||
if (!isPlainRecord(businessData)) return null
|
||||
const rawSnapshot = businessData[LEGACY_ASSET_SKU_GROUP_KEY]
|
||||
if (!isPlainRecord(rawSnapshot)) return null
|
||||
return normalizeLegacyAssetSkuGroupSnapshot(rawSnapshot, fallback)
|
||||
}
|
||||
|
||||
export function readLegacyAssetSkuGroupsFromFamilyMetadata(
|
||||
metadata: unknown,
|
||||
assetFamilyId?: string | null,
|
||||
): LegacyAssetSkuGroupSnapshot[] {
|
||||
if (!isPlainRecord(metadata) || !Array.isArray(metadata[LEGACY_ASSET_SKU_GROUPS_KEY])) {
|
||||
return []
|
||||
}
|
||||
|
||||
return metadata[LEGACY_ASSET_SKU_GROUPS_KEY]
|
||||
.map((item, index) =>
|
||||
normalizeLegacyAssetSkuGroupSnapshot(item, {
|
||||
assetFamilyId,
|
||||
sortOrder: index,
|
||||
}),
|
||||
)
|
||||
.filter((item): item is LegacyAssetSkuGroupSnapshot => item !== null)
|
||||
}
|
||||
|
||||
export function serializeLegacyAssetSkuGroupSnapshot(
|
||||
snapshot: LegacyAssetSkuGroupSnapshot,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
id: snapshot.id,
|
||||
assetFamilyId: snapshot.assetFamilyId,
|
||||
code: snapshot.code,
|
||||
name: snapshot.name,
|
||||
description: snapshot.description,
|
||||
sortOrder: snapshot.sortOrder,
|
||||
metadata: snapshot.metadata,
|
||||
source: 'AssetSkuGroup.retired',
|
||||
}
|
||||
}
|
||||
|
||||
export function writeLegacyAssetSkuGroupToBusinessData(
|
||||
businessData: unknown,
|
||||
snapshot: LegacyAssetSkuGroupSnapshot,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
...(isPlainRecord(businessData) ? businessData : {}),
|
||||
[LEGACY_ASSET_SKU_GROUP_KEY]: serializeLegacyAssetSkuGroupSnapshot(snapshot),
|
||||
}
|
||||
}
|
||||
|
||||
export function writeLegacyAssetSkuGroupsToFamilyMetadata(
|
||||
metadata: unknown,
|
||||
snapshots: LegacyAssetSkuGroupSnapshot[],
|
||||
): Record<string, unknown> | null {
|
||||
const next = { ...(isPlainRecord(metadata) ? metadata : {}) }
|
||||
if (snapshots.length > 0) {
|
||||
const serializedSnapshots = snapshots
|
||||
.map((snapshot, index) => ({
|
||||
...serializeLegacyAssetSkuGroupSnapshot(snapshot),
|
||||
sortOrder: snapshot.sortOrder === Number.MAX_SAFE_INTEGER ? index : snapshot.sortOrder,
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
const leftSort = typeof left.sortOrder === 'number' ? left.sortOrder : Number.MAX_SAFE_INTEGER
|
||||
const rightSort = typeof right.sortOrder === 'number' ? right.sortOrder : Number.MAX_SAFE_INTEGER
|
||||
return leftSort - rightSort || String(left['code']).localeCompare(String(right['code']), 'zh-CN', { numeric: true })
|
||||
})
|
||||
next[LEGACY_ASSET_SKU_GROUPS_KEY] = serializedSnapshots
|
||||
} else {
|
||||
delete next[LEGACY_ASSET_SKU_GROUPS_KEY]
|
||||
}
|
||||
|
||||
return Object.keys(next).length > 0 ? next : null
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { AssetFamily, AssetSeries, AssetSkuGroup, AssetVariant, Prisma, Substrate } from '@prisma/client'
|
||||
import { AssetFamily, AssetSeries, AssetVariant, Prisma, Substrate } from '@prisma/client'
|
||||
import { BaseRepository, FindManyOptions } from './base.repository'
|
||||
import { prisma as prismaClient } from '@/lib/prisma'
|
||||
import {
|
||||
buildDeletedAssetVariantUniqueValue,
|
||||
buildSoftDeletedAssetVariantBusinessData,
|
||||
@ -9,6 +10,15 @@ import {
|
||||
getRestorableAssetVariantIdentityKey,
|
||||
} from '@/lib/open-product/asset-variant-soft-delete'
|
||||
import { normalizeCoreIdentity } from '@/lib/open-product/asset-coding'
|
||||
import {
|
||||
LegacyAssetSkuGroupSnapshot,
|
||||
createLegacyAssetSkuGroupSnapshot,
|
||||
isPlainRecord,
|
||||
readLegacyAssetSkuGroupFromBusinessData,
|
||||
readLegacyAssetSkuGroupsFromFamilyMetadata,
|
||||
writeLegacyAssetSkuGroupToBusinessData,
|
||||
writeLegacyAssetSkuGroupsToFamilyMetadata,
|
||||
} from '@/lib/open-product/asset-sku-group-legacy'
|
||||
|
||||
export interface AssetCenterFinishRelation {
|
||||
id: string
|
||||
@ -76,7 +86,10 @@ export interface AssetSkuGroupFilter {
|
||||
assetFamilyId?: string
|
||||
}
|
||||
|
||||
export interface AssetSkuGroupWithRelations extends AssetSkuGroup {
|
||||
export interface AssetSkuGroupWithRelations extends LegacyAssetSkuGroupSnapshot {
|
||||
assetFamilyId: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
assetFamily: {
|
||||
id: string
|
||||
code: string
|
||||
@ -99,7 +112,8 @@ export interface AssetVariantWithRelations extends AssetVariant {
|
||||
deletedAt: Date | null
|
||||
assetSeries: { id: string; code: string; name: string } | null
|
||||
}
|
||||
skuGroup: {
|
||||
skuGroupId?: string | null
|
||||
skuGroup?: {
|
||||
id: string
|
||||
assetFamilyId: string
|
||||
code: string
|
||||
@ -181,17 +195,6 @@ export const assetVariantRelationInclude = {
|
||||
assetSeries: { select: { id: true, code: true, name: true, metadata: true } },
|
||||
},
|
||||
},
|
||||
skuGroup: {
|
||||
select: {
|
||||
id: true,
|
||||
assetFamilyId: true,
|
||||
code: true,
|
||||
name: true,
|
||||
description: true,
|
||||
sortOrder: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
substrate: { select: { id: true, code: true, name: true } },
|
||||
finish: {
|
||||
select: {
|
||||
@ -221,21 +224,6 @@ export const assetVariantRelationInclude = {
|
||||
lastModifier: { select: { id: true, name: true } },
|
||||
} satisfies Prisma.AssetVariantInclude
|
||||
|
||||
export const assetSkuGroupRelationInclude = {
|
||||
assetFamily: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
type: true,
|
||||
assetSeriesId: true,
|
||||
deletedAt: true,
|
||||
assetSeries: { select: { id: true, code: true, name: true, metadata: true } },
|
||||
},
|
||||
},
|
||||
_count: { select: { variants: true } },
|
||||
} satisfies Prisma.AssetSkuGroupInclude
|
||||
|
||||
export const substrateRelationInclude = {
|
||||
category: { select: { id: true, name: true } },
|
||||
environmentGrade: { select: { id: true, name: true } },
|
||||
@ -449,107 +437,306 @@ class AssetSeriesRepositoryClass extends BaseRepository<
|
||||
}
|
||||
}
|
||||
|
||||
class AssetSkuGroupRepositoryClass extends BaseRepository<
|
||||
AssetSkuGroup,
|
||||
Prisma.AssetSkuGroupCreateInput,
|
||||
Prisma.AssetSkuGroupUpdateInput,
|
||||
AssetSkuGroupFilter
|
||||
> {
|
||||
protected modelName = 'assetSkuGroup'
|
||||
class AssetSkuGroupRepositoryClass {
|
||||
private prisma = prismaClient
|
||||
|
||||
private toRecord(input: {
|
||||
family: AssetFamilyWithRelations
|
||||
snapshot: LegacyAssetSkuGroupSnapshot
|
||||
variantCount?: number
|
||||
}): AssetSkuGroupWithRelations {
|
||||
return {
|
||||
...input.snapshot,
|
||||
assetFamilyId: input.family.id,
|
||||
assetFamily: {
|
||||
id: input.family.id,
|
||||
code: input.family.code,
|
||||
name: input.family.name,
|
||||
type: input.family.type,
|
||||
assetSeriesId: input.family.assetSeriesId,
|
||||
deletedAt: input.family.deletedAt,
|
||||
assetSeries: input.family.assetSeries
|
||||
? {
|
||||
id: input.family.assetSeries.id,
|
||||
code: input.family.assetSeries.code,
|
||||
name: input.family.assetSeries.name,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
_count: { variants: input.variantCount ?? 0 },
|
||||
createdAt: input.family.createdAt,
|
||||
updatedAt: input.family.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
private sortRecords(records: AssetSkuGroupWithRelations[]): AssetSkuGroupWithRelations[] {
|
||||
return [...records].sort(
|
||||
(left, right) =>
|
||||
left.assetFamilyId.localeCompare(right.assetFamilyId) ||
|
||||
left.sortOrder - right.sortOrder ||
|
||||
left.code.localeCompare(right.code, 'zh-CN', { numeric: true }),
|
||||
)
|
||||
}
|
||||
|
||||
private getVariantSnapshot(record: AssetVariantWithRelations): LegacyAssetSkuGroupSnapshot | null {
|
||||
return readLegacyAssetSkuGroupFromBusinessData(record.businessData, {
|
||||
assetFamilyId: record.assetFamilyId,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
id: `variant:${record.id}`,
|
||||
sortOrder: Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
}
|
||||
|
||||
async findByIdWithRelations(id: string): Promise<AssetSkuGroupWithRelations | null> {
|
||||
return this.prisma.assetSkuGroup.findUnique({
|
||||
where: { id },
|
||||
include: assetSkuGroupRelationInclude,
|
||||
}) as Promise<AssetSkuGroupWithRelations | null>
|
||||
const families = await this.prisma.assetFamily.findMany({
|
||||
where: { deletedAt: null },
|
||||
include: assetFamilyRelationInclude,
|
||||
}) as AssetFamilyWithRelations[]
|
||||
|
||||
for (const family of families) {
|
||||
const snapshot = readLegacyAssetSkuGroupsFromFamilyMetadata(family.metadata, family.id)
|
||||
.find((group) => group.id === id) ?? null
|
||||
if (!snapshot) continue
|
||||
|
||||
const variantCount = await this.prisma.assetVariant.count({
|
||||
where: {
|
||||
assetFamilyId: family.id,
|
||||
deletedAt: null,
|
||||
businessData: {
|
||||
path: ['legacySkuGroup', 'id'],
|
||||
equals: id,
|
||||
} as Prisma.JsonFilter,
|
||||
},
|
||||
})
|
||||
return this.toRecord({ family, snapshot, variantCount })
|
||||
}
|
||||
|
||||
const variant = await this.prisma.assetVariant.findFirst({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
businessData: {
|
||||
path: ['legacySkuGroup', 'id'],
|
||||
equals: id,
|
||||
} as Prisma.JsonFilter,
|
||||
assetFamily: { is: { deletedAt: null } },
|
||||
},
|
||||
include: assetVariantRelationInclude,
|
||||
}) as AssetVariantWithRelations | null
|
||||
if (!variant) return null
|
||||
|
||||
const snapshot = this.getVariantSnapshot(variant)
|
||||
return snapshot
|
||||
? this.toRecord({ family: variant.assetFamily as unknown as AssetFamilyWithRelations, snapshot, variantCount: 1 })
|
||||
: null
|
||||
}
|
||||
|
||||
async findByFamilyAndCode(
|
||||
assetFamilyId: string,
|
||||
code: string,
|
||||
): Promise<AssetSkuGroupWithRelations | null> {
|
||||
return this.prisma.assetSkuGroup.findUnique({
|
||||
where: {
|
||||
assetFamilyId_code: {
|
||||
assetFamilyId,
|
||||
code: normalizeCoreIdentity(code),
|
||||
},
|
||||
},
|
||||
include: assetSkuGroupRelationInclude,
|
||||
}) as Promise<AssetSkuGroupWithRelations | null>
|
||||
const normalizedCode = normalizeCoreIdentity(code)
|
||||
if (!assetFamilyId || !normalizedCode) return null
|
||||
|
||||
const groups = await this.findManyByFamilyIds([assetFamilyId])
|
||||
return groups.find((group) => normalizeCoreIdentity(group.code) === normalizedCode) ?? null
|
||||
}
|
||||
|
||||
async findManyByFamilyIds(assetFamilyIds: string[]): Promise<AssetSkuGroupWithRelations[]> {
|
||||
if (assetFamilyIds.length === 0) return []
|
||||
const ids = Array.from(new Set(assetFamilyIds.map((id) => id.trim()).filter(Boolean)))
|
||||
if (ids.length === 0) return []
|
||||
|
||||
return this.prisma.assetSkuGroup.findMany({
|
||||
where: {
|
||||
assetFamilyId: { in: assetFamilyIds },
|
||||
assetFamily: { is: { deletedAt: null } },
|
||||
},
|
||||
orderBy: [
|
||||
{ assetFamilyId: 'asc' },
|
||||
{ sortOrder: 'asc' },
|
||||
{ code: 'asc' },
|
||||
],
|
||||
include: assetSkuGroupRelationInclude,
|
||||
}) as Promise<AssetSkuGroupWithRelations[]>
|
||||
const [families, variants] = await Promise.all([
|
||||
this.prisma.assetFamily.findMany({
|
||||
where: { id: { in: ids }, deletedAt: null },
|
||||
include: assetFamilyRelationInclude,
|
||||
}) as Promise<AssetFamilyWithRelations[]>,
|
||||
this.prisma.assetVariant.findMany({
|
||||
where: {
|
||||
assetFamilyId: { in: ids },
|
||||
deletedAt: null,
|
||||
assetFamily: { is: { deletedAt: null } },
|
||||
},
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations[]>,
|
||||
])
|
||||
|
||||
const familyById = new Map(families.map((family) => [family.id, family]))
|
||||
const variantCountByGroupId = new Map<string, number>()
|
||||
const snapshotById = new Map<string, LegacyAssetSkuGroupSnapshot>()
|
||||
|
||||
variants.forEach((variant) => {
|
||||
const snapshot = this.getVariantSnapshot(variant)
|
||||
if (!snapshot) return
|
||||
snapshotById.set(snapshot.id, snapshot)
|
||||
variantCountByGroupId.set(snapshot.id, (variantCountByGroupId.get(snapshot.id) ?? 0) + 1)
|
||||
})
|
||||
|
||||
families.forEach((family) => {
|
||||
readLegacyAssetSkuGroupsFromFamilyMetadata(family.metadata, family.id).forEach((snapshot) => {
|
||||
snapshotById.set(snapshot.id, snapshot)
|
||||
})
|
||||
})
|
||||
|
||||
const records = Array.from(snapshotById.values())
|
||||
.map((snapshot) => {
|
||||
const familyId = snapshot.assetFamilyId ?? ''
|
||||
const family = familyById.get(familyId)
|
||||
return family
|
||||
? this.toRecord({
|
||||
family,
|
||||
snapshot,
|
||||
variantCount: variantCountByGroupId.get(snapshot.id) ?? 0,
|
||||
})
|
||||
: null
|
||||
})
|
||||
.filter((item): item is AssetSkuGroupWithRelations => item !== null)
|
||||
|
||||
return this.sortRecords(records)
|
||||
}
|
||||
|
||||
async findManyBySeriesCode(seriesCode: string): Promise<AssetSkuGroupWithRelations[]> {
|
||||
const normalizedSeriesCode = normalizeCoreIdentity(seriesCode)
|
||||
if (!normalizedSeriesCode) return []
|
||||
|
||||
return this.prisma.assetSkuGroup.findMany({
|
||||
const families = await this.prisma.assetFamily.findMany({
|
||||
where: {
|
||||
assetFamily: {
|
||||
is: {
|
||||
deletedAt: null,
|
||||
assetSeries: {
|
||||
is: { code: normalizedSeriesCode },
|
||||
},
|
||||
},
|
||||
},
|
||||
deletedAt: null,
|
||||
assetSeries: { is: { code: normalizedSeriesCode } },
|
||||
},
|
||||
orderBy: [
|
||||
{ assetFamilyId: 'asc' },
|
||||
{ sortOrder: 'asc' },
|
||||
{ code: 'asc' },
|
||||
],
|
||||
include: assetSkuGroupRelationInclude,
|
||||
}) as Promise<AssetSkuGroupWithRelations[]>
|
||||
select: { id: true },
|
||||
})
|
||||
return this.findManyByFamilyIds(families.map((family) => family.id))
|
||||
}
|
||||
|
||||
async createWithRelations(data: Prisma.AssetSkuGroupCreateInput): Promise<AssetSkuGroupWithRelations> {
|
||||
return this.prisma.assetSkuGroup.create({
|
||||
data,
|
||||
include: assetSkuGroupRelationInclude,
|
||||
}) as Promise<AssetSkuGroupWithRelations>
|
||||
async createWithRelations(data: {
|
||||
assetFamily: { connect: { id: string } }
|
||||
code: string
|
||||
name: string
|
||||
description?: string | null
|
||||
sortOrder?: number
|
||||
metadata?: Prisma.InputJsonValue | Prisma.NullableJsonNullValueInput | null
|
||||
}): Promise<AssetSkuGroupWithRelations> {
|
||||
const familyId = data.assetFamily.connect.id
|
||||
const family = await assetFamilyRepository.findWithRelationsById(familyId)
|
||||
if (!family) {
|
||||
throw new Error('Asset family not found')
|
||||
}
|
||||
|
||||
const snapshot = createLegacyAssetSkuGroupSnapshot({
|
||||
assetFamilyId: family.id,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
sortOrder: data.sortOrder,
|
||||
metadata: isPlainRecord(data.metadata) ? data.metadata : null,
|
||||
})
|
||||
const existing = readLegacyAssetSkuGroupsFromFamilyMetadata(family.metadata, family.id)
|
||||
.filter((group) => group.id !== snapshot.id)
|
||||
const metadata = writeLegacyAssetSkuGroupsToFamilyMetadata(family.metadata, [...existing, snapshot])
|
||||
|
||||
const updatedFamily = await this.prisma.assetFamily.update({
|
||||
where: { id: family.id },
|
||||
data: { metadata: metadata === null ? Prisma.JsonNull : metadata as Prisma.InputJsonValue },
|
||||
include: assetFamilyRelationInclude,
|
||||
}) as AssetFamilyWithRelations
|
||||
|
||||
return this.toRecord({ family: updatedFamily, snapshot })
|
||||
}
|
||||
|
||||
async updateWithRelations(
|
||||
id: string,
|
||||
data: Prisma.AssetSkuGroupUpdateInput,
|
||||
data: {
|
||||
code?: string
|
||||
name?: string
|
||||
description?: string | null
|
||||
sortOrder?: number
|
||||
metadata?: Prisma.InputJsonValue | Prisma.NullableJsonNullValueInput | null
|
||||
},
|
||||
): Promise<AssetSkuGroupWithRelations> {
|
||||
return this.prisma.assetSkuGroup.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: assetSkuGroupRelationInclude,
|
||||
}) as Promise<AssetSkuGroupWithRelations>
|
||||
const existing = await this.findByIdWithRelations(id)
|
||||
if (!existing) {
|
||||
throw new Error('Asset SKU group not found')
|
||||
}
|
||||
const family = await assetFamilyRepository.findWithRelationsById(existing.assetFamilyId)
|
||||
if (!family) {
|
||||
throw new Error('Asset family not found')
|
||||
}
|
||||
|
||||
const nextSnapshot = createLegacyAssetSkuGroupSnapshot({
|
||||
assetFamilyId: family.id,
|
||||
id: existing.id,
|
||||
code: data.code ?? existing.code,
|
||||
name: data.name ?? existing.name,
|
||||
description: data.description === undefined ? existing.description : data.description,
|
||||
sortOrder: data.sortOrder ?? existing.sortOrder,
|
||||
metadata: data.metadata === undefined
|
||||
? existing.metadata
|
||||
: isPlainRecord(data.metadata)
|
||||
? data.metadata
|
||||
: null,
|
||||
})
|
||||
const groups = readLegacyAssetSkuGroupsFromFamilyMetadata(family.metadata, family.id)
|
||||
.filter((group) => group.id !== id)
|
||||
const metadata = writeLegacyAssetSkuGroupsToFamilyMetadata(family.metadata, [...groups, nextSnapshot])
|
||||
|
||||
const variants = await this.prisma.assetVariant.findMany({
|
||||
where: {
|
||||
assetFamilyId: family.id,
|
||||
deletedAt: null,
|
||||
businessData: {
|
||||
path: ['legacySkuGroup', 'id'],
|
||||
equals: id,
|
||||
} as Prisma.JsonFilter,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
businessData: true,
|
||||
},
|
||||
})
|
||||
|
||||
const updatedFamily = await this.prisma.$transaction(async (tx) => {
|
||||
const nextFamily = await tx.assetFamily.update({
|
||||
where: { id: family.id },
|
||||
data: { metadata: metadata === null ? Prisma.JsonNull : metadata as Prisma.InputJsonValue },
|
||||
include: assetFamilyRelationInclude,
|
||||
})
|
||||
await Promise.all(
|
||||
variants.map((variant) =>
|
||||
tx.assetVariant.update({
|
||||
where: { id: variant.id },
|
||||
data: {
|
||||
businessData: writeLegacyAssetSkuGroupToBusinessData(
|
||||
variant.businessData,
|
||||
nextSnapshot,
|
||||
) as Prisma.InputJsonValue,
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
return nextFamily
|
||||
}) as AssetFamilyWithRelations
|
||||
|
||||
return this.toRecord({ family: updatedFamily, snapshot: nextSnapshot })
|
||||
}
|
||||
|
||||
async deleteById(id: string): Promise<void> {
|
||||
await this.prisma.assetSkuGroup.delete({ where: { id } })
|
||||
}
|
||||
const existing = await this.findByIdWithRelations(id)
|
||||
if (!existing) return
|
||||
const family = await assetFamilyRepository.findWithRelationsById(existing.assetFamilyId)
|
||||
if (!family) return
|
||||
|
||||
protected buildWhereClause(filter?: AssetSkuGroupFilter): Prisma.AssetSkuGroupWhereInput {
|
||||
return {
|
||||
assetFamilyId: filter?.assetFamilyId,
|
||||
assetFamily: { is: { deletedAt: null } },
|
||||
}
|
||||
const groups = readLegacyAssetSkuGroupsFromFamilyMetadata(family.metadata, family.id)
|
||||
.filter((group) => group.id !== id)
|
||||
const metadata = writeLegacyAssetSkuGroupsToFamilyMetadata(family.metadata, groups)
|
||||
await this.prisma.assetFamily.update({
|
||||
where: { id: family.id },
|
||||
data: { metadata: metadata === null ? Prisma.JsonNull : metadata as Prisma.InputJsonValue },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AssetVariantRepositoryClass extends BaseRepository<
|
||||
AssetVariant,
|
||||
Prisma.AssetVariantCreateInput,
|
||||
@ -558,6 +745,42 @@ class AssetVariantRepositoryClass extends BaseRepository<
|
||||
> {
|
||||
protected modelName = 'assetVariant'
|
||||
|
||||
private attachLegacySkuGroup(record: AssetVariantWithRelations): AssetVariantWithRelations {
|
||||
const snapshot = readLegacyAssetSkuGroupFromBusinessData(record.businessData, {
|
||||
assetFamilyId: record.assetFamilyId,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
id: `variant:${record.id}`,
|
||||
sortOrder: Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
|
||||
if (!snapshot) {
|
||||
return {
|
||||
...record,
|
||||
skuGroupId: null,
|
||||
skuGroup: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
skuGroupId: snapshot.id,
|
||||
skuGroup: {
|
||||
id: snapshot.id,
|
||||
assetFamilyId: snapshot.assetFamilyId ?? record.assetFamilyId,
|
||||
code: snapshot.code,
|
||||
name: snapshot.name,
|
||||
description: snapshot.description,
|
||||
sortOrder: snapshot.sortOrder,
|
||||
metadata: snapshot.metadata as Prisma.JsonValue | null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private attachLegacySkuGroups(records: AssetVariantWithRelations[]): AssetVariantWithRelations[] {
|
||||
return records.map((record) => this.attachLegacySkuGroup(record))
|
||||
}
|
||||
|
||||
async findByCode(code: string): Promise<AssetVariant | null> {
|
||||
return this.prisma.assetVariant.findFirst({
|
||||
where: {
|
||||
@ -668,16 +891,17 @@ class AssetVariantRepositoryClass extends BaseRepository<
|
||||
}
|
||||
|
||||
async findByIdWithRelations(id: string): Promise<AssetVariantWithRelations | null> {
|
||||
return this.prisma.assetVariant.findUnique({
|
||||
const record = await this.prisma.assetVariant.findUnique({
|
||||
where: { id },
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations | null>
|
||||
}) as AssetVariantWithRelations | null
|
||||
return record ? this.attachLegacySkuGroup(record) : null
|
||||
}
|
||||
|
||||
async findByIdsWithRelations(ids: string[]): Promise<AssetVariantWithRelations[]> {
|
||||
if (ids.length === 0) return []
|
||||
|
||||
return this.prisma.assetVariant.findMany({
|
||||
const records = await this.prisma.assetVariant.findMany({
|
||||
where: {
|
||||
id: { in: ids },
|
||||
deletedAt: null,
|
||||
@ -685,19 +909,21 @@ class AssetVariantRepositoryClass extends BaseRepository<
|
||||
},
|
||||
orderBy: [{ assetFamilyId: 'asc' }, { createdAt: 'desc' }, { code: 'desc' }],
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations[]>
|
||||
}) as AssetVariantWithRelations[]
|
||||
return this.attachLegacySkuGroups(records)
|
||||
}
|
||||
|
||||
async findByIdsWithRelationsIncludingDeleted(ids: string[]): Promise<AssetVariantWithRelations[]> {
|
||||
if (ids.length === 0) return []
|
||||
|
||||
return this.prisma.assetVariant.findMany({
|
||||
const records = await this.prisma.assetVariant.findMany({
|
||||
where: {
|
||||
id: { in: ids },
|
||||
},
|
||||
orderBy: [{ assetFamilyId: 'asc' }, { createdAt: 'desc' }, { code: 'desc' }],
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations[]>
|
||||
}) as AssetVariantWithRelations[]
|
||||
return this.attachLegacySkuGroups(records)
|
||||
}
|
||||
|
||||
async findManyByFamilyIds(
|
||||
@ -706,7 +932,7 @@ class AssetVariantRepositoryClass extends BaseRepository<
|
||||
): Promise<AssetVariantWithRelations[]> {
|
||||
if (assetFamilyIds.length === 0) return []
|
||||
|
||||
return this.prisma.assetVariant.findMany({
|
||||
const records = await this.prisma.assetVariant.findMany({
|
||||
where: {
|
||||
assetFamilyId: { in: assetFamilyIds },
|
||||
status: filter?.status,
|
||||
@ -715,13 +941,14 @@ class AssetVariantRepositoryClass extends BaseRepository<
|
||||
},
|
||||
orderBy: [{ assetFamilyId: 'asc' }, { createdAt: 'desc' }, { code: 'desc' }],
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations[]>
|
||||
}) as AssetVariantWithRelations[]
|
||||
return this.attachLegacySkuGroups(records)
|
||||
}
|
||||
|
||||
async findAvailableByFamilyIds(assetFamilyIds: string[]): Promise<AssetVariantWithRelations[]> {
|
||||
if (assetFamilyIds.length === 0) return []
|
||||
|
||||
return this.prisma.assetVariant.findMany({
|
||||
const records = await this.prisma.assetVariant.findMany({
|
||||
where: {
|
||||
assetFamilyId: { in: assetFamilyIds },
|
||||
status: 'AVAILABLE',
|
||||
@ -730,14 +957,15 @@ class AssetVariantRepositoryClass extends BaseRepository<
|
||||
},
|
||||
orderBy: [{ assetFamilyId: 'asc' }, { createdAt: 'desc' }, { code: 'desc' }],
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations[]>
|
||||
}) as AssetVariantWithRelations[]
|
||||
return this.attachLegacySkuGroups(records)
|
||||
}
|
||||
|
||||
async findActiveBySeriesCode(seriesCode: string): Promise<AssetVariantWithRelations[]> {
|
||||
const normalizedSeriesCode = seriesCode.trim().toUpperCase()
|
||||
if (!normalizedSeriesCode) return []
|
||||
|
||||
return this.prisma.assetVariant.findMany({
|
||||
const records = await this.prisma.assetVariant.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
assetFamily: {
|
||||
@ -751,25 +979,28 @@ class AssetVariantRepositoryClass extends BaseRepository<
|
||||
},
|
||||
orderBy: [{ code: 'asc' }, { updatedAt: 'asc' }],
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations[]>
|
||||
}) as AssetVariantWithRelations[]
|
||||
return this.attachLegacySkuGroups(records)
|
||||
}
|
||||
|
||||
async createWithRelations(data: Prisma.AssetVariantCreateInput): Promise<AssetVariantWithRelations> {
|
||||
return this.prisma.assetVariant.create({
|
||||
const record = await this.prisma.assetVariant.create({
|
||||
data,
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations>
|
||||
}) as AssetVariantWithRelations
|
||||
return this.attachLegacySkuGroup(record)
|
||||
}
|
||||
|
||||
async updateWithRelations(
|
||||
id: string,
|
||||
data: Prisma.AssetVariantUpdateInput,
|
||||
): Promise<AssetVariantWithRelations> {
|
||||
return this.prisma.assetVariant.update({
|
||||
const record = await this.prisma.assetVariant.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations>
|
||||
}) as AssetVariantWithRelations
|
||||
return this.attachLegacySkuGroup(record)
|
||||
}
|
||||
|
||||
async deleteById(id: string): Promise<void> {
|
||||
@ -777,14 +1008,15 @@ class AssetVariantRepositoryClass extends BaseRepository<
|
||||
}
|
||||
|
||||
async restoreById(id: string): Promise<AssetVariantWithRelations> {
|
||||
return this.prisma.assetVariant.update({
|
||||
const record = await this.prisma.assetVariant.update({
|
||||
where: { id },
|
||||
data: {
|
||||
deletedAt: null,
|
||||
deleter: { disconnect: true },
|
||||
},
|
||||
include: assetVariantRelationInclude,
|
||||
}) as Promise<AssetVariantWithRelations>
|
||||
}) as AssetVariantWithRelations
|
||||
return this.attachLegacySkuGroup(record)
|
||||
}
|
||||
|
||||
protected buildWhereClause(filter?: AssetVariantFilter): Prisma.AssetVariantWhereInput {
|
||||
|
||||
@ -150,6 +150,27 @@ class PreSkuRepositoryClass extends BaseRepository<
|
||||
}) as Promise<PreSkuWithRelations[]>
|
||||
}
|
||||
|
||||
async findByDraftGroupIds(draftGroupIds: string[]): Promise<PreSkuWithRelations[]> {
|
||||
if (draftGroupIds.length === 0) return []
|
||||
|
||||
return this.prisma.preSku.findMany({
|
||||
where: {
|
||||
draftGroupId: {
|
||||
in: draftGroupIds,
|
||||
},
|
||||
},
|
||||
orderBy: [{ draftGroupId: 'asc' }, { sequence: 'asc' }],
|
||||
include: {
|
||||
basicInfo: true,
|
||||
packagingPlan: {
|
||||
include: {
|
||||
packagedItems: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as Promise<PreSkuWithRelations[]>
|
||||
}
|
||||
|
||||
async findActiveByAssetSeriesId(
|
||||
assetSeriesId: string,
|
||||
excludeDraftGroupId?: string,
|
||||
|
||||
@ -116,7 +116,6 @@ export {
|
||||
assetFamilyRepository,
|
||||
assetSeriesRepository,
|
||||
assetSkuGroupRepository,
|
||||
assetSkuGroupRelationInclude,
|
||||
assetVariantRelationInclude,
|
||||
assetVariantRepository,
|
||||
substrateRelationInclude,
|
||||
|
||||
@ -866,15 +866,17 @@ describe('assetCenterService', () => {
|
||||
lossFactor: 1,
|
||||
})
|
||||
|
||||
expect(mockedAssetSkuGroupRepository.createWithRelations).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: 'DBZ-SPJ03',
|
||||
}),
|
||||
)
|
||||
expect(mockedAssetSkuGroupRepository.createWithRelations).not.toHaveBeenCalled()
|
||||
expect(mockedAssetVariantRepository.createWithRelations).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: 'DBZ-SPJ03模型1',
|
||||
skuGroup: { connect: { id: 'sku-group-DBZ-SPJ03' } },
|
||||
businessData: expect.objectContaining({
|
||||
legacySkuGroup: expect.objectContaining({
|
||||
id: 'legacy-sku-group:family-board-top:DBZ-SPJ03',
|
||||
assetFamilyId: 'family-board-top',
|
||||
code: 'DBZ-SPJ03',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(variant.code).toBe('DBZ-SPJ03模型1')
|
||||
@ -986,7 +988,13 @@ describe('assetCenterService', () => {
|
||||
expect(mockedAssetVariantRepository.createWithRelations).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: 'DBZ-SPJ02模型3',
|
||||
skuGroup: { connect: { id: 'sku-group-02' } },
|
||||
businessData: expect.objectContaining({
|
||||
legacySkuGroup: expect.objectContaining({
|
||||
id: 'sku-group-02',
|
||||
assetFamilyId: 'family-board-top',
|
||||
code: 'DBZ-SPJ02',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(variant.code).toBe('DBZ-SPJ02模型3')
|
||||
@ -2422,7 +2430,7 @@ describe('assetCenterService', () => {
|
||||
1,
|
||||
'variant-board-top-uv',
|
||||
expect.objectContaining({
|
||||
businessData: {
|
||||
businessData: expect.objectContaining({
|
||||
renderConfig: {
|
||||
uv: {
|
||||
version: 1,
|
||||
@ -2435,7 +2443,7 @@ describe('assetCenterService', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(mockedAssetVariantThumbnailService.generateForVariant).toHaveBeenCalledWith(
|
||||
@ -2537,11 +2545,11 @@ describe('assetCenterService', () => {
|
||||
expect(mockedAssetVariantRepository.updateWithRelations).toHaveBeenCalledWith(
|
||||
'variant-board-top-uv-fast',
|
||||
expect.objectContaining({
|
||||
businessData: {
|
||||
businessData: expect.objectContaining({
|
||||
renderConfig: {
|
||||
uv: uvConfig,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(mockedAssetVariantThumbnailService.generateForVariant).not.toHaveBeenCalled()
|
||||
|
||||
@ -51,6 +51,7 @@ jest.mock('../../repositories', () => ({
|
||||
createManyForDraftGroup: jest.fn(),
|
||||
findActiveByAssetSeriesId: jest.fn(),
|
||||
findByDraftGroupId: jest.fn(),
|
||||
findByDraftGroupIds: jest.fn(),
|
||||
findByIdWithRelations: jest.fn(),
|
||||
updateDisplaySummary: jest.fn(),
|
||||
upsertBasicInfo: jest.fn(),
|
||||
@ -376,6 +377,7 @@ describe('draftGroupService', () => {
|
||||
mockedPreSkuRepository.findByBasicInfoField.mockResolvedValue([])
|
||||
mockedPreSkuRepository.findByBasicInfoFields.mockResolvedValue([])
|
||||
mockedPreSkuRepository.findActiveByAssetSeriesId.mockResolvedValue([])
|
||||
mockedPreSkuRepository.findByDraftGroupIds.mockResolvedValue([])
|
||||
mockedIntakeNonIntegratedProductRepository.findActiveSeriesProductByNames.mockResolvedValue(null)
|
||||
mockedSkuRepository.findBySkuCode.mockResolvedValue(null)
|
||||
mockedSkuRepository.findByAssetProfileBarcode.mockResolvedValue(null)
|
||||
@ -1469,6 +1471,84 @@ describe('draftGroupService', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('批量读取草稿组准 SKU 时也会把资产货号对应条码补到部件快照', async () => {
|
||||
mockedPreSkuRepository.findByDraftGroupIds.mockResolvedValue([
|
||||
{
|
||||
id: 'pre-sku-batch-1',
|
||||
draftGroupId: 'draft-group-1',
|
||||
sequence: 1,
|
||||
defaultName: '单板桌',
|
||||
customName: null,
|
||||
status: 'READY_FOR_INTAKE',
|
||||
sourceCandidateKey: 'candidate-1',
|
||||
familySelection: {},
|
||||
variantSelection: {
|
||||
slots: [
|
||||
{
|
||||
slotKey: 'desktop',
|
||||
role: 'desktop',
|
||||
assetFamilyId: 'family-desktop-a',
|
||||
variantId: 'variant-desktop-a-white',
|
||||
code: 'DBZ-SPJ01',
|
||||
name: '桌面板',
|
||||
assetType: 'BOARD',
|
||||
dimensions: { length: 1400, width: 600, thickness: 25 },
|
||||
weightKg: 14.5,
|
||||
costPrice: 210,
|
||||
modelUrl: '/models/top-a-1200.glb',
|
||||
mountPoints: [],
|
||||
assemblyBasis: {},
|
||||
},
|
||||
],
|
||||
hardwareSelections: [],
|
||||
},
|
||||
recommendedHardware: [],
|
||||
previewNodes: [],
|
||||
displaySummary: null,
|
||||
bomPreview: [],
|
||||
weightPreview: null,
|
||||
componentCodes: ['DBZ-SPJ01'],
|
||||
installStepPreview: [],
|
||||
notes: null,
|
||||
createdAt: new Date('2026-03-30T12:00:00.000Z'),
|
||||
updatedAt: new Date('2026-03-30T12:00:00.000Z'),
|
||||
packagingPlan: null,
|
||||
},
|
||||
] as never)
|
||||
mockedAssetVariantRepository.findByIdsWithRelations.mockResolvedValue([
|
||||
{
|
||||
id: 'variant-desktop-a-white',
|
||||
barcode: 'fallback-barcode',
|
||||
skuGroup: {
|
||||
metadata: {
|
||||
skuContentFields: {
|
||||
correspondingBarcode: 'DNZ038401N1440V01',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
] as never)
|
||||
|
||||
const result = await draftGroupService.listPreSkusByDraftGroupIds([
|
||||
'draft-group-1',
|
||||
'draft-group-1',
|
||||
' ',
|
||||
])
|
||||
|
||||
expect(mockedPreSkuRepository.findByDraftGroupIds).toHaveBeenCalledWith([
|
||||
'draft-group-1',
|
||||
])
|
||||
expect(mockedAssetVariantRepository.findByIdsWithRelations).toHaveBeenCalledWith([
|
||||
'variant-desktop-a-white',
|
||||
])
|
||||
expect(result[0].variantSelection.slots[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
barcode: 'FALLBACK-BARCODE',
|
||||
correspondingBarcode: 'DNZ038401N1440V01',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('读取草稿组详情时优先返回 displaySummary 中的装配状态和 warnings', async () => {
|
||||
mockedPreSkuRepository.findByDraftGroupId.mockResolvedValue([
|
||||
{
|
||||
|
||||
@ -141,6 +141,12 @@ import {
|
||||
normalizeObjectStorageUrlForPersistence,
|
||||
resolveObjectStorageUrlForClientAccess,
|
||||
} from '@/lib/storage'
|
||||
import {
|
||||
createLegacyAssetSkuGroupSnapshot,
|
||||
readLegacyAssetSkuGroupFromBusinessData,
|
||||
writeLegacyAssetSkuGroupToBusinessData,
|
||||
type LegacyAssetSkuGroupSnapshot,
|
||||
} from '@/lib/open-product/asset-sku-group-legacy'
|
||||
|
||||
interface VariantThumbnailRenderOptions {
|
||||
renderBaseUrl?: string | null
|
||||
@ -2111,6 +2117,13 @@ function toMaterialLikeDTO(record: AssetCenterFinishRelation): MaterialLikeDTO {
|
||||
|
||||
function toAssetVariantDTO(record: AssetVariantWithRelations): AssetVariantDTO {
|
||||
const businessData = isRecord(record.businessData) ? record.businessData : null
|
||||
const legacySkuGroup = readLegacyAssetSkuGroupFromBusinessData(businessData, {
|
||||
assetFamilyId: record.assetFamilyId,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
id: `variant:${record.id}`,
|
||||
sortOrder: Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
const uvConfig = readAssetVariantUvConfig(businessData)
|
||||
const assemblyBasis = recoverAssemblyBasis(
|
||||
isRecord(record.assemblyBasis) ? record.assemblyBasis : null,
|
||||
@ -2122,11 +2135,11 @@ function toAssetVariantDTO(record: AssetVariantWithRelations): AssetVariantDTO {
|
||||
assetFamilyId: record.assetFamilyId,
|
||||
assetFamilyCode: record.assetFamily.code,
|
||||
assetFamilyName: record.assetFamily.name,
|
||||
skuGroupId: record.skuGroupId,
|
||||
skuGroupCode: record.skuGroup?.code ?? null,
|
||||
skuGroupName: record.skuGroup?.name ?? null,
|
||||
skuGroupSortOrder: record.skuGroup?.sortOrder ?? null,
|
||||
skuGroupMetadata: isRecord(record.skuGroup?.metadata) ? record.skuGroup.metadata : null,
|
||||
skuGroupId: legacySkuGroup?.id ?? record.skuGroupId ?? null,
|
||||
skuGroupCode: legacySkuGroup?.code ?? record.skuGroup?.code ?? null,
|
||||
skuGroupName: legacySkuGroup?.name ?? record.skuGroup?.name ?? null,
|
||||
skuGroupSortOrder: legacySkuGroup?.sortOrder ?? record.skuGroup?.sortOrder ?? null,
|
||||
skuGroupMetadata: legacySkuGroup?.metadata ?? (isRecord(record.skuGroup?.metadata) ? record.skuGroup.metadata : null),
|
||||
assetType: record.assetType,
|
||||
code: record.code,
|
||||
name: record.name,
|
||||
@ -2409,7 +2422,7 @@ async function ensureAssetSkuGroupForVariant(input: {
|
||||
skuGroupId?: string | null
|
||||
defaultCode: string
|
||||
defaultName: string
|
||||
}): Promise<AssetSkuGroupWithRelations> {
|
||||
}): Promise<LegacyAssetSkuGroupSnapshot> {
|
||||
const requestedId = input.skuGroupId?.trim()
|
||||
if (requestedId) {
|
||||
const group = await assetSkuGroupRepository.findByIdWithRelations(requestedId)
|
||||
@ -2419,26 +2432,42 @@ async function ensureAssetSkuGroupForVariant(input: {
|
||||
if (group.assetFamilyId !== input.assetFamilyId) {
|
||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '货号组必须属于当前型号')
|
||||
}
|
||||
return group
|
||||
return createLegacyAssetSkuGroupSnapshot({
|
||||
assetFamilyId: group.assetFamilyId,
|
||||
id: group.id,
|
||||
code: group.code,
|
||||
name: group.name,
|
||||
description: group.description,
|
||||
sortOrder: group.sortOrder,
|
||||
metadata: group.metadata,
|
||||
})
|
||||
}
|
||||
|
||||
const code = normalizeAssetSkuGroupCode(input.defaultCode)
|
||||
const existing = await assetSkuGroupRepository.findByFamilyAndCode(input.assetFamilyId, code)
|
||||
if (existing) return existing
|
||||
if (existing) {
|
||||
return createLegacyAssetSkuGroupSnapshot({
|
||||
assetFamilyId: existing.assetFamilyId,
|
||||
id: existing.id,
|
||||
code: existing.code,
|
||||
name: existing.name,
|
||||
description: existing.description,
|
||||
sortOrder: existing.sortOrder,
|
||||
metadata: existing.metadata,
|
||||
})
|
||||
}
|
||||
|
||||
const maxSortOrder = await prisma.assetSkuGroup.aggregate({
|
||||
where: { assetFamilyId: input.assetFamilyId },
|
||||
_max: { sortOrder: true },
|
||||
})
|
||||
const existingGroups = await assetSkuGroupRepository.findManyByFamilyIds([input.assetFamilyId])
|
||||
const maxSortOrder = Math.max(0, ...existingGroups.map((group) => group.sortOrder))
|
||||
|
||||
return assetSkuGroupRepository.createWithRelations({
|
||||
assetFamily: { connect: { id: input.assetFamilyId } },
|
||||
return createLegacyAssetSkuGroupSnapshot({
|
||||
assetFamilyId: input.assetFamilyId,
|
||||
code,
|
||||
name: input.defaultName.trim() || code,
|
||||
sortOrder: (maxSortOrder._max.sortOrder ?? 0) + 1,
|
||||
sortOrder: maxSortOrder + 1,
|
||||
metadata: {
|
||||
source: 'variant-default',
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -2452,7 +2481,15 @@ async function ensureVariantBarcodeAvailable(
|
||||
if (
|
||||
existing &&
|
||||
existing.id !== currentId &&
|
||||
(!allowedSkuGroupId || existing.skuGroupId !== allowedSkuGroupId)
|
||||
(
|
||||
!allowedSkuGroupId ||
|
||||
readLegacyAssetSkuGroupFromBusinessData(existing.businessData, {
|
||||
assetFamilyId: existing.assetFamilyId,
|
||||
code: existing.code,
|
||||
name: existing.name,
|
||||
id: `variant:${existing.id}`,
|
||||
})?.id !== allowedSkuGroupId
|
||||
)
|
||||
) {
|
||||
throw new AppError(ErrorCode.VALIDATION_FAILED, '条码已存在')
|
||||
}
|
||||
@ -2912,7 +2949,7 @@ class AssetCenterService {
|
||||
|
||||
private async generateNextVariantModelCodeForSkuGroup(input: {
|
||||
assetFamilyId: string
|
||||
skuGroup: AssetSkuGroupWithRelations
|
||||
skuGroup: Pick<LegacyAssetSkuGroupSnapshot, 'id' | 'code'>
|
||||
currentVariantId?: string | null
|
||||
}): Promise<string> {
|
||||
const variants = await assetVariantRepository.findManyByFamilyIds([input.assetFamilyId])
|
||||
@ -3736,11 +3773,8 @@ class AssetCenterService {
|
||||
})
|
||||
|
||||
const sortOrder = input.sortOrder ?? await (async () => {
|
||||
const maxSortOrder = await prisma.assetSkuGroup.aggregate({
|
||||
where: { assetFamilyId: family.id },
|
||||
_max: { sortOrder: true },
|
||||
})
|
||||
return (maxSortOrder._max.sortOrder ?? 0) + 1
|
||||
const existingGroups = await assetSkuGroupRepository.findManyByFamilyIds([family.id])
|
||||
return Math.max(0, ...existingGroups.map((group) => group.sortOrder)) + 1
|
||||
})()
|
||||
|
||||
const created = await assetSkuGroupRepository.createWithRelations({
|
||||
@ -3806,8 +3840,11 @@ class AssetCenterService {
|
||||
}
|
||||
const activeVariantCount = await prisma.assetVariant.count({
|
||||
where: {
|
||||
skuGroupId: id,
|
||||
deletedAt: null,
|
||||
businessData: {
|
||||
path: ['legacySkuGroup', 'id'],
|
||||
equals: id,
|
||||
} as Prisma.JsonFilter,
|
||||
},
|
||||
})
|
||||
if (activeVariantCount > 0) {
|
||||
@ -4455,7 +4492,7 @@ class AssetCenterService {
|
||||
}
|
||||
|
||||
const requestedCode = input.code?.trim().toUpperCase() ?? ''
|
||||
let skuGroup: AssetSkuGroupWithRelations
|
||||
let skuGroup: LegacyAssetSkuGroupSnapshot
|
||||
let code: string
|
||||
if (family.type === 'PACKAGING') {
|
||||
code = requestedCode || await this.generateNextSkuGroupCodeForFamily(family)
|
||||
@ -4554,11 +4591,15 @@ class AssetCenterService {
|
||||
dimensions: resolvedDimensions,
|
||||
lossFactor: resolvedLossFactor,
|
||||
})
|
||||
const resolvedBusinessData = resolveBusinessDataForVariantCreate({
|
||||
const rawResolvedBusinessData = resolveBusinessDataForVariantCreate({
|
||||
businessData: input.businessData,
|
||||
uvConfig: input.uvConfig,
|
||||
inheritedBusinessData,
|
||||
})
|
||||
const resolvedBusinessData = writeLegacyAssetSkuGroupToBusinessData(
|
||||
rawResolvedBusinessData ?? {},
|
||||
skuGroup,
|
||||
)
|
||||
const resolvedAssemblyBasis = resolveInheritedJsonRecord(
|
||||
input.assemblyBasis,
|
||||
inheritedVariant?.assemblyBasis,
|
||||
@ -4582,7 +4623,6 @@ class AssetCenterService {
|
||||
|
||||
const created = await assetVariantRepository.createWithRelations({
|
||||
assetFamily: { connect: { id: input.assetFamilyId } },
|
||||
skuGroup: { connect: { id: skuGroup.id } },
|
||||
code,
|
||||
name,
|
||||
identityKey,
|
||||
@ -4729,9 +4769,14 @@ class AssetCenterService {
|
||||
input.skuGroupId !== undefined
|
||||
? input.skuGroupId
|
||||
: nextFamilyId === existing.assetFamilyId
|
||||
? existing.skuGroupId
|
||||
? readLegacyAssetSkuGroupFromBusinessData(existing.businessData, {
|
||||
assetFamilyId: existing.assetFamilyId,
|
||||
code: existing.code,
|
||||
name: existing.name,
|
||||
id: `variant:${existing.id}`,
|
||||
})?.id ?? null
|
||||
: null
|
||||
let nextSkuGroup: AssetSkuGroupWithRelations
|
||||
let nextSkuGroup: LegacyAssetSkuGroupSnapshot
|
||||
let nextCode: string
|
||||
|
||||
if (nextFamily.type === 'PACKAGING') {
|
||||
@ -4835,6 +4880,10 @@ class AssetCenterService {
|
||||
uvConfig: input.uvConfig,
|
||||
})
|
||||
: undefined
|
||||
const persistedNextBusinessData = writeLegacyAssetSkuGroupToBusinessData(
|
||||
nextBusinessData ?? existingBusinessData ?? {},
|
||||
nextSkuGroup,
|
||||
)
|
||||
const existingUvConfig = readAssetVariantUvConfig(existingBusinessData)
|
||||
const nextUvConfig =
|
||||
input.uvConfig !== undefined
|
||||
@ -4931,7 +4980,6 @@ class AssetCenterService {
|
||||
input.assetFamilyId === undefined
|
||||
? undefined
|
||||
: { connect: { id: input.assetFamilyId } },
|
||||
skuGroup: { connect: { id: nextSkuGroup.id } },
|
||||
code: nextCode,
|
||||
name: nextName,
|
||||
identityKey,
|
||||
@ -4976,7 +5024,7 @@ class AssetCenterService {
|
||||
namingFields:
|
||||
input.namingFields !== undefined ? toJsonValue(input.namingFields) : undefined,
|
||||
businessData:
|
||||
nextBusinessData !== undefined ? toJsonValue(nextBusinessData) : undefined,
|
||||
toJsonValue(persistedNextBusinessData),
|
||||
weightKg: input.weightKg === undefined ? undefined : input.weightKg,
|
||||
costPrice: input.costPrice === undefined ? undefined : input.costPrice,
|
||||
lossFactor: input.lossFactor === undefined ? undefined : input.lossFactor,
|
||||
|
||||
@ -555,6 +555,17 @@ class DraftGroupService {
|
||||
}
|
||||
}
|
||||
|
||||
async listPreSkusByDraftGroupIds(draftGroupIds: string[]): Promise<DraftGroupPreSkuDTO[]> {
|
||||
const uniqueDraftGroupIds = Array.from(
|
||||
new Set(draftGroupIds.map((id) => id.trim()).filter(Boolean)),
|
||||
)
|
||||
if (uniqueDraftGroupIds.length === 0) return []
|
||||
|
||||
const preSkus = await preSkuRepository.findByDraftGroupIds(uniqueDraftGroupIds)
|
||||
const preSkuDTOs = preSkus.map((item) => toDraftGroupPreSkuDTO(item))
|
||||
return this.enrichSelectedVariantBarcodes(preSkuDTOs)
|
||||
}
|
||||
|
||||
private async enrichSelectedVariantBarcodes(
|
||||
preSkus: DraftGroupPreSkuDTO[],
|
||||
): Promise<DraftGroupPreSkuDTO[]> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user