Files
star-erp/resources/js/Components/ActivityLog/LogTable.tsx
sky121113 d671c08338
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m1s
feat: 實作使用者啟停用功能與安全性強化
- 新增使用者「啟用/停用」狀態切換功能 (含後端 API、權限控管、活動紀錄)
- 強化安全性:隱藏超級管理員角色的可見度與操作權限
- 更新開發規範:加入多租戶資料同步規範於 framework.md
- 前端優化:使用 Switch 元件進行狀態快速切換,調整表格欄位順序
2026-02-03 11:51:46 +08:00

216 lines
9.1 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)
}
export default function LogTable({
activities,
sortField,
sortOrder,
onSort,
onViewDetail,
from = 1
}: LogTableProps) {
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 = ['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">{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">
{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>
);
}