143 lines
6.1 KiB
TypeScript
143 lines
6.1 KiB
TypeScript
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/Components/ui/dialog";
|
||
import { Badge } from "@/Components/ui/badge";
|
||
import { ScrollArea } from "@/Components/ui/scroll-area";
|
||
|
||
interface Activity {
|
||
id: number;
|
||
description: string;
|
||
subject_type: string;
|
||
event: string;
|
||
causer: string;
|
||
created_at: string;
|
||
properties: {
|
||
attributes?: Record<string, any>;
|
||
old?: Record<string, any>;
|
||
};
|
||
}
|
||
|
||
interface Props {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
activity: Activity | null;
|
||
}
|
||
|
||
export default function ActivityDetailDialog({ open, onOpenChange, activity }: Props) {
|
||
if (!activity) return null;
|
||
|
||
const attributes = activity.properties?.attributes || {};
|
||
const old = activity.properties?.old || {};
|
||
|
||
// Get all keys from both attributes and old to ensure we show all changes
|
||
const allKeys = Array.from(new Set([...Object.keys(attributes), ...Object.keys(old)]));
|
||
|
||
// Filter out internal keys often logged but not useful for users
|
||
const filteredKeys = allKeys.filter(key =>
|
||
!['created_at', 'updated_at', 'deleted_at', 'id'].includes(key)
|
||
);
|
||
|
||
const getEventBadgeColor = (event: string) => {
|
||
switch (event) {
|
||
case 'created': return 'bg-green-500';
|
||
case 'updated': return 'bg-blue-500';
|
||
case 'deleted': return 'bg-red-500';
|
||
default: return 'bg-gray-500';
|
||
}
|
||
};
|
||
|
||
const getEventLabel = (event: string) => {
|
||
switch (event) {
|
||
case 'created': return '新增';
|
||
case 'updated': return '更新';
|
||
case 'deleted': return '刪除';
|
||
default: return event;
|
||
}
|
||
};
|
||
|
||
const formatValue = (value: any) => {
|
||
if (value === null || value === undefined) return <span className="text-gray-400">-</span>;
|
||
if (typeof value === 'boolean') return value ? '是' : '否';
|
||
if (typeof value === 'object') return JSON.stringify(value);
|
||
return String(value);
|
||
};
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="max-w-2xl">
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2">
|
||
操作詳情
|
||
<Badge className={getEventBadgeColor(activity.event)}>
|
||
{getEventLabel(activity.event)}
|
||
</Badge>
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
{activity.created_at} 由 {activity.causer} 執行的操作
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="mt-4 space-y-4">
|
||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||
<div>
|
||
<span className="text-gray-500">操作對象:</span>
|
||
<span className="font-medium ml-2">{activity.subject_type}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-gray-500">描述:</span>
|
||
<span className="font-medium ml-2">{activity.description}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{activity.event === 'created' ? (
|
||
<div className="bg-gray-50 p-4 rounded-md text-center text-gray-500 text-sm">
|
||
已新增資料 (初始建立)
|
||
</div>
|
||
) : (
|
||
<div className="border rounded-md">
|
||
<div className="grid grid-cols-3 bg-gray-50 p-2 text-sm font-medium text-gray-500">
|
||
<div>欄位</div>
|
||
<div>異動前</div>
|
||
<div>異動後</div>
|
||
</div>
|
||
<ScrollArea className="h-[300px]">
|
||
{filteredKeys.length > 0 ? (
|
||
<div className="divide-y">
|
||
{filteredKeys.map((key) => {
|
||
const oldValue = old[key];
|
||
const newValue = attributes[key];
|
||
// Ensure we catch changes even if one value is missing/null
|
||
// For deleted events, newValue might be empty, so we just show oldValue
|
||
const isChanged = JSON.stringify(oldValue) !== JSON.stringify(newValue);
|
||
|
||
return (
|
||
<div key={key} className={`grid grid-cols-3 p-2 text-sm ${isChanged ? 'bg-yellow-50/30' : ''}`}>
|
||
<div className="font-medium text-gray-700">{key}</div>
|
||
<div className="text-gray-600 break-words pr-2">
|
||
{formatValue(oldValue)}
|
||
</div>
|
||
<div className="text-gray-900 break-words font-medium">
|
||
{activity.event === 'deleted' ? '-' : formatValue(newValue)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<div className="p-8 text-center text-gray-500 text-sm">
|
||
無詳細異動內容
|
||
</div>
|
||
)}
|
||
</ScrollArea>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|