diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx index f326fc94..56b83206 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx @@ -2,7 +2,12 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; -import type { ChangeEvent, ReactNode, RefObject } from "react"; +import type { + ChangeEvent, + PointerEvent as ReactPointerEvent, + ReactNode, + RefObject, +} from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ArrowLeft, Ban, CheckCircle2, Search } from "lucide-react"; import CameraScannerModal from "@/components/mobile/CameraScannerModal"; @@ -170,6 +175,24 @@ import { SUPPLIER_CODE_BY_NAME, SUPPLIER_DIRECTORY } from "./supplierDirectory"; type BasicFieldType = "text" | "textarea" | "select"; type SkuTabKey = "skuInfo" | "fileUpload"; +type SkuColumnWidthKey = + | "model" + | "skuCode" + | "barcode" + | "setCount" + | "specName" + | "size" + | "color" + | "material" + | "versionNo" + | "attributeCode" + | "drawingId" + | "installMethod" + | "executeStandard" + | "supplierName" + | "manufacturerCode" + | "batchNo" + | "dingTalkFlowNo"; type ProductFactoryTabKey = "factoryInfo" | "skuInfo"; type UploadCategoryKey = | "未分类" @@ -188,6 +211,38 @@ type UploadCategoryKey = | "产品核价" | "工厂质检"; +const SKU_COLUMN_DEFAULT_WIDTHS: Record = { + model: 96, + skuCode: 96, + barcode: 150, + setCount: 96, + specName: 120, + size: 96, + color: 96, + material: 136, + versionNo: 56, + attributeCode: 96, + drawingId: 150, + installMethod: 96, + executeStandard: 140, + supplierName: 140, + manufacturerCode: 96, + batchNo: 64, + dingTalkFlowNo: 120, +}; +const SKU_COLUMN_MAX_WIDTH = 520; + +function normalizeSkuColumnWidth( + column: SkuColumnWidthKey, + width: number, +) { + const minWidth = SKU_COLUMN_DEFAULT_WIDTHS[column]; + return Math.min( + SKU_COLUMN_MAX_WIDTH, + Math.max(minWidth, Math.round(width)), + ); +} + type DocumentWorkspaceView = "detail" | "document"; type DocumentWorkspaceCategory = NonIntegratedDocumentWorkspaceCategory; type WorkflowNodeCategory = NonIntegratedWorkflowNodeCategory; @@ -1865,6 +1920,7 @@ function createEmptySkuItem(): NonIntegratedSkuItem { return { id: buildNonIntegratedSkuId(), skuSequence: 1, + batchNo: 1, packageGroupId: "", packageSuffix: "", image: "", @@ -1877,6 +1933,7 @@ function createEmptySkuItem(): NonIntegratedSkuItem { size: "", color: "", material: "", + versionNo: "V01", attributeCode: "", drawingId: "", installMethod: "", @@ -2189,20 +2246,39 @@ function buildAutoSpecName(item: NonIntegratedSkuItem) { return [baseSpecName, `${item.packageSuffix}包`].filter(Boolean).join(" "); } +function normalizeSkuVersionNo(value: string) { + const normalizedValue = value.trim().toUpperCase(); + if (!normalizedValue) return ""; + + const numericMatch = normalizedValue.match(/^V?(\d+)$/); + if (!numericMatch) return normalizedValue; + + const numericValue = Number.parseInt(numericMatch[1], 10); + if (!Number.isFinite(numericValue) || numericValue <= 0) return ""; + + return `V${String(numericValue).padStart(2, "0")}`; +} + function buildAutoDrawingId(item: NonIntegratedSkuItem) { const model = stripNonIntegratedBrandPrefix(item.model); const attributeCode = item.attributeCode.trim().toUpperCase(); + const versionNo = normalizeSkuVersionNo(item.versionNo); if (!model) return ""; - return `${model}${attributeCode}V01`; + return `${model}${attributeCode}${versionNo}`; } -function buildAutoBarcode(skuCode: string, manufacturerCode: string) { +function buildAutoBarcode( + skuCode: string, + manufacturerCode: string, + versionNo = "V01", +) { const normalizedSkuCode = skuCode.trim().toUpperCase(); const normalizedManufacturerCode = manufacturerCode.trim().toUpperCase(); + const normalizedVersionNo = normalizeSkuVersionNo(versionNo); if (!normalizedSkuCode) return ""; - return `${normalizedSkuCode}${normalizedManufacturerCode}V01`; + return `${normalizedSkuCode}${normalizedManufacturerCode}${normalizedVersionNo}`; } function buildAutoBarcodeSkuCandidates(skuCode: string) { @@ -2227,6 +2303,7 @@ function shouldKeepCustomBarcode( currentBarcode: string, skuCode: string, manufacturerCode: string, + versionNo = "V01", ) { const normalizedBarcode = currentBarcode.trim().toUpperCase(); const normalizedManufacturerCode = manufacturerCode.trim().toUpperCase(); @@ -2234,8 +2311,10 @@ function shouldKeepCustomBarcode( const autoBarcodeCandidates = new Set( buildAutoBarcodeSkuCandidates(skuCode).flatMap((candidateSkuCode) => [ - buildAutoBarcode(candidateSkuCode, normalizedManufacturerCode), - buildAutoBarcode(candidateSkuCode, ""), + buildAutoBarcode(candidateSkuCode, normalizedManufacturerCode, versionNo), + buildAutoBarcode(candidateSkuCode, "", versionNo), + `${candidateSkuCode}${normalizedManufacturerCode}`, + candidateSkuCode, ]), ); @@ -2244,9 +2323,25 @@ function shouldKeepCustomBarcode( } const normalizedSkuCode = skuCode.trim().toUpperCase(); - return !new RegExp(`^${normalizedSkuCode}[A-Z0-9]*V01$`).test( - normalizedBarcode, - ); + return !new RegExp( + `^${escapeRegExp(normalizedSkuCode)}[A-Z0-9]*(?:V\\d+)?$`, + ).test(normalizedBarcode); +} + +function shouldKeepCustomDrawingId( + currentDrawingId: string, + item: NonIntegratedSkuItem, +) { + const normalizedDrawingId = currentDrawingId.trim().toUpperCase(); + const model = stripNonIntegratedBrandPrefix(item.model); + const attributeCode = item.attributeCode.trim().toUpperCase(); + if (!normalizedDrawingId || !model) return false; + if (normalizedDrawingId === buildAutoDrawingId(item)) return false; + if (normalizedDrawingId === `${model}${attributeCode}`) return false; + + return !new RegExp( + `^${escapeRegExp(model)}${escapeRegExp(attributeCode)}(?:V\\d+)?$`, + ).test(normalizedDrawingId); } function getFileNameWithoutExtension(fileName: string) { @@ -2264,6 +2359,7 @@ function cloneSkuItemForNextRow( ...item, id: buildNonIntegratedSkuId(), skuSequence: item.skuSequence + 1, + batchNo: item.batchNo, packageGroupId: "", packageSuffix: "", skuCode: "", @@ -2942,8 +3038,12 @@ function isBomBarcodeForSku(barcode: string, sku: NonIntegratedSkuItem) { return true; } + const versionNo = normalizeSkuVersionNo(sku.versionNo); + return buildAutoBarcodeSkuCandidates(sku.skuCode).some((candidateSkuCode) => - new RegExp(`^${escapeRegExp(candidateSkuCode)}[A-Z0-9]*V01$`).test( + new RegExp( + `^${escapeRegExp(candidateSkuCode)}[A-Z0-9]*${escapeRegExp(versionNo)}$`, + ).test( normalizedBarcode, ), ); @@ -3281,21 +3381,35 @@ function deriveSkuItem( ? `${autoSkuCode}${item.packageSuffix}` : autoSkuCode; const autoSpecName = buildAutoSpecName(item); - const autoDrawingId = buildAutoDrawingId(item); - const autoBarcode = buildAutoBarcode(finalSkuCode, item.manufacturerCode); + const versionNo = normalizeSkuVersionNo(item.versionNo); + const normalizedItem = { ...item, versionNo }; + const autoDrawingId = buildAutoDrawingId(normalizedItem); + const autoBarcode = buildAutoBarcode( + finalSkuCode, + item.manufacturerCode, + versionNo, + ); return { ...item, + batchNo: + Number.isFinite(item.batchNo) && item.batchNo > 0 + ? Math.floor(item.batchNo) + : 1, skuCode: finalSkuCode, specName: autoSpecName, barcode: shouldKeepCustomBarcode( item.barcode, finalSkuCode, item.manufacturerCode, + versionNo, ) ? item.barcode.trim() : autoBarcode, - drawingId: item.drawingId.trim() || autoDrawingId, + versionNo, + drawingId: shouldKeepCustomDrawingId(item.drawingId, normalizedItem) + ? item.drawingId.trim() + : autoDrawingId, installMethod: item.installMethod.trim(), executeStandard: item.executeStandard.trim(), manufacturer: item.manufacturer.trim(), @@ -3314,44 +3428,64 @@ function normalizeFactorySupplier(value: string) { }; } +function resolveAutoAwareBarcode( + currentBarcode: string | undefined, + fallbackBarcode: string, + skuCode: string, + manufacturerCode: string, + versionNo = "V01", +) { + const normalizedCurrentBarcode = currentBarcode?.trim() ?? ""; + + if ( + normalizedCurrentBarcode && + shouldKeepCustomBarcode( + normalizedCurrentBarcode, + skuCode, + manufacturerCode, + versionNo, + ) + ) { + return normalizedCurrentBarcode; + } + + return fallbackBarcode; +} + function createProductFactoryRowFromSku( sku: NonIntegratedSkuItem, existingRow?: NonIntegratedFactoryRow, ): NonIntegratedFactoryRow { const confirmed = existingRow?.confirmed ?? false; - if (confirmed && existingRow) { - return { - ...existingRow, - skuId: sku.id, - }; - } - - const supplier = normalizeFactorySupplier( - existingRow?.supplierName || sku.manufacturer, - ); + const savedSupplierName = existingRow?.supplierName?.trim() ?? ""; + const supplierSource = confirmed + ? sku.manufacturer || savedSupplierName + : savedSupplierName || sku.manufacturer; + const supplier = normalizeFactorySupplier(supplierSource); const supplierCode = - existingRow?.supplierCode?.trim().toUpperCase() || - supplier.code || - sku.manufacturerCode.trim().toUpperCase(); + confirmed + ? sku.manufacturerCode.trim().toUpperCase() || supplier.code + : existingRow?.supplierCode?.trim().toUpperCase() || + supplier.code || + sku.manufacturerCode.trim().toUpperCase(); const supplierName = - existingRow?.supplierName?.trim() || supplier.name || sku.manufacturer; - const skuCode = existingRow?.skuCode?.trim() || sku.skuCode.trim(); + supplierSource.trim() || supplier.name || sku.manufacturer; + const skuCode = sku.skuCode.trim(); + const fallbackBarcode = + sku.barcode.trim() || buildAutoBarcode(skuCode, supplierCode, sku.versionNo); return { skuId: sku.id, - confirmed: false, - model: existingRow?.model?.trim() || sku.model.trim(), + confirmed, + model: sku.model.trim(), skuCode, - setCount: existingRow?.setCount?.trim() || sku.setCount.trim() || "1", - barcode: - existingRow?.barcode?.trim() || - sku.barcode.trim() || - buildAutoBarcode(skuCode, supplierCode), - specName: existingRow?.specName?.trim() || sku.specName.trim(), - size: existingRow?.size?.trim() || sku.size.trim(), - color: existingRow?.color?.trim() || sku.color.trim(), - material: existingRow?.material?.trim() || sku.material.trim(), - installMethod: existingRow?.installMethod?.trim() || sku.installMethod, + setCount: sku.setCount.trim() || "1", + barcode: fallbackBarcode, + specName: sku.specName.trim(), + size: sku.size.trim(), + color: sku.color.trim(), + material: sku.material.trim(), + installMethod: sku.installMethod, supplierName, supplierCode, warehouse: existingRow?.warehouse?.trim() || "", @@ -3362,8 +3496,7 @@ function createProductFactoryRowFromSku( installationService: existingRow?.installationService?.trim() || "", supplierCategory: existingRow?.supplierCategory?.trim() || "", purchaseOrderMethod: existingRow?.purchaseOrderMethod?.trim() || "", - executeStandard: - existingRow?.executeStandard?.trim() || sku.executeStandard.trim(), + executeStandard: sku.executeStandard.trim(), }; } @@ -3427,17 +3560,15 @@ type ProductPricingDisplayRow = NonIntegratedPricingRow & { function createProductPricingRowFromSku( sku: NonIntegratedSkuItem, existingRow?: NonIntegratedPricingRow, - factoryRow?: NonIntegratedFactoryRow, ): ProductPricingDisplayRow { return { skuId: sku.id, model: sku.model.trim(), skuCode: sku.skuCode.trim(), - barcode: factoryRow?.barcode.trim() || sku.barcode.trim(), + barcode: sku.barcode.trim(), setCount: sku.setCount.trim() || "1", specName: sku.specName.trim(), - supplierName: - factoryRow?.supplierName.trim() || sku.manufacturer.trim() || "-", + supplierName: sku.manufacturer.trim() || "-", costPrice: existingRow?.costPrice?.trim() ?? "", productTag: existingRow?.productTag?.trim() ?? "", }; @@ -3446,16 +3577,13 @@ function createProductPricingRowFromSku( function buildProductPricingRows( skuItems: NonIntegratedSkuItem[], savedRows: NonIntegratedPricingRow[], - factoryRows: NonIntegratedFactoryRow[], ): ProductPricingDisplayRow[] { const pricingRowBySkuId = new Map(savedRows.map((row) => [row.skuId, row])); - const factoryRowBySkuId = new Map(factoryRows.map((row) => [row.skuId, row])); return skuItems.map((sku) => createProductPricingRowFromSku( sku, pricingRowBySkuId.get(sku.id), - factoryRowBySkuId.get(sku.id), ), ); } @@ -5379,6 +5507,59 @@ function buildRawMaterialProcessConfigHref(materialName: string) { return `/intake?${params.toString()}`; } +function SkuColumnResizableFrame({ + columnWidth, + minWidth, + onColumnResize, + className = "", + children, +}: { + columnWidth: number; + minWidth: number; + onColumnResize?: (width: number) => void; + className?: string; + children: ReactNode; +}) { + function handlePointerDown(event: ReactPointerEvent) { + if (!onColumnResize) return; + event.preventDefault(); + event.stopPropagation(); + + const startX = event.clientX; + const startWidth = columnWidth; + const handlePointerMove = (moveEvent: globalThis.PointerEvent) => { + onColumnResize(startWidth + moveEvent.clientX - startX); + }; + const handlePointerUp = () => { + window.removeEventListener("pointermove", handlePointerMove); + window.removeEventListener("pointerup", handlePointerUp); + }; + + window.addEventListener("pointermove", handlePointerMove); + window.addEventListener("pointerup", handlePointerUp, { once: true }); + } + + return ( +
+ {children} + {onColumnResize ? ( +
+ +
+ ) : null} +
+ ); +} + function SkuRowField({ value, placeholder, @@ -5386,8 +5567,11 @@ function SkuRowField({ onBlur, readOnly = false, mode = "textarea", + width = "default", + columnWidth, tone = "default", testId, + onColumnResize, }: { value: string | number; placeholder?: string; @@ -5395,21 +5579,35 @@ function SkuRowField({ onBlur?: () => void; readOnly?: boolean; mode?: "textarea" | "singleLineResizable"; + width?: "default" | "compact"; + columnWidth?: number; tone?: "default" | "manual"; testId?: string; + onColumnResize?: (width: number) => void; }) { const editableBackgroundClassName = tone === "manual" ? "bg-[#eef1f4] placeholder:text-apple-label-tertiary" : "bg-apple-bg-secondary"; - const baseClassName = `w-full min-w-[96px] rounded border border-apple-separator px-2 py-1 text-xs text-apple-label-primary outline-none ${ + const widthClassName = + columnWidth === undefined + ? width === "compact" + ? "w-14 min-w-[56px]" + : "w-full min-w-[96px]" + : ""; + const baseClassName = `${widthClassName} rounded border border-apple-separator px-2 py-1 text-xs text-apple-label-primary outline-none ${ readOnly ? "bg-apple-bg-primary text-apple-label-secondary" : editableBackgroundClassName }`; + const resizeClassName = columnWidth === undefined ? "resize-x" : "resize-none"; + const fieldClassName = + columnWidth === undefined ? "" : "h-full w-full min-w-0"; + const fieldStyle = + columnWidth === undefined ? undefined : { width: "100%", minWidth: 0 }; - if (mode === "singleLineResizable") { - return ( + const field = + mode === "singleLineResizable" ? (