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 ( +
+
+ + Item Approvals +
+ +
+ {sortedItems.map((item) => { + const approvedAt = (item as any).approved_at; + const itemName = getItemName(item); + const actionLabel = getActionLabel(item.action_type); + + return ( +
+ {/* Approval Icon */} +
+ +
+ + {/* Item Info */} +
+
+
+ + {itemName} + + + {actionLabel} + + + {item.item_type} + +
+ + {formatDistanceToNow(new Date(approvedAt), { addSuffix: true })} + +
+ + {/* Reviewer Info */} + {reviewerProfile && ( +
+ + + + + + + + Approved by{' '} + + {reviewerProfile.display_name || reviewerProfile.username} + + +
+ )} +
+
+ ); + })} +
+
+ ); +}); + +ItemLevelApprovalHistory.displayName = 'ItemLevelApprovalHistory'; diff --git a/src/components/moderation/QueueItem.tsx b/src/components/moderation/QueueItem.tsx index ecee6bd6..992e97bc 100644 --- a/src/components/moderation/QueueItem.tsx +++ b/src/components/moderation/QueueItem.tsx @@ -23,6 +23,7 @@ import { QueueItemActions } from './renderers/QueueItemActions'; import { SubmissionMetadataPanel } from './SubmissionMetadataPanel'; import { AuditTrailViewer } from './AuditTrailViewer'; import { RawDataViewer } from './RawDataViewer'; +import { ItemLevelApprovalHistory } from './ItemLevelApprovalHistory'; interface QueueItemProps { item: ModerationItem; @@ -330,6 +331,15 @@ export const QueueItem = memo(({ {item.type === 'content_submission' && (
+ + {/* Item-level approval history */} + {item.submission_items && item.submission_items.length > 0 && ( + + )} +
)}