'use client'; import React, { useState, useMemo } from 'react'; import Link from 'next/link'; import { useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel, getPaginationRowModel, ColumnDef, flexRender, SortingState, ColumnFiltersState, } from '@tanstack/react-table'; import { ExecutionStatus } from '@/lib/pipeline-types'; import { JobStatus } from './ScraperTest'; interface ReportsViewProps { executions: ExecutionStatus[]; jobsMap: Map; onRefresh?: () => void; } // Helper to format duration function formatDuration(ms: number | null | undefined): string { if (!ms) return '-'; if (ms < 1000) return `${ms}ms`; const seconds = ms / 1000; if (seconds < 60) return `${seconds.toFixed(1)}s`; const mins = Math.floor(seconds / 60); const secs = Math.round(seconds % 60); return `${mins}m ${secs}s`; } // Sort icon component function SortIcon({ sorted }: { sorted: false | 'asc' | 'desc' }) { if (!sorted) { return ( ); } return ( ); } // Extract business name from execution using jobs map function extractBusinessName(execution: ExecutionStatus, jobsMap: Map): string { // First try to get from the linked job if (execution.job_id) { const job = jobsMap.get(execution.job_id); if (job?.business_name) return job.business_name; } // Try to get from result_summary if (execution.result_summary) { const summary = execution.result_summary as Record; if (summary.business_name) return String(summary.business_name); } // Try to get from input_summary if (execution.input_summary) { const summary = execution.input_summary as Record; if (summary.business_name) return String(summary.business_name); } // Fallback to business_id if (execution.business_id) return execution.business_id; return 'Unknown Business'; } // Extract review count from execution using jobs map function extractReviewCount(execution: ExecutionStatus, jobsMap: Map): number | null { // First try to get from the linked job if (execution.job_id) { const job = jobsMap.get(execution.job_id); if (job?.reviews_count) return job.reviews_count; } if (execution.result_summary) { const summary = execution.result_summary as Record; if (typeof summary.review_count === 'number') return summary.review_count; if (typeof summary.reviews_processed === 'number') return summary.reviews_processed; } if (execution.input_summary) { const summary = execution.input_summary as Record; if (typeof summary.review_count === 'number') return summary.review_count; } return null; } export default function ReportsView({ executions, jobsMap, onRefresh }: ReportsViewProps) { const [sorting, setSorting] = useState([{ id: 'created_at', desc: true }]); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); // Filter executions by status const filteredExecutions = useMemo(() => { if (statusFilter === 'all') return executions; return executions.filter(e => e.status === statusFilter); }, [executions, statusFilter]); // Status counts for filter badges const statusCounts = useMemo(() => { const counts: Record = { all: executions.length }; executions.forEach(e => { counts[e.status] = (counts[e.status] || 0) + 1; }); return counts; }, [executions]); // Calculate summary stats const stats = useMemo(() => { const completed = executions.filter(e => e.status === 'completed'); const failed = executions.filter(e => e.status === 'failed'); const running = executions.filter(e => e.status === 'running'); const totalReviews = completed.reduce((sum, e) => { const count = extractReviewCount(e, jobsMap); return sum + (count || 0); }, 0); const avgDuration = completed.length > 0 ? completed.reduce((sum, e) => sum + (e.total_duration_ms || 0), 0) / completed.length : 0; const successRate = executions.length > 0 ? (completed.length / executions.length) * 100 : 0; // Reports today const today = new Date(); today.setHours(0, 0, 0, 0); const reportsToday = executions.filter(e => new Date(e.created_at || '') >= today).length; return { total: executions.length, completed: completed.length, failed: failed.length, running: running.length, totalReviews, avgDuration, successRate, reportsToday, }; }, [executions, jobsMap]); const columns = useMemo[]>( () => [ { accessorKey: 'business', header: ({ column }) => ( ), accessorFn: (row) => extractBusinessName(row, jobsMap), cell: ({ row }) => { const name = extractBusinessName(row.original, jobsMap); const reviewCount = extractReviewCount(row.original, jobsMap); return (
{name}
{reviewCount !== null && (
{reviewCount.toLocaleString()} reviews analyzed
)}
); }, }, { accessorKey: 'job_id', header: 'Job', cell: ({ row }) => { const jobId = row.original.job_id; if (!jobId) return -; return ( e.stopPropagation()} > {jobId.slice(0, 8)}... ); }, }, { accessorKey: 'status', header: ({ column }) => ( ), cell: ({ row }) => { const status = row.original.status; return ( {status === 'running' && (
)} {status.charAt(0).toUpperCase() + status.slice(1)} ); }, }, { accessorKey: 'stages_completed', header: 'Stages', cell: ({ row }) => { const requested = row.original.stages_requested || []; const completed = row.original.stages_completed || []; return (
{requested.map((stage) => ( {stage.slice(0, 3)} ))}
); }, }, { accessorKey: 'total_duration_ms', header: ({ column }) => ( ), cell: ({ row }) => ( {formatDuration(row.original.total_duration_ms)} ), }, { accessorKey: 'created_at', header: ({ column }) => ( ), cell: ({ row }) => { const dateStr = row.original.created_at; if (!dateStr) return -; const date = new Date(dateStr); const isToday = new Date().toDateString() === date.toDateString(); return (
{isToday ? 'Today' : date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
{date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
); }, sortingFn: (rowA, rowB) => { const dateA = rowA.original.created_at ? new Date(rowA.original.created_at).getTime() : 0; const dateB = rowB.original.created_at ? new Date(rowB.original.created_at).getTime() : 0; return dateA - dateB; }, }, { id: 'actions', header: 'Actions', cell: ({ row }) => { const execution = row.original; const isCompleted = execution.status === 'completed'; const hasJobId = !!execution.job_id; return (
{/* View Report - only for completed with job_id */} {isCompleted && hasJobId && ( e.stopPropagation()} title="View Report" > View Report )} {/* Metrics button */} e.stopPropagation()} title="View Execution Details" > Details
); }, }, ], [jobsMap] ); const table = useReactTable({ data: filteredExecutions, columns, state: { sorting, columnFilters, globalFilter, }, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onGlobalFilterChange: setGlobalFilter, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 10, }, }, }); return (
{/* Summary Stats */}
Total Reports
{stats.total}
Success Rate
{stats.successRate.toFixed(0)}%
Reviews Analyzed
{stats.totalReviews.toLocaleString()}
Today
{stats.reportsToday}
{/* Filters */}
{['all', 'completed', 'running', 'failed', 'pending'].map((status) => ( ))}
{onRefresh && ( )}
{/* Table */}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( ))} ))} {table.getRowModel().rows.length === 0 ? ( ) : ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( ))} )) )}
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
No reports found Run a pipeline to generate reports
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{/* Pagination */}
Showing {table.getRowModel().rows.length} of {filteredExecutions.length} reports
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
); }