import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/Components/ui/dialog"; import { Badge } from "@/Components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/Components/ui/table"; import { User, Clock, Package, Activity as ActivityIcon } from "lucide-react"; interface Activity { id: number; description: string; subject_type: string; event: string; causer: string; created_at: string; properties: { attributes?: Record; old?: Record; snapshot?: Record; sub_subject?: string; items_diff?: { added: any[]; removed: any[]; updated: any[]; }; }; } interface Props { open: boolean; onOpenChange: (open: boolean) => void; activity: Activity | null; } // 欄位翻譯對照表 const fieldLabels: Record = { id: 'ID', created_at: '建立時間', updated_at: '更新時間', deleted_at: '刪除時間', name: '名稱', code: '商品代號', username: '登入帳號', description: '描述', // ... (保持原有翻譯) price: '價格', cost: '成本', stock: '庫存', category_id: '分類', unit_id: '單位', is_active: '啟用狀態', conversion_rate: '換算率', specification: '規格', brand: '品牌', base_unit_id: '基本單位', large_unit_id: '大單位', purchase_unit_id: '採購單位', email: 'Email', password: '密碼', phone: '電話', address: '地址', role_id: '角色', email_verified_at: '電子郵件驗證時間', remember_token: '登入權杖', barcode: '條碼', external_pos_id: '外部 POS ID', cost_price: '成本價', member_price: '會員價', wholesale_price: '批發價', // 快照欄位 category_name: '分類名稱', base_unit_name: '基本單位名稱', large_unit_name: '大單位名稱', purchase_unit_name: '採購單位名稱', // 廠商欄位 short_name: '簡稱', tax_id: '統編', owner: '負責人', contact_name: '聯絡人', tel: '電話', remark: '備註', license_plate: '車牌號碼', driver_name: '司機姓名', default_transit_warehouse_id: '預設在途倉庫', // 倉庫與庫存欄位 warehouse_name: '倉庫名稱', product_name: '商品名稱', warehouse_id: '倉庫', product_id: '商品', quantity: '數量', safety_stock: '安全庫存', location: '儲位', unit_cost: '單位成本', total_value: '總價值', // 庫存欄位 batch_number: '批號', box_number: '箱號', origin_country: '來源國家', arrival_date: '入庫日期', expiry_date: '有效期限', source_purchase_order_id: '來源採購單', quality_status: '品質狀態', quality_remark: '品質備註', purchase_order_id: '來源採購單', inventory_id: '庫存 ID', balance_before: '異動前餘額', balance_after: '異動後餘額', reference_type: '參考單據類型', reference_id: '參考單據 ID', actual_time: '實際時點', // 採購單欄位 po_number: '採購單號', vendor_id: '廠商', vendor_name: '廠商名稱', user_name: '建單人員', user_id: '建單人員', total_amount: '小計', expected_delivery_date: '預計到貨日', order_date: '下單日期', status: '狀態', tax_amount: '稅額', grand_total: '總計', invoice_number: '發票號碼', invoice_date: '發票日期', invoice_amount: '發票金額', last_price: '供貨價格', // 公共事業費欄位 transaction_date: '費用日期', category: '費用類別', amount: '金額', // 進貨單欄位 gr_number: '進貨單號', received_date: '入庫日期', type: '入庫類型', remarks: '備註', // 生產管理欄位 production_number: '工單編號', production_date: '生產日期', actual_quantity: '實際產量', consumption_status: '物料消耗狀態', recipe_id: '生產配方', recipe_name: '配方名稱', yield_quantity: '預期產量', // 庫存單據通用欄位 doc_no: '單據編號', snapshot_date: '快照日期', completed_at: '完成日期', posted_at: '過帳日期', from_warehouse_id: '來源倉庫', from_warehouse_name: '來源倉庫名稱', to_warehouse_id: '目的地倉庫', to_warehouse_name: '目的地倉庫名稱', reason: '原因', count_doc_id: '盤點單 ID', count_doc_no: '盤點單號', created_by: '建立者', updated_by: '更新者', completed_by: '完成者', posted_by: '過帳者', counted_qty: '盤點數量', adjust_qty: '調整數量', // 調撥單專有欄位 transit_warehouse_id: '在途倉庫', transit_warehouse_name: '在途倉庫名稱', dispatched_at: '出貨日期', dispatched_by: '出貨人', received_at: '收貨日期', received_by: '收貨人', reserved_quantity: '預扣數量', snapshot_quantity: '異動前庫存 (快照)', // 門市叫貨欄位 store_warehouse_id: '申請倉庫', store_warehouse_name: '申請倉庫', supply_warehouse_id: '供貨倉庫', supply_warehouse_name: '供貨倉庫', approved_by: '審核人', approved_user_name: '審核人', approved_at: '審核時間', submitted_at: '提交時間', reject_reason: '駁回原因', status_label: '處理狀態', transfer_order_id: '調撥單 ID', transfer_order_name: '調撥單號', }; // 狀態翻譯對照表 const statusMap: Record = { // 採購單狀態 draft: '草稿', pending: '待審核', approved: '已核准', ordered: '已下單', received: '已收貨', cancelled: '已取消', completed: '已完成', closed: '已結案', partial: '部分收貨', // 庫存單據狀態 counting: '盤點中', posted: '已過帳', no_adjust: '無需盤調', adjusted: '已盤調', // 生產工單狀態 planned: '已計畫', in_progress: '生產中', // 調撥單狀態 voided: '已作廢', dispatched: '已出貨', }; // 主體類型解析 (Model 類名轉中文) const subjectTypeMap: Record = { // 完整路徑映射 'App\\Modules\\Inventory\\Models\\Product': '商品資料', 'App\\Modules\\Inventory\\Models\\Warehouse': '倉庫資料', 'App\\Modules\\Inventory\\Models\\Inventory': '庫存異動', 'App\\Modules\\Inventory\\Models\\Category': '商品分類', 'App\\Modules\\Inventory\\Models\\Unit': '單位', 'App\\Modules\\Inventory\\Models\\InventoryTransaction': '庫存異動紀錄', 'App\\Modules\\Inventory\\Models\\GoodsReceipt': '進貨單', 'App\\Modules\\Inventory\\Models\\InventoryCountDoc': '庫存盤點單', 'App\\Modules\\Inventory\\Models\\InventoryAdjustDoc': '庫存盤調單', 'App\\Modules\\Inventory\\Models\\StoreRequisition': '門市叫貨單', // 簡寫映射 (應對後端回傳 class_basename 的情況) 'Product': '商品資料', 'Warehouse': '倉庫資料', 'Inventory': '庫存異動', 'InventoryTransaction': '庫存異動紀錄', 'Category': '商品分類', 'Unit': '單位', 'Vendor': '廠商資料', 'PurchaseOrder': '採購單', 'GoodsReceipt': '進貨單', 'ProductionOrder': '生產工單', 'Recipe': '生產配方', 'InventoryCountDoc': '庫存盤點單', 'InventoryAdjustDoc': '庫存盤調單', 'InventoryTransferOrder': '庫庫調撥單', 'StoreRequisition': '門市叫貨單', 'StockMovementDoc': '庫存單據', 'User': '使用者帳號', 'Role': '角色權限', 'UtilityFee': '公共事業費', }; // 庫存品質狀態對照表 const qualityStatusMap: Record = { normal: '正常', frozen: '凍結', rejected: '瑕疵/拒收', }; // 入庫類型翻譯對照表 const typeMap: Record = { standard: '採購進貨', miscellaneous: '雜項入庫', other: '其他入庫', }; export default function ActivityDetailDialog({ open, onOpenChange, activity }: Props) { if (!activity) return null; const attributes = activity.properties?.attributes || {}; const old = activity.properties?.old || {}; const snapshot = activity.properties?.snapshot || {}; // 取得屬性和舊值的所有鍵,以確保顯示所有變更 const allKeys = Array.from(new Set([...Object.keys(attributes), ...Object.keys(old)])); // 自訂欄位排序順序 const sortOrder = [ 'doc_no', 'po_number', 'gr_number', 'production_number', 'transfer_order_name', 'vendor_name', 'warehouse_name', 'order_date', 'expected_delivery_date', 'status', 'remark', 'invoice_number', 'invoice_date', 'invoice_amount', 'total_amount', 'tax_amount', 'grand_total' ]; // 過濾掉通常會記錄但對使用者無用的內部鍵,以及已被解析為名稱的原始 ID 欄位 const filteredKeys = allKeys .filter(key => { if (['created_at', 'updated_at', 'deleted_at', 'id', 'remember_token', 'activityProperties'].includes(key)) return false; // 隱藏冗餘的狀態標籤 (因為後端已統一替換 status 內容) if (key === 'status_label') return false; // 隱藏技術用的 ID 欄位 (如果已有對應的名稱欄位) if (key.endsWith('_id')) { const nameKey = key.replace('_id', '_name'); const userNameKey = key.replace('_id', '_user_name'); if (allKeys.includes(nameKey) || allKeys.includes(userNameKey)) return false; } // 特別隱藏調撥單 ID if (key === 'transfer_order_id' && (allKeys.includes('transfer_order_name') || allKeys.includes('transfer_order_doc_no'))) return false; return true; }) .sort((a, b) => { const indexA = sortOrder.indexOf(a); const indexB = sortOrder.indexOf(b); if (indexA !== -1 && indexB !== -1) return indexA - indexB; if (indexA !== -1) return -1; if (indexB !== -1) return 1; return a.localeCompare(b); }); // 檢查鍵是否為快照名稱欄位或輔助名稱欄位的輔助函式 const isSnapshotField = (key: string) => { const snapshotFields = [ 'category_name', 'base_unit_name', 'large_unit_name', 'purchase_unit_name', 'warehouse_name', 'user_name', 'from_warehouse_name', 'to_warehouse_name', 'created_by_name', 'updated_by_name', 'completed_by_name', 'posted_by_name', 'vendor_name', 'product_name', 'recipe_name' ]; if (snapshotFields.includes(key)) return true; if (key.endsWith('_name')) return true; return false; }; const getEventBadgeClass = (event: string) => { switch (event) { case 'created': return 'bg-green-50 text-green-700 border-green-200'; case 'updated': return 'bg-blue-50 text-blue-700 border-blue-200'; case 'deleted': return 'bg-red-50 text-red-700 border-red-200'; default: return 'bg-gray-50 text-gray-700 border-gray-200'; } }; const getEventLabel = (event: string) => { switch (event) { case 'created': return '新增'; case 'updated': return '更新'; case 'deleted': return '刪除'; case 'updated_items': return '異動品項'; default: return event; } }; const formatValue = (key: string, value: any) => { if (key === 'password') return '******'; if (value === null || value === undefined) return '-'; if (typeof value === 'boolean') return value ? '是' : '否'; if (key === 'is_active') return value ? '啟用' : '停用'; if (key === 'status' && typeof value === 'string' && statusMap[value]) { return statusMap[value]; } if (key === 'quality_status' && typeof value === 'string' && qualityStatusMap[value]) { return qualityStatusMap[value]; } if (key === 'type' && typeof value === 'string' && typeMap[value]) { return typeMap[value]; } // 處理日期與時間 if ((key === 'order_date' || key === 'expected_delivery_date' || key === 'invoice_date' || key === 'arrival_date' || key === 'expiry_date' || key === 'received_date' || key === 'production_date') && typeof value === 'string') { return value.split('T')[0].split(' ')[0]; } if ((key === 'snapshot_date' || key === 'completed_at' || key === 'posted_at' || key === 'created_at' || key === 'updated_at' || key === 'actual_time' || key === 'submitted_at' || key === 'approved_at') && typeof value === 'string') { try { // 處理部分 ISO 字串包含 T 的情況 const normalizedValue = value.replace('T', ' '); const date = new Date(normalizedValue); if (isNaN(date.getTime())) return 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); }; const getFormattedValue = (key: string, value: any) => { if (key.endsWith('_id')) { const nameKey = key.replace('_id', '_name'); const nameValue = snapshot[nameKey] || attributes[nameKey]; if (nameValue) return `${nameValue}`; } return formatValue(key, value); }; const getFieldLabel = (key: string) => fieldLabels[key] || key; const getSubjectTypeLabel = (type: string) => subjectTypeMap[type] || type; const getSubjectName = () => { if ((snapshot.warehouse_name || attributes.warehouse_name) && (snapshot.product_name || attributes.product_name)) { return `${snapshot.warehouse_name || attributes.warehouse_name} - ${snapshot.product_name || attributes.product_name}`; } const nameParams = ['doc_no', 'po_number', 'gr_number', 'production_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'title']; for (const param of nameParams) { if (snapshot[param]) return snapshot[param]; if (attributes[param]) return attributes[param]; if (old[param]) return old[param]; } if (attributes.id || old.id) return `#${attributes.id || old.id}`; return ''; }; const subjectName = getSubjectName(); return (
操作詳情 {getEventLabel(activity.event)}
{activity.causer}
{activity.created_at}
{subjectName ? `${subjectName} ` : ''} ({getSubjectTypeLabel(activity.properties?.sub_subject || activity.subject_type)})
{/* 僅在描述與事件名稱不同時顯示(不太可能發生但為了安全起見) */} {activity.description !== getEventLabel(activity.event) && activity.description !== 'created' && activity.description !== 'updated' && (
{activity.description}
)}
{activity.event === 'created' ? (
欄位 異動前 異動後 {filteredKeys .filter(key => attributes[key] !== null && attributes[key] !== '' && !isSnapshotField(key)) .map((key) => ( {getFieldLabel(key)} - {getFormattedValue(key, attributes[key])} ))} {filteredKeys.filter(key => attributes[key] !== null && attributes[key] !== '' && !isSnapshotField(key)).length === 0 && ( 無初始資料 )}
) : (
欄位 異動前 異動後 {filteredKeys.some(key => !isSnapshotField(key)) ? ( filteredKeys .filter(key => { if (isSnapshotField(key)) return false; // 如果是更新事件,僅顯示有變動的欄位 if (activity.event === 'updated') { const oldValue = old[key]; const newValue = attributes[key]; return JSON.stringify(oldValue) !== JSON.stringify(newValue); } return true; }) .map((key) => { const oldValue = old[key]; const newValue = attributes[key]; const isChanged = JSON.stringify(oldValue) !== JSON.stringify(newValue); // 對於刪除事件,我們希望在 "變更前" 欄位顯示當前屬性 const displayBefore = activity.event === 'deleted' ? getFormattedValue(key, newValue || oldValue) : getFormattedValue(key, oldValue); const displayAfter = activity.event === 'deleted' ? '-' : getFormattedValue(key, newValue); return ( {getFieldLabel(key)} {displayBefore} {displayAfter} ); }) ) : ( 無詳細異動內容 )}
)} {/* 項目差異區塊(採購單專用) */} {activity.properties?.items_diff && (

品項異動明細

商品名稱 異動類型 異動詳情 (舊 → 新) {/* 1. 處理物件格式的 items_diff (現有的採購、盤點、調撥初始紀錄格式) */} {!Array.isArray(activity.properties?.items_diff) && ( <> {/* 更新項目 */} {activity.properties?.items_diff?.updated?.map((item: any, idx: number) => { const getQty = (obj: any) => obj?.quantity ?? obj?.quantity_received ?? obj?.requested_qty; const getAmt = (obj: any) => obj?.subtotal ?? obj?.total_amount; const oldQty = getQty(item.old); const newQty = getQty(item.new); const oldAmt = getAmt(item.old); const newAmt = getAmt(item.new); return ( {item.product_name} 更新
{oldQty !== newQty && oldQty !== undefined && (
數量: {oldQty}{newQty}
)} {oldAmt !== newAmt && oldAmt !== undefined && (
金額: {oldAmt}{newAmt}
)} {item.old?.counted_qty !== item.new?.counted_qty && item.old?.counted_qty !== undefined && (
盤點量: {item.old.counted_qty ?? '未盤'}{item.new.counted_qty ?? '未盤'}
)} {item.old?.remark !== item.new?.remark && item.old?.remark !== undefined && (
備註: {item.old.remark || '無'}{item.new.remark || '無'}
)}
); }) || null} {/* 新增項目 */} {activity.properties?.items_diff?.added?.map((item: any, idx: number) => { const qty = item.new?.quantity ?? item.new?.quantity_received ?? item.new?.requested_qty ?? item.quantity; const amt = item.new?.subtotal ?? item.new?.total_amount; const remark = item.new?.remark; return ( {item.product_name} 新增
數量: {qty} {item.unit_name || item.new?.unit_name || ''}
{amt !== undefined &&
金額: {amt}
} {remark &&
備註: {remark}
}
); }) || null} {/* 移除項目 */} {activity.properties?.items_diff?.removed?.map((item: any, idx: number) => { const qty = item.old?.quantity ?? item.old?.quantity_received ?? item.old?.requested_qty ?? item.quantity ?? item.quantity_received; return ( {item.product_name} 移除 原數量: {qty} {item.unit_name || item.old?.unit_name || ''} ); }) || null} )} {/* 2. 處理陣列格式的 items_diff (調撥單過帳/收貨的複合紀錄格式) */} {Array.isArray(activity.properties?.items_diff) && activity.properties.items_diff.map((item: any, idx: number) => ( {item.product_name}
批號: {item.batch_number}
異動
{item.source_warehouse && (
來源 ({item.source_warehouse}): {Number(item.source_before || 0).toFixed(0)} → {Number(item.source_after || 0).toFixed(0)} -{item.quantity}
)} {item.target_warehouse && (
目的 ({item.target_warehouse}): {Number(item.target_before || 0).toFixed(0)} → {Number(item.target_after || 0).toFixed(0)} +{item.quantity}
)}
))}
)}
); }