From 7a59f8a6bca682761526f43ac1980172894c9fd2 Mon Sep 17 00:00:00 2001 From: gitea_admin Date: Wed, 10 Jun 2026 14:41:43 +0000 Subject: [PATCH] Automated commit --- frontend/src/App.tsx | 341 ++++++++++++++++++++++++++++--------------- 1 file changed, 224 insertions(+), 117 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 860cef0..e9248a4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,144 +1,251 @@ -import { useEffect, useState } from "react" +import { useState, useEffect, useRef, useCallback } from "react" import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { ChatPanel } from "@/components/chat" -interface SearchResult { - title: string - url: string - snippet: string +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 [appName, setAppName] = useState("My App") - const [tab, setTab] = useState<"home" | "search" | "chat">("home") + 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 - useEffect(() => { - fetch("/api/info") - .then((res) => res.json()) - .then((data) => setAppName(data.app_name)) - .catch(() => {}) + 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 ( -
-
+
+
-

{appName}

+

Vergunningzoeker

- - - + {(["search", "upload", "chat"] as const).map(t => ( + + ))}
- {tab === "home" && ( -
+
+ + {/* ---- SEARCH TAB ---- */} + {tab === "search" && <> - - {appName} - - -

- Welkom! Klik op Assistent om te chatten met de AI-assistent, - of pas deze pagina aan voor je eigen applicatie. -

+ +
+ 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}
+
+
+ ) + })}
} +
+
+ } - {tab === "search" && } + {/* ---- 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}
} +
+
} - {tab === "chat" && ( -
- -
- )} + {/* ---- 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}
+
} +
+
+
} +
) } -function SearchPage() { - const [query, setQuery] = useState("") - const [results, setResults] = useState([]) - const [loading, setLoading] = useState(false) - const [searched, setSearched] = useState(false) - - const search = async () => { - if (!query.trim()) return - setLoading(true) - setSearched(true) - try { - const r = await fetch("/api/ai/search", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ query: query.trim() }), - }) - const data = await r.json() - setResults(data.results || []) - } catch { - setResults([]) - } finally { - setLoading(false) - } - } - - return ( -
- - - Web Search - - -
- setQuery(e.target.value)} - onKeyDown={e => e.key === "Enter" && search()} - placeholder="Zoek op het web..." - disabled={loading} - /> - -
-
-
- - {searched && ( - - - {loading ? ( -

Zoeken...

- ) : results.length === 0 ? ( -

Geen resultaten gevonden.

- ) : ( -
- {results.map((r, i) => ( -
- - {r.title} - -

{r.url}

-

{r.snippet}

-
- ))} -
- )} -
-
- )} -
- ) -} - export default App