import { useState, useEffect, useRef, useCallback } from "react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { ChatPanel } from "@/components/chat" interface PermitSummary { id: number; permit_number: string; applicant_name: string permit_type: string; location: string; source_file: string source_system: string; status: string; archive_status: string issue_date: string; upload_date: string } interface PermitDetail extends PermitSummary { permit_holder_name: string; issuer_name: string expiry_date: string; applicable_law: string work_type: string; water_type: string; embankment_type: string archive_nomination: string; retention_years: number | null extracted_text: string | null; error_message: string | null } // Chat is handled by the template's ChatPanel component const STATUS = { processing: { label: "Verwerken...", cls: "text-blue-600 bg-blue-50 border-blue-200" }, processed: { label: "Verwerkt", cls: "text-green-700 bg-green-50 border-green-200" }, error: { label: "Fout", cls: "text-red-700 bg-red-50 border-red-200" }, } as Record const ARCHIVE_CLS: Record = { "te vernietigen": "text-red-700 bg-red-50", "te bewaren (oneindig)": "text-green-700 bg-green-50", } function App() { const [tab, setTab] = useState<"search" | "upload" | "chat">("search") const [permits, setPermits] = useState([]) const [query, setQuery] = useState("") const [detail, setDetail] = useState(null) const [loading, setLoading] = useState(true) // Upload const [file, setFile] = useState(null) const [permitNum, setPermitNum] = useState("") const [applicant, setApplicant] = useState("") const [uploading, setUploading] = useState(false) const [uploadErr, setUploadErr] = useState(null) const [dragOver, setDragOver] = useState(false) const fileRef = useRef(null) // Chat // Chat state managed by ChatPanel component const load = useCallback(async (q = "") => { try { const url = q ? `/api/permits/search?q=${encodeURIComponent(q)}` : "/api/permits" const r = await fetch(url); if (r.ok) setPermits(await r.json()) } finally { setLoading(false) } }, []) useEffect(() => { load() }, [load]) useEffect(() => { if (!permits.some(p => p.status === "processing")) return const id = setInterval(() => load(query), 3000); return () => clearInterval(id) }, [permits, query, load]) // Chat scroll handled by ChatPanel const upload = async () => { if (!file) return; setUploading(true); setUploadErr(null) try { const fd = new FormData(); fd.append("file", file) if (permitNum) fd.append("permit_number", permitNum) if (applicant) fd.append("applicant_name", applicant) const r = await fetch("/api/permits/upload", { method: "POST", body: fd }) const d = await r.json() if (!r.ok) { setUploadErr(d.error); return } setFile(null); setPermitNum(""); setApplicant("") if (fileRef.current) fileRef.current.value = "" load(query); setTab("search") } catch { setUploadErr("Verbindingsfout") } finally { setUploading(false) } } // Chat send handled by ChatPanel → app/agent.py const openDetail = async (id: number) => { const r = await fetch(`/api/permits/${id}`); if (r.ok) setDetail(await r.json()) } const del = async (id: number) => { if (!confirm("Verwijderen?")) return await fetch(`/api/permits/${id}`, { method: "DELETE" }) setDetail(null); load(query) } const fmt = (iso: string | null) => { if (!iso) return "—" try { return new Date(iso).toLocaleDateString("nl-NL", { dateStyle: "long" }) } catch { return iso } } const fmtFull = (iso: string | null) => { if (!iso) return "—" try { return new Date(iso).toLocaleString("nl-NL", { dateStyle: "long", timeStyle: "short" }) } catch { return iso } } const MetaRow = ({ label, value }: { label: string; value: string | null | undefined }) => value ?

{label}

{value}

: null return (

Vergunningzoeker

{(["search", "upload", "chat"] as const).map(t => ( ))}
{/* ---- SEARCH TAB ---- */} {tab === "search" && <>
setQuery(e.target.value)} onKeyDown={e => e.key === "Enter" && load(query)} placeholder="Zoek op nummer, aanvrager, locatie, type, wet..." /> {query && }
{loading ?

Laden...

: permits.length === 0 ?

{query ? "Geen resultaten." : "Nog geen vergunningen."}

:
{permits.map(p => { const s = STATUS[p.status] || STATUS.processed const a = p.archive_status ? (ARCHIVE_CLS[p.archive_status] || "text-amber-700 bg-amber-50") : "" return (
openDetail(p.id)}>
{p.permit_number || p.source_file} {s.label} {p.archive_status && {p.archive_status}}
{p.applicant_name && {p.applicant_name}} {p.location && {p.location}} {p.permit_type && p.permit_type !== "onbekend" && {p.permit_type.replace(/_/g, " ")}}
{p.issue_date &&
{fmt(p.issue_date)}
}
{p.source_system}
) })}
}
} {/* ---- UPLOAD TAB ---- */} {tab === "upload" && Document uploaden Upload een scan, foto of PDF. Het systeem extraheert automatisch metadata (OCR), classificeert het type en berekent de archiefstatus.
{ e.preventDefault(); setDragOver(true) }} onDragLeave={() => setDragOver(false)} onDrop={e => { e.preventDefault(); setDragOver(false); e.dataTransfer.files?.[0] && setFile(e.dataTransfer.files[0]) }} onClick={() => !uploading && fileRef.current?.click()}> e.target.files?.[0] && setFile(e.target.files[0])} /> {file ?

{file.name}

{(file.size/1024).toFixed(0)} KB

:

Sleep een bestand hierheen of klik

JPG, PNG of PDF — max 25 MB

}
setPermitNum(e.target.value)} placeholder="Vergunningnummer (optioneel)" disabled={uploading} /> setApplicant(e.target.value)} placeholder="Aanvrager (optioneel)" disabled={uploading} />
{uploadErr &&
{uploadErr}
}
} {/* ---- CHAT TAB — uses template ChatPanel ---- */} {tab === "chat" && (
)} {/* ---- DETAIL MODAL ---- */} {detail &&
setDetail(null)}> e.stopPropagation()}>
{detail.permit_number || detail.source_file}
{/* Archive status banner */} {detail.archive_status &&
Archiefstatus: {detail.archive_status} {detail.archive_nomination && <> — Nominatie: {detail.archive_nomination}} {detail.retention_years && <> ({detail.retention_years} jaar)}
} {/* Metadata grid */}
{detail.error_message &&
{detail.error_message}
} {detail.extracted_text && !detail.extracted_text.startsWith("Error:") &&

Geëxtraheerde tekst

{detail.extracted_text}
}
}
) } export default App