import { useState } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { ArrowLeft, Play, Trash2, Save, Plus, X } from 'lucide-react'; import { api } from '../../lib/api-client'; import { AppShell, PageHeader, PageBody } from '../../layout/AppShell'; import { Button, Badge, Card, CardContent, Input, Label, Select, Skeleton, Textarea } from '../../components/ui/primitives'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../components/ui/tabs'; import { Table, THead, TBody, TR, TH, TD } from '../../components/ui/table'; import { useJobPoll } from '../../hooks/useJobPoll'; import type { Feed, SyncLog, FieldMapping } from './types'; const TRANSFORMS = [ '', 'lowercase', 'uppercase', 'trim', 'strip_html', 'strip_whitespace', 'parse_price', 'parse_boolean', ]; const TARGET_FIELDS = [ 'sku', 'name', 'description', 'brand', 'model', 'category', 'gender', 'price', 'sale_price', 'currency', 'in_stock', 'stock_count', 'image_url', 'product_url', 'size', 'color', 'ean', 'gtin', 'mpn', ]; export function FeedDetail() { const { id } = useParams<{ id: string }>(); const feedId = Number(id); const navigate = useNavigate(); const qc = useQueryClient(); const [activeJobId, setActiveJobId] = useState(null); const { data: feedData, isLoading } = useQuery({ queryKey: ['feed', feedId], queryFn: () => api.get<{ feed: Feed }>(`/feeds/${feedId}`), enabled: !!feedId, }); const { job: syncJob } = useJobPoll(activeJobId); const syncMut = useMutation({ mutationFn: () => api.post<{ jobId: number }>(`/feeds/${feedId}/sync`), onSuccess: (res) => { setActiveJobId(res.jobId); toast.success(`Sync gestart (job #${res.jobId})`); }, onError: (e: Error) => toast.error(e.message), }); const deleteMut = useMutation({ mutationFn: () => api.delete(`/feeds/${feedId}`), onSuccess: () => { toast.success('Feed verwijderd'); navigate('/cms/feeds'); }, onError: (e: Error) => toast.error(e.message), }); function confirmDelete() { if (confirm('Weet je zeker dat je deze feed wilt verwijderen? Alle mappings en sync-geschiedenis gaan mee.')) { deleteMut.mutate(); } } if (isLoading) { return ( ); } const feed = feedData?.feed; if (!feed) { return ( ); } return ( } /> {syncJob && (
Sync job #{syncJob.id} {syncJob.status}
{syncJob.log &&

{syncJob.log}

} {syncJob.error &&

{syncJob.error}

} )} Configuratie Field mappings Historie Mapping tester qc.invalidateQueries({ queryKey: ['feed', feedId] })} /> ); } function ConfigTab({ feed, onSaved }: { feed: Feed; onSaved: () => void }) { const [form, setForm] = useState({ name: feed.name, feed_url: feed.feed_url, is_active: feed.is_active, sync_frequency_hours: feed.sync_frequency_hours, }); const saveMut = useMutation({ mutationFn: (body: typeof form) => api.put<{ feed: Feed }>(`/feeds/${feed.id}`, body), onSuccess: () => { toast.success('Opgeslagen'); onSaved(); }, onError: (e: Error) => toast.error(e.message), }); return (
setForm({ ...form, name: e.target.value })} />
setForm({ ...form, sync_frequency_hours: Number(e.target.value) })} />
setForm({ ...form, feed_url: e.target.value })} />

Totaal producten

{feed.total_products?.toLocaleString('nl-NL') ?? 0}

Laatste sync

{feed.last_synced_at ? new Date(feed.last_synced_at).toLocaleString('nl-NL') : '—'}

Laatste succes

{feed.last_success_at ? new Date(feed.last_success_at).toLocaleString('nl-NL') : '—'}

Opeenvolgende fouten

{feed.consecutive_failures}

); } function MappingsTab({ feedId }: { feedId: number }) { const { data, isLoading } = useQuery({ queryKey: ['feed-mappings', feedId], queryFn: () => api.get<{ mappings: FieldMapping[] }>(`/feeds/${feedId}/mappings`), }); const [rows, setRows] = useState(null); const current = rows ?? data?.mappings ?? []; const qc = useQueryClient(); const saveMut = useMutation({ mutationFn: (body: FieldMapping[]) => api.put(`/feeds/${feedId}/mappings`, { mappings: body }), onSuccess: () => { toast.success('Mappings opgeslagen'); setRows(null); qc.invalidateQueries({ queryKey: ['feed-mappings', feedId] }); }, onError: (e: Error) => toast.error(e.message), }); function update(i: number, patch: Partial) { const next = [...current]; next[i] = { ...next[i], ...patch }; setRows(next); } function add() { setRows([ ...current, { id: 0, feed_id: feedId, source_field: '', target_field: 'name', transform: null, default_value: null, is_required: false, }, ]); } function remove(i: number) { const next = [...current]; next.splice(i, 1); setRows(next); } if (isLoading) return ; return (

Koppel velden uit de feed aan onze product-velden. Gebruik transforms om data op te schonen.

{current.length === 0 ? (

Nog geen mappings. Voeg er één toe.

) : (
{current.map((m, i) => (
update(i, { source_field: e.target.value })} /> update(i, { default_value: e.target.value || null })} />
))}
)} {rows !== null && (
)}
); } function HistoryTab({ feedId }: { feedId: number }) { const { data, isLoading } = useQuery({ queryKey: ['feed-logs', feedId], queryFn: () => api.get<{ logs: SyncLog[] }>(`/feeds/${feedId}/logs`), }); if (isLoading) return ; const logs = data?.logs ?? []; return ( {logs.length === 0 ? (

Nog geen sync-geschiedenis.

) : ( {logs.map((l) => ( ))}
Start Status Duur Fetched Nieuw Updated Removed Errors
{new Date(l.started_at).toLocaleString('nl-NL')} {l.status} {l.duration_ms ? `${Math.round(l.duration_ms / 1000)}s` : '—'} {l.products_fetched} {l.products_new} {l.products_updated} {l.products_removed} {l.errors_count > 0 ? ( {l.errors_count} ) : ( 0 )}
)}
); } function TesterTab({ feedId }: { feedId: number }) { const [sample, setSample] = useState(''); const [result, setResult] = useState<{ mapped: Record; normalized: Record; missingFields: string[] } | null>(null); const testMut = useMutation({ mutationFn: (body: { sample: Record }) => api.post<{ mapped: Record; normalized: Record; missingFields: string[] }>(`/feeds/${feedId}/test-mapping`, body), onSuccess: (data) => setResult(data), onError: (e: Error) => toast.error(e.message), }); function run() { try { const parsed = JSON.parse(sample); testMut.mutate({ sample: parsed }); } catch { toast.error('Ongeldige JSON — plak een enkel product-object'); } } return (