diff --git a/furniture-3d-system/src/app/intake/sample-progress/SampleProgressClient.tsx b/furniture-3d-system/src/app/intake/sample-progress/SampleProgressClient.tsx index ad62c8a6..9d2186b3 100644 --- a/furniture-3d-system/src/app/intake/sample-progress/SampleProgressClient.tsx +++ b/furniture-3d-system/src/app/intake/sample-progress/SampleProgressClient.tsx @@ -6,6 +6,8 @@ import { AlertTriangle, ArrowLeft, ArrowRight, CalendarClock, Link2, Plus, Searc import { canEditSampleProgress, isSampleProgressAdministrator, + isSampleProgressTerminalStatus, + normalizeSampleProgressStatus, SAMPLE_PROGRESS_STATUS_LABELS, SAMPLE_PROGRESS_STATUSES, } from '@/lib/sample-progress' @@ -27,11 +29,12 @@ const isOverdue = (item: SampleProgressDTO) => sampleProgressSuppliers(item) export default function SampleProgressClient({ initialItems, currentUserName, currentUserRole, currentSampleStageOpeningCodes = [], embedded = false, permissionScope, permissionCanOperate = true, permissionCanEditAllRecords = false }: { initialItems: SampleProgressDTO[]; currentUserName: string; currentUserRole?: string; currentSampleStageOpeningCodes?: string[]; embedded?: boolean; permissionScope?: 'supply-chain'; permissionCanOperate?: boolean; permissionCanEditAllRecords?: boolean }) { const [items, setItems] = useState(initialItems) const [keyword, setKeyword] = useState('') - const [status, setStatus] = useState('') + const [selectedStatuses, setSelectedStatuses] = useState([]) const [overdueOnly, setOverdueOnly] = useState(false) const [page, setPage] = useState(1) const [pageInput, setPageInput] = useState('1') const [editorItem, setEditorItem] = useState(null) + const [viewItem, setViewItem] = useState(null) const [historyItem, setHistoryItem] = useState(null) const [previewImage, setPreviewImage] = useState(null) const columnVisibility = useSampleProgressColumnVisibility(currentUserName) @@ -40,17 +43,22 @@ export default function SampleProgressClient({ initialItems, currentUserName, cu const canDelete = permissionCanOperate && isSampleProgressAdministrator(currentUserRole) const currentStageCodes = useMemo(() => new Set(currentSampleStageOpeningCodes.map((code) => code.trim().toUpperCase())), [currentSampleStageOpeningCodes]) - const currentStageItems = useMemo(() => items.filter((item) => currentStageCodes.has(item.openingCode.trim().toUpperCase())), [currentStageCodes, items]) + const currentStageItems = useMemo(() => items.filter((item) => currentStageCodes.has(item.openingCode.trim().toUpperCase()) && !isSampleProgressTerminalStatus(item.status)), [currentStageCodes, items]) const filtered = useMemo(() => items.filter((item) => { const supplierKeywords = sampleProgressSuppliers(item).flatMap((supplier) => [supplier.supplier, supplier.courierTrackingNumber, supplier.progressDescription]) const haystack = [item.openingCode, item.brand, item.designer, item.purchaseOwner, item.color, item.material, ...supplierKeywords].join(' ').toLowerCase() - const statusMatches = status === 'ALL' || (!status ? currentStageCodes.has(item.openingCode.trim().toUpperCase()) : item.status === status) + const normalizedStatus = normalizeSampleProgressStatus(item.status) + const statusMatches = selectedStatuses.includes('ALL') + ? true + : selectedStatuses.length > 0 + ? selectedStatuses.includes(normalizedStatus) + : currentStageCodes.has(item.openingCode.trim().toUpperCase()) && !isSampleProgressTerminalStatus(normalizedStatus) return (!keyword || haystack.includes(keyword.toLowerCase())) && statusMatches && (!overdueOnly || isOverdue(item)) - }), [currentStageCodes, items, keyword, overdueOnly, status]) + }), [currentStageCodes, items, keyword, overdueOnly, selectedStatuses]) const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)) const rows = filtered.slice((Math.min(page, totalPages) - 1) * PAGE_SIZE, Math.min(page, totalPages) * PAGE_SIZE) const overdueCount = currentStageItems.filter(isOverdue).length - const statusCounts = SAMPLE_PROGRESS_STATUSES.map((value) => ({ value, count: items.filter((item) => item.status === value).length })) + const statusCounts = SAMPLE_PROGRESS_STATUSES.map((value) => ({ value, count: items.filter((item) => normalizeSampleProgressStatus(item.status) === value).length })) const tableMinWidth = columnVisibility.visibleColumns.reduce((total, column) => total + column.minWidth, 0) useEffect(() => { @@ -72,6 +80,24 @@ export default function SampleProgressClient({ initialItems, currentUserName, cu setPageInput(String(nextPage)) } + function toggleStatus(value: string) { + setSelectedStatuses((current) => { + if (value === 'ALL') return current.includes('ALL') ? [] : ['ALL'] + const withoutAll = current.filter((statusValue) => statusValue !== 'ALL') + return withoutAll.includes(value) + ? withoutAll.filter((statusValue) => statusValue !== value) + : [...withoutAll, value] + }) + setPage(1) + } + + function resetFilters() { + setKeyword('') + setSelectedStatuses([]) + setOverdueOnly(false) + setPage(1) + } + async function remove(item: SampleProgressDTO) { if (!window.confirm(`确认删除 ${item.openingCode} 的打样记录?`)) return const response = await fetch(`/api/intake/sample-progress/${item.id}${scopeQuery}`, { method: 'DELETE' }) @@ -92,9 +118,9 @@ export default function SampleProgressClient({ initialItems, currentUserName, cu } label="OA 台账匹配" value={`${currentStageItems.length} / ${currentStageCodes.size}`} tone={currentStageItems.length === currentStageCodes.size ? 'success' : 'danger'} /> -
+
{statusCounts.map(({ value, count }) => ( - @@ -102,17 +128,32 @@ export default function SampleProgressClient({ initialItems, currentUserName, cu
-
- - - {overdueOnly && } - - {embedded && permissionCanOperate && } +
+
+ +
+ 打样状态{selectedStatuses.length > 0 && !selectedStatuses.includes('ALL') ? `(已选 ${selectedStatuses.length})` : ''} +
+ 打样状态筛选 + + {SAMPLE_PROGRESS_STATUSES.map((value) => )} +
+
+ {overdueOnly && } + +
+
+ + {embedded && permissionCanOperate && } +
@@ -122,7 +163,7 @@ export default function SampleProgressClient({ initialItems, currentUserName, cu {columnVisibility.visibleColumns.map((column) => {column.label})} - {rows.map((item) => setEditorItem(item)} onHistory={setHistoryItem} onDelete={(row) => void remove(row)} onPreview={setPreviewImage}/>) } + {rows.map((item) => setEditorItem(item)} onView={setViewItem} onHistory={setHistoryItem} onDelete={(row) => void remove(row)} onPreview={setPreviewImage}/>) } {rows.length === 0 &&
暂无匹配的打样记录
} @@ -151,6 +192,14 @@ export default function SampleProgressClient({ initialItems, currentUserName, cu setEditorItem(null) }} />} + {viewItem && setViewItem(null)} + onSaved={() => setViewItem(null)} + />} {historyItem && setHistoryItem(null)}/>} {previewImage?.imageUrl &&
{ if (event.target === event.currentTarget) setPreviewImage(null) }}>
{previewImage.openingCode}
{previewImage.openingCode}
} diff --git a/furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressColumnSettings.tsx b/furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressColumnSettings.tsx index 25406266..ff7e5ff3 100644 --- a/furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressColumnSettings.tsx +++ b/furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressColumnSettings.tsx @@ -1,23 +1,30 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { Check, LockKeyhole, RotateCcw, Settings2, X } from 'lucide-react' -import { SAMPLE_PROGRESS_COLUMNS, type SampleProgressColumnKey } from '../sampleProgressColumns' +import { Check, EyeOff, GripVertical, LockKeyhole, RotateCcw, Settings2, X } from 'lucide-react' +import { getOrderedSampleProgressColumns, type SampleProgressColumnKey } from '../sampleProgressColumns' type Props = { hiddenCount: number visibleColumnKeys: ReadonlySet + columnOrder: ReadonlyArray onToggleColumn: (key: SampleProgressColumnKey) => void + onMoveColumn: (sourceKey: SampleProgressColumnKey, targetKey: SampleProgressColumnKey) => void onShowAllColumns: () => void + onHideAllColumns: () => void } export default function SampleProgressColumnSettings({ hiddenCount, visibleColumnKeys, + columnOrder, onToggleColumn, + onMoveColumn, onShowAllColumns, + onHideAllColumns, }: Props) { const [open, setOpen] = useState(false) + const [draggingKey, setDraggingKey] = useState(null) const containerRef = useRef(null) useEffect(() => { @@ -55,13 +62,17 @@ export default function SampleProgressColumnSettings({
显示字段
-
设置仅影响当前账号在本浏览器的表格
+
拖动字段可调整顺序,仅影响当前账号在本浏览器的表格
- + @@ -69,16 +80,37 @@ export default function SampleProgressColumnSettings({
- {SAMPLE_PROGRESS_COLUMNS.map((column) => { + {getOrderedSampleProgressColumns(columnOrder).map((column) => { const checked = visibleColumnKeys.has(column.key) - return
-
+ {!readOnly &&
{message}
-
+
} {previewOpen && draft.imageUrl &&
{ if (event.target === event.currentTarget) setPreviewOpen(false) }}>
@@ -235,8 +238,8 @@ export default function SampleProgressEditorDrawer({ item, permissionScope, onCl function EditorSection({ title, children }: { title: string; children: React.ReactNode }) { return

{title}

{children}
} -function TextField({ label, value, onChange }: { label: string; value: string; onChange: (value: string) => void }) { - return +function TextField({ label, value, disabled = false, onChange }: { label: string; value: string; disabled?: boolean; onChange: (value: string) => void }) { + return } function ReadOnlyField({ label, value }: { label: string; value: string }) { return
{label}
{value}
diff --git a/furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressSupplierEditor.tsx b/furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressSupplierEditor.tsx index 9458b76e..c1b594df 100644 --- a/furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressSupplierEditor.tsx +++ b/furniture-3d-system/src/app/intake/sample-progress/components/SampleProgressSupplierEditor.tsx @@ -30,11 +30,12 @@ type Props = { value: SampleProgressSupplierInput dueDateChanged: boolean canRemove: boolean + readOnly?: boolean onChange: (value: SampleProgressSupplierInput) => void onRemove: () => void } -export default function SampleProgressSupplierEditor({ index, value, dueDateChanged, canRemove, onChange, onRemove }: Props) { +export default function SampleProgressSupplierEditor({ index, value, dueDateChanged, canRemove, readOnly = false, onChange, onRemove }: Props) { const setField = (key: K, nextValue: SampleProgressSupplierInput[K]) => { onChange({ ...value, [key]: nextValue }) } @@ -43,31 +44,30 @@ export default function SampleProgressSupplierEditor({ index, value, dueDateChan return

供应商 {String.fromCharCode(65 + index)}

- {canRemove && } + {canRemove && !readOnly && }
- setField('supplier', nextValue)}/> - setField('supplierConfirmedAt', nextValue)}/> - setField('sampleNotifiedAt', nextValue)}/> - - {dueDateChanged && setField('deliveryDateChangeReason', nextValue)}/>} - setField('courierTrackingNumber', nextValue)}/> - - setField('requestedCost', nextValue)}/> - setField('actualCost', nextValue)}/> -