Files
star-erp/resources/js/Components/ActivityLog/LogTable.tsx
2026-03-02 16:42:12 +08:00

264 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { Badge } from "@/Components/ui/badge";
import { Eye, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
import { Button } from '@/Components/ui/button';
export interface Activity {
id: number;
description: string;
subject_type: string;
event: string;
causer: string;
created_at: string;
properties: any;
}
interface LogTableProps {
activities: Activity[];
sortField?: string;
sortOrder?: 'asc' | 'desc';
onSort?: (field: string) => void;
onViewDetail: (activity: Activity) => void;
from?: number; // 起始索引編號 (paginator.from)
}
// 主體類型解析 (Model 類名轉中文)
const subjectTypeMap: Record<string, string> = {
// 完整路徑映射
'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\\InventoryTransferOrder': '庫存調撥單',
'App\\Modules\\Inventory\\Models\\StoreRequisition': '門市叫貨單',
'App\\Modules\\Inventory\\Models\\StockMovementDoc': '庫存單據',
'App\\Modules\\Procurement\\Models\\Vendor': '廠商資料',
'App\\Modules\\Procurement\\Models\\PurchaseOrder': '採購單',
'App\\Modules\\Production\\Models\\ProductionOrder': '生產工單',
'App\\Modules\\Production\\Models\\Recipe': '生產配方',
'App\\Modules\\Production\\Models\\RecipeItem': '配方品項',
'App\\Modules\\Production\\Models\\ProductionOrderItem': '工單品項',
'App\\Modules\\Finance\\Models\\UtilityFee': '公共事業費',
'App\\Modules\\Core\\Models\\User': '使用者帳號',
'App\\Modules\\Core\\Models\\Role': '角色權限',
// 簡寫映射
'Product': '商品資料',
'Warehouse': '倉庫資料',
'Inventory': '庫存異動',
'InventoryTransaction': '庫存異動紀錄',
'Category': '商品分類',
'Unit': '單位',
'Vendor': '廠商資料',
'PurchaseOrder': '採購單',
'GoodsReceipt': '進貨單',
'ProductionOrder': '生產工單',
'Recipe': '生產配方',
'InventoryCountDoc': '庫存盤點單',
'InventoryAdjustDoc': '庫存盤調單',
'InventoryTransferOrder': '庫存調撥單',
'StoreRequisition': '門市叫貨單',
'StockMovementDoc': '庫存單據',
'User': '使用者帳號',
'Role': '角色權限',
'UtilityFee': '公共事業費',
};
export default function LogTable({
activities,
sortField,
sortOrder,
onSort,
onViewDetail,
from = 1
}: LogTableProps) {
const getSubjectTypeLabel = (type: string) => subjectTypeMap[type] || type;
const getEventBadgeClass = (event: string) => {
switch (event) {
case 'created': return 'bg-green-50 text-green-700 border-green-200 hover:bg-green-100';
case 'updated': return 'bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100';
case 'deleted': return 'bg-red-50 text-red-700 border-red-200 hover:bg-red-100';
default: return 'bg-gray-50 text-gray-700 border-gray-200 hover:bg-gray-100';
}
};
const getEventLabel = (event: string) => {
switch (event) {
case 'created': return '新增';
case 'updated': return '更新';
case 'deleted': return '刪除';
default: return event;
}
};
const getDescription = (activity: Activity) => {
const props = activity.properties || {};
const attrs = props.attributes || {};
const old = props.old || {};
const snapshot = props.snapshot || {};
// 嘗試在快照、屬性或舊值中尋找名稱
// 優先順序:快照 > 特定名稱欄位 > 通用名稱 > 代碼 > ID
const nameParams = ['doc_no', 'po_number', 'name', 'code', 'product_name', 'warehouse_name', 'category_name', 'base_unit_name', 'title', 'category'];
let subjectName = '';
// 庫存的特殊處理:顯示 "倉庫 - 商品"
if ((snapshot.warehouse_name || attrs.warehouse_name) && (snapshot.product_name || attrs.product_name)) {
const wName = snapshot.warehouse_name || attrs.warehouse_name;
const pName = snapshot.product_name || attrs.product_name;
subjectName = `${wName} - ${pName}`;
} else if (old.warehouse_name && old.product_name) {
subjectName = `${old.warehouse_name} - ${old.product_name}`;
} else {
// 預設備案
for (const param of nameParams) {
if (snapshot[param]) {
subjectName = snapshot[param];
break;
}
if (attrs[param]) {
subjectName = attrs[param];
break;
}
if (old[param]) {
subjectName = old[param];
break;
}
}
}
// 如果找不到名稱,嘗試使用 ID如果可能則格式化顯示或者如果與主題類型重複則不顯示
if (!subjectName && (attrs.id || old.id)) {
subjectName = `#${attrs.id || old.id}`;
}
// 組合部分:[操作者] [動作] [名稱] [主題]
// Example: Admin 新增 可樂 商品
// Example: Admin 更新 台北倉 - 可樂 庫存
return (
<span className="flex items-center gap-1.5 flex-wrap">
<span className="font-medium text-gray-900">{activity.causer}</span>
<span className="text-gray-500">{getEventLabel(activity.event)}</span>
{subjectName && (
<span className="font-medium text-primary-600 bg-primary-50 px-1.5 py-0.5 rounded text-xs">
{subjectName}
</span>
)}
{props.sub_subject ? (
<span className="text-gray-700">{props.sub_subject}</span>
) : (
<span className="text-gray-700">{getSubjectTypeLabel(activity.subject_type)}</span>
)}
{/* 如果有原因/來源則顯示(例如:來自補貨) */}
{(attrs._reason || old._reason) && (
<span className="text-gray-500 text-xs">
( {attrs._reason || old._reason})
</span>
)}
</span>
);
};
const SortIcon = ({ field }: { field: string }) => {
if (!onSort) return null;
if (sortField !== field) {
return <ArrowUpDown className="h-4 w-4 text-muted-foreground ml-1" />;
}
if (sortOrder === "asc") {
return <ArrowUp className="h-4 w-4 text-primary-main ml-1" />;
}
return <ArrowDown className="h-4 w-4 text-primary-main ml-1" />;
};
return (
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead className="w-[180px]">
{onSort ? (
<button
onClick={() => onSort('created_at')}
className="flex items-center gap-1 hover:text-gray-900 transition-colors"
>
<SortIcon field="created_at" />
</button>
) : (
"時間"
)}
</TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px] text-center"></TableHead>
<TableHead></TableHead>
<TableHead className="w-[100px] text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{activities.length > 0 ? (
activities.map((activity, index) => (
<TableRow key={activity.id}>
<TableCell className="text-gray-500 font-medium text-center">
{from + index}
</TableCell>
<TableCell className="text-gray-500 font-medium whitespace-nowrap">
{activity.created_at}
</TableCell>
<TableCell>
<span className="font-medium text-gray-900">{activity.causer}</span>
</TableCell>
<TableCell className="min-w-[300px]">
<div className="break-all">
{getDescription(activity)}
</div>
</TableCell>
<TableCell className="text-center">
<Badge variant="outline" className={getEventBadgeClass(activity.event)}>
{getEventLabel(activity.event)}
</Badge>
</TableCell>
<TableCell className="max-w-[200px]">
<Badge variant="outline" className="bg-slate-50 text-slate-600 border-slate-200 break-all whitespace-normal text-left h-auto py-1">
{getSubjectTypeLabel(activity.subject_type)}
</Badge>
</TableCell>
<TableCell className="text-center">
<Button
variant="outline"
size="sm"
onClick={() => onViewDetail(activity)}
className="button-outlined-primary"
title="檢視詳情"
>
<Eye className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-gray-500">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}