import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link, useNavigate } from 'react-router-dom'; import toast from 'react-hot-toast'; import { Plus, RefreshCw, Play, Power, AlertTriangle, CheckCircle2, Clock, Pause } from 'lucide-react'; import { api } from '../../lib/api-client'; import { AppShell, PageHeader, PageBody } from '../../layout/AppShell'; import { Button, Badge, Card, Skeleton, EmptyState } from '../../components/ui/primitives'; import { Table, THead, TBody, TR, TH, TD } from '../../components/ui/table'; import type { Feed } from './types'; function formatDate(iso: string | null) { if (!iso) return '—'; const d = new Date(iso); const now = new Date(); const diff = (now.getTime() - d.getTime()) / 1000; if (diff < 60) return 'net'; if (diff < 3600) return `${Math.floor(diff / 60)}m`; if (diff < 86400) return `${Math.floor(diff / 3600)}u`; return `${Math.floor(diff / 86400)}d`; } function statusBadge(feed: Feed) { if (!feed.is_active) return Uit; if (feed.consecutive_failures >= 3) return Fout; if (feed.last_error && feed.consecutive_failures > 0) return Waarschuwing; if (!feed.last_synced_at) return Nieuw; return Actief; } function HealthDot({ feed }: { feed: Feed }) { let color = 'bg-[#666]'; let Icon = Pause; if (feed.is_active) { if (feed.consecutive_failures >= 3) { color = 'bg-[#FF3B3B]'; Icon = AlertTriangle; } else if (feed.consecutive_failures > 0) { color = 'bg-[#FFA500]'; Icon = AlertTriangle; } else if (feed.last_success_at) { color = 'bg-[#00FF6A]'; Icon = CheckCircle2; } else { color = 'bg-[#3B82F6]'; Icon = Clock; } } return (
); } export function FeedsList() { const qc = useQueryClient(); const navigate = useNavigate(); const [syncingAll, setSyncingAll] = useState(false); const { data, isLoading } = useQuery({ queryKey: ['feeds'], queryFn: () => api.get<{ feeds: Feed[] }>('/feeds'), refetchInterval: 10000, }); const syncMut = useMutation({ mutationFn: (id: number) => api.post<{ jobId: number }>(`/feeds/${id}/sync`), onSuccess: (res) => { toast.success(`Sync gestart (job #${res.jobId})`); qc.invalidateQueries({ queryKey: ['feeds'] }); }, onError: (e: Error) => toast.error(e.message), }); const toggleMut = useMutation({ mutationFn: ({ id, is_active }: { id: number; is_active: boolean }) => api.put<{ feed: Feed }>(`/feeds/${id}`, { is_active }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['feeds'] }); toast.success('Feed bijgewerkt'); }, onError: (e: Error) => toast.error(e.message), }); async function syncAll() { setSyncingAll(true); try { const res = await api.post<{ enqueued: number }>('/feeds/sync-all'); toast.success(`${res.enqueued} feeds in de wachtrij`); qc.invalidateQueries({ queryKey: ['feeds'] }); } catch (e) { toast.error(e instanceof Error ? e.message : 'Sync all mislukt'); } finally { setSyncingAll(false); } } const feeds = data?.feeds ?? []; return ( } /> {isLoading ? (
{[0, 1, 2, 3].map((i) => ( ))}
) : feeds.length === 0 ? ( navigate('/cms/feeds/new')}> Nieuwe feed } /> ) : ( {feeds.map((feed) => ( ))}
Status Naam Provider Producten Laatste sync Status Acties
{feed.name} {feed.last_error && feed.consecutive_failures > 0 && (

{feed.last_error}

)}
{feed.provider_name ?? feed.provider_slug ?? '—'} {feed.total_products?.toLocaleString('nl-NL') ?? '0'} {formatDate(feed.last_synced_at)} {statusBadge(feed)}
)}
); }