Files
star-erp/resources/js/Pages/Admin/ActivityLog/ActivityDetailDialog.tsx
sky121113 0d7bb2758d
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 58s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
feat: 實作操作紀錄與商品分類單位異動紀錄 (Operation Logs for System, Products, Categories, Units)
2026-01-16 17:36:37 +08:00

143 lines
6.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 {
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>
);
}