feat(inventory): 重構庫存盤點流程與優化操作日誌

1. 重構盤點流程:實作自動狀態轉換(盤點中/盤點完成)、整合按鈕為「儲存盤點結果」、更名 UI 狀態標籤。
2. 優化操作日誌:
   - 實作全域 ID 轉名稱邏輯(倉庫、使用者)。
   - 合併單次操作的日誌記錄,避免重複產生。
   - 修復日誌產生過程中的 Collection 修改錯誤。
3. 修正 TypeScript lint 錯誤(Index, Show 頁面)。
This commit is contained in:
2026-02-04 15:12:10 +08:00
parent f4f597e96d
commit 702af0a259
9 changed files with 291 additions and 59 deletions

View File

@@ -143,6 +143,10 @@ const fieldLabels: Record<string, string> = {
reason: '原因',
count_doc_id: '盤點單 ID',
count_doc_no: '盤點單號',
created_by: '建立者',
updated_by: '更新者',
completed_by: '完成者',
counted_qty: '盤點數量',
};
// 狀態翻譯對照表
@@ -271,6 +275,25 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
return value.split('T')[0].split(' ')[0];
}
// 處理日期時間欄位 (YYYY-MM-DD HH:mm:ss)
if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at') && typeof value === 'string') {
try {
const date = new Date(value);
return date.toLocaleString('zh-TW', {
timeZone: 'Asia/Taipei',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/\//g, '-');
} catch (e) {
return value;
}
}
return String(value);
};
@@ -301,7 +324,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
return `${wName} - ${pName}`;
}
const nameParams = ['po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title'];
const nameParams = ['doc_no', 'po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title'];
for (const param of nameParams) {
if (snapshot[param]) return snapshot[param];
if (attributes[param]) return attributes[param];
@@ -480,12 +503,18 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P
{item.old.quantity !== item.new.quantity && (
<div>: <span className="text-gray-500 line-through">{item.old.quantity}</span> <span className="text-blue-700 font-bold">{item.new.quantity}</span></div>
)}
{item.old.counted_qty !== item.new.counted_qty && (
<div>: <span className="text-gray-500 line-through">{item.old.counted_qty ?? '未盤'}</span> <span className="text-blue-700 font-bold">{item.new.counted_qty ?? '未盤'}</span></div>
)}
{item.old.unit_name !== item.new.unit_name && (
<div>: <span className="text-gray-500 line-through">{item.old.unit_name || '-'}</span> <span className="text-blue-700 font-bold">{item.new.unit_name || '-'}</span></div>
)}
{item.old.subtotal !== item.new.subtotal && (
<div>: <span className="text-gray-500 line-through">${item.old.subtotal}</span> <span className="text-blue-700 font-bold">${item.new.subtotal}</span></div>
)}
{item.old.notes !== item.new.notes && (
<div>: <span className="text-gray-500 line-through">{item.old.notes || '-'}</span> <span className="text-blue-700 font-bold">{item.new.notes || '-'}</span></div>
)}
</div>
</TableCell>
</TableRow>

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import LogTable, { Activity } from './LogTable';
import ActivityDetailDialog from './ActivityDetailDialog';
import { History } from 'lucide-react';
interface Props {
activities: Activity[];
className?: string;
}
export default function ActivityLog({ activities, className = '' }: Props) {
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(null);
const [isDetailOpen, setIsDetailOpen] = useState(false);
const handleViewDetail = (activity: Activity) => {
setSelectedActivity(activity);
setIsDetailOpen(true);
};
return (
<div className={`space-y-4 ${className}`}>
<div className="flex items-center gap-2">
<History className="h-5 w-5 text-gray-500" />
<h3 className="text-lg font-semibold text-gray-900"></h3>
</div>
<LogTable
activities={activities}
onViewDetail={handleViewDetail}
/>
<ActivityDetailDialog
open={isDetailOpen}
onOpenChange={setIsDetailOpen}
activity={selectedActivity}
/>
</div>
);
}

View File

@@ -63,7 +63,7 @@ export default function LogTable({
// 嘗試在快照、屬性或舊值中尋找名稱
// 優先順序:快照 > 特定名稱欄位 > 通用名稱 > 代碼 > ID
const nameParams = ['po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title', 'category'];
const nameParams = ['doc_no', 'po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title', 'category'];
let subjectName = '';
// 庫存的特殊處理:顯示 "倉庫 - 商品"