From 46c08e10e8ec2da21d8a3405adc230fb3fd813d7 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:28:50 +0000 Subject: [PATCH] Add item-level approval history Introduce ItemLevelApprovalHistory component to display which specific submission items were approved, when, and by whom, and integrate it into QueueItem between metadata and audit trail. The component shows item names, approval timestamps, action types, and reviewer info. --- .../moderation/ItemLevelApprovalHistory.tsx | 125 ++++++++++++++++++ src/components/moderation/QueueItem.tsx | 10 ++ 2 files changed, 135 insertions(+) create mode 100644 src/components/moderation/ItemLevelApprovalHistory.tsx diff --git a/src/components/moderation/ItemLevelApprovalHistory.tsx b/src/components/moderation/ItemLevelApprovalHistory.tsx new file mode 100644 index 00000000..4d033cec --- /dev/null +++ b/src/components/moderation/ItemLevelApprovalHistory.tsx @@ -0,0 +1,125 @@ +import { memo } from 'react'; +import { formatDistanceToNow } from 'date-fns'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Badge } from '@/components/ui/badge'; +import { CheckCircle2, User } from 'lucide-react'; +import type { SubmissionItem } from '@/types/moderation'; + +interface ItemLevelApprovalHistoryProps { + items: SubmissionItem[]; + reviewerProfile?: { + user_id: string; + username: string; + display_name?: string | null; + avatar_url?: string | null; + } | null; +} + +export const ItemLevelApprovalHistory = memo(({ + items, + reviewerProfile, +}: ItemLevelApprovalHistoryProps) => { + // Filter to only approved items with timestamps + const approvedItems = items.filter( + item => item.status === 'approved' && (item as any).approved_at + ); + + if (approvedItems.length === 0) { + return null; + } + + // Sort by approval time (newest first) + const sortedItems = [...approvedItems].sort((a, b) => { + const timeA = new Date((a as any).approved_at).getTime(); + const timeB = new Date((b as any).approved_at).getTime(); + return timeB - timeA; + }); + + // Helper to get item display name + const getItemName = (item: SubmissionItem): string => { + const entityData = item.entity_data || item.item_data; + if (entityData && typeof entityData === 'object' && 'name' in entityData) { + return String(entityData.name); + } + return `${item.item_type} #${item.order_index}`; + }; + + // Helper to get action label + const getActionLabel = (actionType: string): string => { + switch (actionType) { + case 'create': return 'Created'; + case 'edit': return 'Edited'; + case 'delete': return 'Deleted'; + default: return 'Modified'; + } + }; + + return ( +