/** * Item Approval History Component * * Displays detailed audit trail of approved items with exact timestamps. * Features filtering, sorting, CSV export for compliance reporting. */ import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { supabase } from '@/integrations/supabase/client'; import { format } from 'date-fns'; import { ExternalLink, Download, Clock, User, FileText } from 'lucide-react'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { handleError } from '@/lib/errorHandler'; import type { EntityType } from '@/types/submissions'; interface ApprovalHistoryItem { item_id: string; submission_id: string; item_type: string; action_type: string; status: string; approved_at: string; approved_entity_id: string; created_at: string; approval_time_seconds: number; submission_type: string; submitter_username: string | null; submitter_display_name: string | null; submitter_avatar_url: string | null; approver_username: string | null; approver_display_name: string | null; approver_avatar_url: string | null; entity_slug: string | null; entity_name: string | null; } interface ItemApprovalHistoryProps { submissionId?: string; dateRange?: { from: Date; to: Date }; itemType?: EntityType; limit?: number; embedded?: boolean; } const getApprovalSpeed = (seconds: number) => { const hours = seconds / 3600; if (hours < 1) return { label: 'Fast', variant: 'default' as const, color: 'text-green-600 dark:text-green-400' }; if (hours < 24) return { label: 'Normal', variant: 'secondary' as const, color: 'text-blue-600 dark:text-blue-400' }; return { label: 'Slow', variant: 'destructive' as const, color: 'text-orange-600 dark:text-orange-400' }; }; const formatDuration = (seconds: number) => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (hours > 48) { const days = Math.floor(hours / 24); return `${days}d ${hours % 24}h`; } if (hours > 0) return `${hours}h ${minutes}m`; return `${minutes}m`; }; const getEntityPath = (itemType: string, slug: string | null) => { if (!slug) return null; switch (itemType) { case 'park': return `/parks/${slug}/`; case 'ride': return `/rides/${slug}`; // Need park slug ideally case 'manufacturer': case 'designer': case 'operator': return `/companies/${slug}/`; case 'ride_model': return `/models/${slug}/`; default: return null; } }; export const ItemApprovalHistory = ({ submissionId, dateRange, itemType, limit = 100, embedded = false }: ItemApprovalHistoryProps) => { const [sortField, setSortField] = useState<'approved_at' | 'approval_time_seconds'>('approved_at'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const { data: history, isLoading, error } = useQuery({ queryKey: ['approval-history', { submissionId, dateRange, itemType, limit }], queryFn: async () => { try { const { data, error } = await supabase.rpc('get_approval_history', { p_item_type: itemType || undefined, p_approver_id: undefined, p_from_date: dateRange?.from?.toISOString() || undefined, p_to_date: dateRange?.to?.toISOString() || undefined, p_limit: limit, p_offset: 0 }); if (error) throw error; // Client-side filter by submission_id if provided let filtered = data as ApprovalHistoryItem[]; if (submissionId) { filtered = filtered.filter(item => item.submission_id === submissionId); } return filtered; } catch (err: unknown) { handleError(err, { action: 'fetch_approval_history' }); throw err; } }, staleTime: 5 * 60 * 1000, // 5 minutes }); const sortedHistory = history ? [...history].sort((a, b) => { const aVal = a[sortField]; const bVal = b[sortField]; const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0; return sortDirection === 'asc' ? comparison : -comparison; }) : []; const handleSort = (field: typeof sortField) => { if (sortField === field) { setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortDirection('desc'); } }; const exportToCSV = () => { if (!history || history.length === 0) return; const headers = [ 'Timestamp', 'Item Type', 'Action', 'Entity Name', 'Submitter', 'Approver', 'Time to Approve (hours)', 'Submission ID', 'Item ID' ]; const rows = history.map(item => [ format(new Date(item.approved_at), 'yyyy-MM-dd HH:mm:ss'), item.item_type, item.action_type, item.entity_name || 'N/A', item.submitter_display_name || item.submitter_username || 'Unknown', item.approver_display_name || item.approver_username || 'Unknown', (item.approval_time_seconds / 3600).toFixed(2), item.submission_id, item.item_id ]); const csv = [headers, ...rows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `approval-history-${format(new Date(), 'yyyy-MM-dd')}.csv`; link.click(); URL.revokeObjectURL(url); }; if (error) { return ( Failed to load approval history. Please try again. ); } const content = ( <> {!embedded && ( Item Approval History Detailed audit trail of approved submissions {sortedHistory.length > 0 && ( Export CSV )} )} {isLoading ? ( {[...Array(5)].map((_, i) => ( ))} ) : sortedHistory.length === 0 ? ( No approval history found ) : ( handleSort('approved_at')} > Approved At {sortField === 'approved_at' && (sortDirection === 'asc' ? '↑' : '↓')} Type Entity Submitter Approver handleSort('approval_time_seconds')} > Time to Approve {sortField === 'approval_time_seconds' && (sortDirection === 'asc' ? '↑' : '↓')} {sortedHistory.map((item) => { const speed = getApprovalSpeed(item.approval_time_seconds); const entityPath = getEntityPath(item.item_type, item.entity_slug); return ( {format(new Date(item.approved_at), 'MMM d, yyyy')} {format(new Date(item.approved_at), 'HH:mm:ss')} {item.item_type} {item.entity_name ? ( {item.entity_name} {entityPath && ( )} ) : ( N/A )} {(item.submitter_display_name || item.submitter_username || 'U')[0].toUpperCase()} {item.submitter_display_name || item.submitter_username || 'Unknown'} {(item.approver_display_name || item.approver_username || 'M')[0].toUpperCase()} {item.approver_display_name || item.approver_username || 'Unknown'} {formatDuration(item.approval_time_seconds)} {speed.label} ); })} )} > ); return embedded ? content : {content}; };
Failed to load approval history. Please try again.
No approval history found