'use client'; import { useEffect, useState } from 'react'; import { useParams, useRouter } from 'next/navigation'; import Link from 'next/link'; import { useJobs } from '@/contexts/JobsContext'; import { JobStatus } from '@/components/ScraperTest'; interface LogEntry { timestamp: string; level: string; message: string; source: string; } interface JobLogs { job_id: string; status: string; error_message: string | null; logs: LogEntry[]; log_count: number; } function formatDuration(seconds: number): string { 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`; } function extractBusinessName(job: JobStatus): string { if (job.business_name) return job.business_name; try { const urlObj = new URL(job.url); const query = urlObj.searchParams.get('query'); return query ? decodeURIComponent(query) : 'Unknown Business'; } catch { return 'Unknown Business'; } } export default function JobDetailPage() { const params = useParams(); const router = useRouter(); const { jobs, refreshJobs } = useJobs(); const [job, setJob] = useState(null); const [logs, setLogs] = useState(null); const [isLoadingLogs, setIsLoadingLogs] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const jobId = params.id as string; // Find job from context or fetch it useEffect(() => { const foundJob = jobs.find(j => j.job_id === jobId); if (foundJob) { setJob(foundJob); } else { // Fetch job directly if not in context fetch(`/api/jobs/${jobId}`) .then(res => res.json()) .then(data => setJob(data)) .catch(console.error); } }, [jobId, jobs]); // Fetch logs useEffect(() => { if (!jobId) return; setIsLoadingLogs(true); fetch(`/api/jobs/${jobId}/logs`) .then(res => res.json()) .then(data => setLogs(data)) .catch(console.error) .finally(() => setIsLoadingLogs(false)); }, [jobId]); const handleDelete = async () => { if (!confirm('Are you sure you want to delete this job?')) return; setIsDeleting(true); try { await fetch(`/api/jobs/${jobId}`, { method: 'DELETE' }); await refreshJobs(); router.push('/jobs'); } catch (err) { console.error('Failed to delete job:', err); } finally { setIsDeleting(false); } }; if (!job) { return (
); } const businessName = extractBusinessName(job); const canViewAnalytics = job.reviews_count && job.reviews_count > 0; return (
{/* Breadcrumb */}
Jobs / {jobId.slice(0, 8)}...
{/* Header */}

{businessName}

{job.business_address && (

{job.business_address}

)}
{job.status === 'running' && (
)} {job.status.charAt(0).toUpperCase() + job.status.slice(1)}
{/* Stats Grid */}
{job.reviews_count !== null && (
{job.reviews_count.toLocaleString()}
Reviews
)} {job.scrape_time !== null && (
{formatDuration(job.scrape_time)}
Duration
)} {job.rating_snapshot !== null && (
{job.rating_snapshot.toFixed(1)}
Rating
)}
{new Date(job.created_at).toLocaleDateString()}
{new Date(job.created_at).toLocaleTimeString()}
Created
{/* Actions */}
{canViewAnalytics && ( View Analytics )} Open in Maps
{/* Error Message */} {job.error_message && (

Error

{job.error_message}

)}
{/* Logs Section */}

Logs

{logs && ( {logs.log_count} entries )}
{isLoadingLogs ? (
) : logs && logs.logs.length > 0 ? (
{[...logs.logs] .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) .map((log, idx) => (
{new Date(log.timestamp).toLocaleTimeString()} {' '} [{log.level}] {' '} [{log.source}] {' '} {log.message}
))}
) : (

No logs available

Logs are recorded during scraping

)}
); }