import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { RefreshCw, XCircle, Play, AlertCircle, Clock, CheckCircle2 } from 'lucide-react'; import { api } from '../../lib/api-client'; import { AppShell, PageHeader, PageBody } from '../../layout/AppShell'; import { Badge, Button, Card, Select } from '../../components/ui/primitives'; import { Table, THead, TBody, TR, TH, TD } from '../../components/ui/table'; interface Job { id: number; type: string; status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; priority: number; attempts: number; max_attempts: number; progress: number; actor_email: string | null; error: string | null; created_at: string; started_at: string | null; finished_at: string | null; } interface Stats { pending: number; running: number; completed: number; failed: number; cancelled: number; total: number; } const STATUS_VARIANTS: Record = { pending: 'muted', running: 'info', completed: 'success', failed: 'danger', cancelled: 'warn', }; export function JobsList() { const qc = useQueryClient(); const [statusFilter, setStatusFilter] = useState(''); const [typeFilter, setTypeFilter] = useState(''); const { data: jobsData } = useQuery({ queryKey: ['jobs', statusFilter, typeFilter], queryFn: () => api.get<{ jobs: Job[] }>('/jobs', { status: statusFilter || undefined, type: typeFilter || undefined, limit: 200, }), refetchInterval: 3000, }); const { data: stats } = useQuery({ queryKey: ['jobs-stats'], queryFn: () => api.get('/jobs/stats'), refetchInterval: 3000, }); const retryMut = useMutation({ mutationFn: (id: number) => api.post(`/jobs/${id}/retry`), onSuccess: () => { toast.success('Job opnieuw in wachtrij'); qc.invalidateQueries({ queryKey: ['jobs'] }); }, onError: (e: Error) => toast.error(e.message), }); const cancelMut = useMutation({ mutationFn: (id: number) => api.post(`/jobs/${id}/cancel`), onSuccess: () => { toast.success('Job geannuleerd'); qc.invalidateQueries({ queryKey: ['jobs'] }); }, onError: (e: Error) => toast.error(e.message), }); const jobs = jobsData?.jobs ?? []; return (
} /> } /> } /> } /> } />
Auto-refresh 3s
{jobs.map((j) => { const duration = j.started_at && j.finished_at ? Math.round((new Date(j.finished_at).getTime() - new Date(j.started_at).getTime()) / 1000) : null; return ( ); })}
# Type Status Progress Attempts Duur Aangemaakt
{j.id} {j.type} {j.status} {j.status === 'running' ? (
{j.progress}%
) : ( )}
{j.attempts}/{j.max_attempts} {duration !== null ? `${duration}s` : '—'} {new Date(j.created_at).toLocaleString('nl-NL')} {(j.status === 'failed' || j.status === 'cancelled') && ( )} {(j.status === 'pending' || j.status === 'running') && ( )}
{jobs.length === 0 && (
Geen jobs voor deze filters.
)}
{jobs.some((j) => j.error) && (

Recente fouten

{jobs .filter((j) => j.error) .slice(0, 10) .map((j) => (
#{j.id} {j.type}

{j.error}

))}
)}
); } function StatCard({ label, value, icon }: { label: string; value: number; icon: React.ReactNode }) { return (

{label}

{value}

{icon}
); }