diff --git a/furniture-3d-system/prisma/migrations/20260827161500_add_finished_product_progress/migration.sql b/furniture-3d-system/prisma/migrations/20260827161500_add_finished_product_progress/migration.sql new file mode 100644 index 00000000..cbbd7c36 --- /dev/null +++ b/furniture-3d-system/prisma/migrations/20260827161500_add_finished_product_progress/migration.sql @@ -0,0 +1,49 @@ +CREATE TABLE "FinishedProductProgressItem" ( + "id" TEXT NOT NULL, + "contractNumber" TEXT NOT NULL, + "imageUrl" TEXT, + "itemNumber" TEXT NOT NULL DEFAULT '', + "barcode" TEXT NOT NULL, + "productName" TEXT NOT NULL DEFAULT '', + "supplier" TEXT NOT NULL DEFAULT '', + "brand" TEXT NOT NULL DEFAULT '', + "orderedQuantity" INTEGER NOT NULL DEFAULT 0, + "inTransitQuantity" INTEGER NOT NULL DEFAULT 0, + "orderDueDate" DATE, + "currentNode" TEXT NOT NULL DEFAULT '', + "realtimeProgress" TEXT NOT NULL DEFAULT '', + "createdById" TEXT, + "createdByName" TEXT NOT NULL DEFAULT '', + "updatedById" TEXT, + "updatedByName" TEXT NOT NULL DEFAULT '', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "FinishedProductProgressItem_pkey" PRIMARY KEY ("id") +); + +CREATE TABLE "FinishedProductProgressChangeLog" ( + "id" TEXT NOT NULL, + "itemId" TEXT NOT NULL, + "operation" TEXT NOT NULL, + "actorId" TEXT, + "actorName" TEXT NOT NULL, + "changes" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "FinishedProductProgressChangeLog_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "FinishedProductProgressItem_contractNumber_barcode_key" ON "FinishedProductProgressItem"("contractNumber", "barcode"); +CREATE INDEX "FinishedProductProgressItem_itemNumber_idx" ON "FinishedProductProgressItem"("itemNumber"); +CREATE INDEX "FinishedProductProgressItem_supplier_idx" ON "FinishedProductProgressItem"("supplier"); +CREATE INDEX "FinishedProductProgressItem_brand_idx" ON "FinishedProductProgressItem"("brand"); +CREATE INDEX "FinishedProductProgressItem_currentNode_idx" ON "FinishedProductProgressItem"("currentNode"); +CREATE INDEX "FinishedProductProgressItem_orderDueDate_idx" ON "FinishedProductProgressItem"("orderDueDate"); +CREATE INDEX "FinishedProductProgressItem_updatedAt_idx" ON "FinishedProductProgressItem"("updatedAt"); +CREATE INDEX "FinishedProductProgressChangeLog_itemId_createdAt_idx" ON "FinishedProductProgressChangeLog"("itemId", "createdAt"); +CREATE INDEX "FinishedProductProgressChangeLog_createdAt_idx" ON "FinishedProductProgressChangeLog"("createdAt"); + +ALTER TABLE "FinishedProductProgressChangeLog" +ADD CONSTRAINT "FinishedProductProgressChangeLog_itemId_fkey" +FOREIGN KEY ("itemId") REFERENCES "FinishedProductProgressItem"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/furniture-3d-system/prisma/schema.prisma b/furniture-3d-system/prisma/schema.prisma index 5f015bf5..95940311 100644 --- a/furniture-3d-system/prisma/schema.prisma +++ b/furniture-3d-system/prisma/schema.prisma @@ -94,6 +94,51 @@ model SampleProgressChangeLog { @@index([itemId, createdAt]) } +model FinishedProductProgressItem { + id String @id @default(cuid()) + contractNumber String + imageUrl String? @db.Text + itemNumber String @default("") + barcode String + productName String @default("") + supplier String @default("") + brand String @default("") + orderedQuantity Int @default(0) + inTransitQuantity Int @default(0) + orderDueDate DateTime? @db.Date + currentNode String @default("") + realtimeProgress String @default("") @db.Text + createdById String? + createdByName String @default("") + updatedById String? + updatedByName String @default("") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + changeLogs FinishedProductProgressChangeLog[] + + @@unique([contractNumber, barcode]) + @@index([itemNumber]) + @@index([supplier]) + @@index([brand]) + @@index([currentNode]) + @@index([orderDueDate]) + @@index([updatedAt]) +} + +model FinishedProductProgressChangeLog { + id String @id @default(cuid()) + itemId String + item FinishedProductProgressItem @relation(fields: [itemId], references: [id], onDelete: Cascade) + operation String + actorId String? + actorName String + changes Json + createdAt DateTime @default(now()) + + @@index([itemId, createdAt]) + @@index([createdAt]) +} + // ============================================ // 产品分类 // ============================================ @@ -3884,3 +3929,4 @@ model InstallationDemoTool { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + diff --git a/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/[id]/history/route.ts b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/[id]/history/route.ts new file mode 100644 index 00000000..6a059644 --- /dev/null +++ b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/[id]/history/route.ts @@ -0,0 +1,12 @@ +import { NextRequest } from 'next/server' +import { handleRequest, jsonSuccess } from '@/server/core/response' +import { finishedProductProgressService } from '@/server/services/finished-product-progress.service' + +type Context = { params: Promise<{ id: string }> } + +export async function GET(request: NextRequest, context: Context) { + return handleRequest(request, async (ctx) => { + const { id } = await context.params + return jsonSuccess(await finishedProductProgressService.listChangeLogs(id, ctx.user), 200, ctx.requestId) + }, { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/[id]/route.ts b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/[id]/route.ts new file mode 100644 index 00000000..93262a70 --- /dev/null +++ b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/[id]/route.ts @@ -0,0 +1,13 @@ +import { NextRequest } from 'next/server' +import { handleRequest, jsonSuccess } from '@/server/core/response' +import { finishedProductProgressService } from '@/server/services/finished-product-progress.service' + +type Context = { params: Promise<{ id: string }> } + +export async function PATCH(request: NextRequest, context: Context) { + return handleRequest(request, async (ctx) => { + const { id } = await context.params + const body = await request.json().catch(() => null) + return jsonSuccess(await finishedProductProgressService.update(id, body, ctx.user), 200, ctx.requestId) + }, { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/import/route.ts b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/import/route.ts new file mode 100644 index 00000000..75b02491 --- /dev/null +++ b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/import/route.ts @@ -0,0 +1,70 @@ +import { NextRequest, NextResponse } from 'next/server' +import * as XLSX from 'xlsx' +import { + FINISHED_PRODUCT_PROGRESS_IMPORT_HEADERS, + type FinishedProductProgressImportProgress, + type FinishedProductProgressImportStreamEvent, +} from '@/lib/finished-product-progress' +import { ErrorCode, isAppError } from '@/server/core/errors' +import { handleRequest, jsonError } from '@/server/core/response' +import { finishedProductProgressService } from '@/server/services/finished-product-progress.service' + +export async function POST(request: NextRequest) { + return handleRequest(request, async (ctx) => { + const form = await request.formData() + const file = form.get('file') + if (!(file instanceof File)) return jsonError(ErrorCode.VALIDATION_REQUIRED_FIELD, '请选择 Excel 文件', 400, ctx.requestId) + if (file.size > 10 * 1024 * 1024) return jsonError(ErrorCode.VALIDATION_OUT_OF_RANGE, 'Excel 文件不能超过10MB', 400, ctx.requestId) + + const workbook = XLSX.read(Buffer.from(await file.arrayBuffer()), { type: 'buffer', cellDates: true }) + const sheet = workbook.Sheets[workbook.SheetNames[0]] + if (!sheet) return jsonError(ErrorCode.VALIDATION_INVALID_FORMAT, 'Excel 文件没有工作表', 400, ctx.requestId) + const rawRows = XLSX.utils.sheet_to_json>(sheet, { defval: '' }) + const rows = rawRows.map((raw) => Object.fromEntries(Object.entries(raw).flatMap(([header, value]) => { + const key = FINISHED_PRODUCT_PROGRESS_IMPORT_HEADERS[header.trim()] + return key ? [[key, value]] : [] + }))) + if (rows.length === 0) return jsonError(ErrorCode.VALIDATION_REQUIRED_FIELD, '导入文件没有可用数据', 400, ctx.requestId) + if (rows.length > 5_000) return jsonError(ErrorCode.VALIDATION_OUT_OF_RANGE, '单次最多导入5000行', 400, ctx.requestId) + + const encoder = new TextEncoder() + const stream = new ReadableStream({ + start(controller) { + let closed = false + let progress: FinishedProductProgressImportProgress = { processed: 0, total: rows.length, created: 0, updated: 0 } + const send = (event: FinishedProductProgressImportStreamEvent) => { + if (closed) return + try { controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`)) } catch { closed = true } + } + const close = () => { + if (closed) return + closed = true + controller.close() + } + + send({ type: 'progress', progress }) + void finishedProductProgressService.importRows(rows, ctx.user, (next) => { + progress = next + send({ type: 'progress', progress }) + }).then((result) => { + progress = { processed: rows.length, total: rows.length, ...result } + send({ type: 'complete', progress }) + close() + }).catch((error: unknown) => { + console.error(`[${ctx.requestId}] Finished product progress import failed:`, error) + send({ type: 'error', progress, message: isAppError(error) ? error.message : '导入过程中发生错误' }) + close() + }) + }, + }) + + return new NextResponse(stream, { + status: 200, + headers: { + 'Content-Type': 'application/x-ndjson; charset=utf-8', + 'Cache-Control': 'no-cache, no-transform', + 'X-Accel-Buffering': 'no', + }, + }) + }, { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/route.ts b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/route.ts new file mode 100644 index 00000000..d56b593e --- /dev/null +++ b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/route.ts @@ -0,0 +1,36 @@ +import { NextRequest } from 'next/server' +import { z } from 'zod' +import { EMPTY_FINISHED_PRODUCT_PROGRESS_FILTERS } from '@/lib/finished-product-progress' +import { ErrorCode } from '@/server/core/errors' +import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response' +import { finishedProductProgressService } from '@/server/services/finished-product-progress.service' + +const listSchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + pageSize: z.coerce.number().int().min(1).max(200).default(50), + search: z.string().trim().max(200).default(''), + supplier: z.string().trim().max(200).default(''), + brand: z.string().trim().max(200).default(''), + currentNode: z.string().trim().max(200).default(''), + dueDateFrom: z.string().trim().max(20).default(''), + dueDateTo: z.string().trim().max(20).default(''), +}) + +export async function GET(request: NextRequest) { + return handleRequest(request, async (ctx) => { + const parsed = listSchema.safeParse({ + ...EMPTY_FINISHED_PRODUCT_PROGRESS_FILTERS, + ...Object.fromEntries(request.nextUrl.searchParams.entries()), + }) + if (!parsed.success) return jsonError(ErrorCode.VALIDATION_INVALID_FORMAT, '成品进度筛选条件不正确', 400, ctx.requestId) + const { page, pageSize, ...filters } = parsed.data + return jsonSuccess(await finishedProductProgressService.listPage({ page, pageSize, filters, user: ctx.user }), 200, ctx.requestId) + }, { requireAuth: true }) +} + +export async function POST(request: NextRequest) { + return handleRequest(request, async (ctx) => { + const body = await request.json().catch(() => null) + return jsonSuccess(await finishedProductProgressService.create(body, ctx.user), 201, ctx.requestId) + }, { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/template/route.ts b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/template/route.ts new file mode 100644 index 00000000..785c7af3 --- /dev/null +++ b/furniture-3d-system/src/app/api/supply-chain/finished-product-progress/template/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from 'next/server' +import * as XLSX from 'xlsx' +import { FINISHED_PRODUCT_PROGRESS_FIELDS } from '@/lib/finished-product-progress' +import { handleRequest } from '@/server/core/response' +import { finishedProductProgressService } from '@/server/services/finished-product-progress.service' + +export async function GET(request: NextRequest) { + return handleRequest(request, async (ctx) => { + await finishedProductProgressService.requireTemplateAccess(ctx.user) + const headers = FINISHED_PRODUCT_PROGRESS_FIELDS.map((field) => field.label) + const sheet = XLSX.utils.aoa_to_sheet([headers]) + sheet['!cols'] = headers.map((header) => ({ wch: Math.max(14, header.length * 2 + 4) })) + const workbook = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(workbook, sheet, '成品进度导入') + const content = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }) as Buffer + return new NextResponse(new Uint8Array(content), { + status: 200, + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition': "attachment; filename*=UTF-8''%E6%88%90%E5%93%81%E8%BF%9B%E5%BA%A6%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx", + 'Cache-Control': 'no-store', + }, + }) + }, { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/supply-chain/components/supply-chain-tabs.tsx b/furniture-3d-system/src/app/supply-chain/components/supply-chain-tabs.tsx index c0767146..de4ac89a 100644 --- a/furniture-3d-system/src/app/supply-chain/components/supply-chain-tabs.tsx +++ b/furniture-3d-system/src/app/supply-chain/components/supply-chain-tabs.tsx @@ -100,6 +100,7 @@ export const SUPPLY_CHAIN_ROUTE_TITLES: Record = { '/supply-chain/database/store-account-management': '店铺账号管理', '/supply-chain/new-products/unified-table': '新品开发进度总表', '/supply-chain/new-products/sample-progress': '打样进度明细', + '/supply-chain/purchase/finished-product-progress': '成品进度明细', '/supply-chain/purchase/supplier-information': '供应商信息', '/supply-chain/purchase/daily-affairs': '日常事项', '/supply-chain/purchase/price-change': '产品变价', diff --git a/furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.test.ts b/furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.test.ts index 52e7376b..1f2afc8f 100644 --- a/furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.test.ts +++ b/furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.test.ts @@ -27,6 +27,11 @@ describe('supply chain menu', () => { module: '采购管理', route: '/supply-chain/new-products/sample-progress', }), + expect.objectContaining({ + label: '成品进度明细', + module: '采购管理', + route: '/supply-chain/purchase/finished-product-progress', + }), expect.objectContaining({ label: '供应商信息', module: '采购管理', @@ -37,6 +42,7 @@ describe('supply chain menu', () => { expect(items.filter((item) => item.module === '采购管理').map((item) => item.label)).toEqual([ '打样进度明细', + '成品进度明细', '供应商信息', '日常事项', '产品变价', diff --git a/furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.ts b/furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.ts index 5166c943..5d19ebe4 100644 --- a/furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.ts +++ b/furniture-3d-system/src/app/supply-chain/config/supply-chain-menu.ts @@ -68,6 +68,7 @@ export const SUPPLY_CHAIN_MENU: SupplyChainMenuModule[] = [ { items: [ { label: '打样进度明细', route: '/supply-chain/new-products/sample-progress', targetKey: 'newProductSampleProgress' }, + { label: '成品进度明细', route: '/supply-chain/purchase/finished-product-progress', targetKey: 'newProductSampleProgress' }, { label: '供应商信息', route: '/supply-chain/purchase/supplier-information', targetKey: 'purchaseSupplierInformation' }, { label: '日常事项', route: '/supply-chain/purchase/daily-affairs', targetKey: 'purchaseDailyAffairs' }, { label: '产品变价', route: '/supply-chain/purchase/price-change', targetKey: 'purchasePriceChange' }, diff --git a/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/FinishedProductProgressClient.tsx b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/FinishedProductProgressClient.tsx new file mode 100644 index 00000000..a3e63128 --- /dev/null +++ b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/FinishedProductProgressClient.tsx @@ -0,0 +1,98 @@ +'use client' + +import Image from 'next/image' +import { useCallback, useRef, useState } from 'react' +import { FileDown, Plus, RotateCcw, Search, Upload, X } from 'lucide-react' +import { + EMPTY_FINISHED_PRODUCT_PROGRESS_FILTERS, + type FinishedProductProgressChangeLogEntry, + type FinishedProductProgressFilters, + type FinishedProductProgressPage, + type FinishedProductProgressRow, +} from '@/lib/finished-product-progress' +import { FinishedProductColumnSettings } from './components/FinishedProductColumnSettings' +import { FinishedProductEditorDrawer } from './components/FinishedProductEditorDrawer' +import { FinishedProductHistoryDialog } from './components/FinishedProductHistoryDialog' +import { FinishedProductImportProgress } from './components/FinishedProductImportProgress' +import { FinishedProductPagination } from './components/FinishedProductPagination' +import { FinishedProductTable } from './components/FinishedProductTable' +import { finishedProductProgressStyles } from './finishedProductProgressStyles' +import { useFinishedProductProgressColumns } from './useFinishedProductProgressColumns' +import { useFinishedProductImport } from './useFinishedProductImport' + +async function readResponse(response: Response) { + const payload = await response.json().catch(() => null) + if (!response.ok || !payload?.success) throw new Error(payload?.error?.message || '请求失败') + return payload.data +} + +export function FinishedProductProgressClient({ initialData, currentUserName, canEdit, canDownload }: { initialData: FinishedProductProgressPage; currentUserName: string; canEdit: boolean; canDownload: boolean }) { + const [data, setData] = useState(initialData) + const [filters, setFilters] = useState({ ...EMPTY_FINISHED_PRODUCT_PROGRESS_FILTERS }) + const [editor, setEditor] = useState(null) + const [logs, setLogs] = useState(null) + const [preview, setPreview] = useState(null) + const [loading, setLoading] = useState(false) + const [pageInput, setPageInput] = useState(String(initialData.pageInfo.page)) + const fileRef = useRef(null) + const columns = useFinishedProductProgressColumns(currentUserName) + + const load = useCallback(async (nextFilters: FinishedProductProgressFilters, page: number, pageSize: number) => { + setLoading(true) + try { + const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }) + Object.entries(nextFilters).forEach(([key, value]) => { if (value) params.set(key, value) }) + const next = await readResponse(await fetch(`/api/supply-chain/finished-product-progress?${params}`, { cache: 'no-store' })) as FinishedProductProgressPage + setData(next) + setFilters(nextFilters) + setPageInput(String(next.pageInfo.page)) + } catch (error) { window.alert(error instanceof Error ? error.message : '加载失败') } finally { setLoading(false) } + }, []) + + const afterImport = useCallback(() => load(filters, 1, data.pageInfo.pageSize), [data.pageInfo.pageSize, filters, load]) + const importTask = useFinishedProductImport(afterImport) + const reset = () => { + const next = { ...EMPTY_FINISHED_PRODUCT_PROGRESS_FILTERS } + setFilters(next) + void load(next, 1, data.pageInfo.pageSize) + } + const openHistory = async (row: FinishedProductProgressRow) => { + try { setLogs(await readResponse(await fetch(`/api/supply-chain/finished-product-progress/${row.id}/history`, { cache: 'no-store' }))) } catch (error) { window.alert(error instanceof Error ? error.message : '日志加载失败') } + } + const commitPage = () => { + const parsed = Number(pageInput) + if (!Number.isFinite(parsed)) return setPageInput(String(data.pageInfo.page)) + const page = Math.min(Math.max(1, Math.trunc(parsed)), data.pageInfo.totalPages) + setPageInput(String(page)) + if (page !== data.pageInfo.page) void load(filters, page, data.pageInfo.pageSize) + } + + return
+
+

成品进度明细

维护采购成品订单交期、在途数量、当前节点和实时进度

+
+ + {canDownload && } + {canEdit && <> { const file = event.target.files?.[0]; if (file) void importTask.importFile(file); event.currentTarget.value = '' }} />} +
+
+
{ event.preventDefault(); void load(filters, 1, data.pageInfo.pageSize) }}> + setFilters({ ...filters, search: event.target.value })} placeholder="搜索合同号、货号、条码、产品名称或实时进度" /> + + + + setFilters({ ...filters, dueDateFrom: event.target.value })} /> + setFilters({ ...filters, dueDateTo: event.target.value })} /> + +
+
+ void openHistory(row)} onPreview={setPreview} /> + void load(filters, page, data.pageInfo.pageSize)} onPageSize={(pageSize) => void load(filters, 1, pageSize)} /> +
+ {editor && setEditor(null)} onSaved={() => { setEditor(null); void load(filters, data.pageInfo.page, data.pageInfo.pageSize) }} />} + {logs && setLogs(null)} />} + + {preview?.imageUrl &&
{ if (event.target === event.currentTarget) setPreview(null) }}>
{preview.productName
} + +
+} diff --git a/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductColumnSettings.tsx b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductColumnSettings.tsx new file mode 100644 index 00000000..8a4bbefd --- /dev/null +++ b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductColumnSettings.tsx @@ -0,0 +1,39 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { Check, Columns3, RotateCcw, X } from 'lucide-react' +import { FINISHED_PRODUCT_PROGRESS_COLUMNS, type FinishedProductProgressColumnKey } from '../finishedProductProgressColumns' + +export function FinishedProductColumnSettings({ + visibleKeys, + hiddenCount, + onToggle, + onShowAll, +}: { + visibleKeys: ReadonlySet + hiddenCount: number + onToggle: (key: FinishedProductProgressColumnKey) => void + onShowAll: () => void +}) { + const [open, setOpen] = useState(false) + const ref = useRef(null) + useEffect(() => { + if (!open) return + const close = (event: MouseEvent) => { if (!ref.current?.contains(event.target as Node)) setOpen(false) } + document.addEventListener('mousedown', close) + return () => document.removeEventListener('mousedown', close) + }, [open]) + + return
+ + {open &&
+
显示字段
+
+ {FINISHED_PRODUCT_PROGRESS_COLUMNS.map((column) => )} +
+ +
} +
+} diff --git a/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductEditorDrawer.tsx b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductEditorDrawer.tsx new file mode 100644 index 00000000..30cf8ed7 --- /dev/null +++ b/furniture-3d-system/src/app/supply-chain/purchase/finished-product-progress/components/FinishedProductEditorDrawer.tsx @@ -0,0 +1,60 @@ +'use client' + +import { useState } from 'react' +import { Save, X } from 'lucide-react' +import type { FinishedProductProgressInput, FinishedProductProgressRow } from '@/lib/finished-product-progress' + +const EMPTY: FinishedProductProgressInput = { + contractNumber: '', imageUrl: '', itemNumber: '', barcode: '', productName: '', supplier: '', brand: '', orderedQuantity: 0, inTransitQuantity: 0, orderDueDate: null, currentNode: '', realtimeProgress: '', +} + +function toDraft(row: FinishedProductProgressRow | null): FinishedProductProgressInput { + if (!row) return EMPTY + const { id: _id, createdAt: _createdAt, updatedAt: _updatedAt, updatedByName: _updatedByName, ...input } = row + return input +} + +export function FinishedProductEditorDrawer({ item, onClose, onSaved }: { item: FinishedProductProgressRow | null; onClose: () => void; onSaved: (row: FinishedProductProgressRow) => void }) { + const [draft, setDraft] = useState(() => toDraft(item)) + const [saving, setSaving] = useState(false) + const set = (key: K, value: FinishedProductProgressInput[K]) => setDraft((current) => ({ ...current, [key]: value })) + const save = async () => { + setSaving(true) + try { + const response = await fetch(item ? `/api/supply-chain/finished-product-progress/${item.id}` : '/api/supply-chain/finished-product-progress', { method: item ? 'PATCH' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(draft) }) + const payload = await response.json().catch(() => null) + if (!response.ok || !payload?.success) throw new Error(payload?.error?.message || '保存失败') + onSaved(payload.data) + } catch (error) { window.alert(error instanceof Error ? error.message : '保存失败') } finally { setSaving(false) } + } + return
{ if (event.target === event.currentTarget && !saving) onClose() }}> +
+

{item ? '编辑成品进度' : '新增成品进度'}

合同号与条码共同识别一条记录

+
+
+ set('contractNumber', value)} /> + set('barcode', value)} /> + set('itemNumber', value)} /> + set('productName', value)} /> + set('supplier', value)} /> + set('brand', value)} /> + set('orderedQuantity', value)} /> + set('inTransitQuantity', value)} /> + + set('currentNode', value)} /> + +