/* Review screen: snelle beeldbeoordeling voor AI-resultaten */ function ReviewQueueItem({ item, actief, index, onSelect }) { return ( ); } function ReviewHistorie({ items }) { if (!items || items.length === 0) { return
Geen eerdere beoordelingen voor dit pand en type.
; } return (
{items.slice(-4).map((h, idx) => (
{h.beoordeling} {h.aanwezig ? 'Aanwezig' : 'Niet aanwezig'} {fmtDateTime(h.beoordeeld_op || h.geanalyseerd_op)}
))}
); } function SchermReview({ gemeenten, defaultGemeente, onNaarPand, resultaatId = null }) { const enkelModus = resultaatId != null; const [gemeente, setGemeente] = useState(() => defaultGemeente || ''); const [detectieType, setDetectieType] = useState('contour'); const [zekerheid, setZekerheid] = useState('laag,midden'); const [queue, setQueue] = useState([]); const [huidig, setHuidig] = useState(0); const [loading, setLoading] = useState(false); const [melding, setMelding] = useState(null); const [editModus, setEditModus] = useState(false); const [laatsteActie, setLaatsteActie] = useState(null); const [opslaanBezig, setOpslaanBezig] = useState(false); // Single-item modus (deep-link /review/{id}): laad dat ene resultaat, ongeacht // gemeente/zekerheid/reviewstatus. useEffect(() => { if (!enkelModus) return; setLoading(true); setMelding(null); fetch(`${API}/review/item/${resultaatId}`) .then(r => r.ok ? r.json() : Promise.reject(r)) .then(item => { setQueue([item]); setHuidig(0); setEditModus(false); if (item.gemeente_code) setGemeente(item.gemeente_code); if (item.detectie_type) setDetectieType(item.detectie_type); }) .catch(() => { setQueue([]); setMelding({ kind: 'error', tekst: 'Resultaat kon niet worden geladen.' }); }) .finally(() => setLoading(false)); }, [resultaatId]); useEffect(() => { if (enkelModus) return; // wachtrij niet laden in single-item modus if (!gemeente) { setQueue([]); setHuidig(0); return; } const params = new URLSearchParams({ gemeente_code: gemeente, detectie_type: detectieType, zekerheid, limit: '100', }); setLoading(true); setMelding(null); fetch(`${API}/review?${params}`) .then(r => r.ok ? r.json() : Promise.reject(r)) .then(data => { setQueue(Array.isArray(data) ? data : []); setHuidig(0); setEditModus(false); }) .catch(() => { setQueue([]); setMelding({ kind: 'error', tekst: 'Reviewwachtrij kon niet worden geladen.' }); }) .finally(() => setLoading(false)); }, [gemeente, detectieType, zekerheid]); const r = queue[huidig]; const ruwAiTekst = r && (r.ai_respons_ruw?.meta?.tekst || r.ai_respons_ruw?.meta?.fout); const gemeenteNaam = gemeenten.find(g => g.code === gemeente)?.naam || gemeente; const voortgang = queue.length ? `${huidig + 1} / ${queue.length}` : '0 / 0'; function volgende() { setEditModus(false); setHuidig(prev => { if (prev + 1 >= queue.length) return prev; return prev + 1; }); } function vorige() { setEditModus(false); setHuidig(prev => Math.max(0, prev - 1)); } async function beoordeel(beoordeling) { if (!r || opslaanBezig) return; setOpslaanBezig(true); setMelding(null); await fetch(`${API}/review/${r.id}`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ beoordeling }), }).then(resp => { if (!resp.ok) throw new Error('review mislukt'); setLaatsteActie({ item: r, index: huidig, beoordeling }); setQueue(prev => prev.filter(x => x.id !== r.id)); setHuidig(prev => Math.min(prev, Math.max(0, queue.length - 2))); setEditModus(false); }).catch(() => { setMelding({ kind: 'error', tekst: 'Beoordeling kon niet worden opgeslagen.' }); }).finally(() => setOpslaanBezig(false)); } async function undoLaatsteActie() { if (!laatsteActie) { vorige(); return; } await fetch(`${API}/review/${laatsteActie.item.id}`, { method: 'DELETE' }) .then(resp => { if (!resp.ok) throw new Error('undo mislukt'); setQueue(prev => { const copy = [...prev]; copy.splice(Math.min(laatsteActie.index, copy.length), 0, laatsteActie.item); return copy; }); setHuidig(Math.min(laatsteActie.index, queue.length)); setLaatsteActie(null); setMelding({ kind: 'info', tekst: 'Laatste beoordeling is teruggezet.' }); }) .catch(() => setMelding({ kind: 'error', tekst: 'Laatste beoordeling kon niet worden teruggezet.' })); } function overslaan() { if (!r) return; if (queue.length <= 1) { setMelding({ kind: 'info', tekst: 'Er staat maar één resultaat in de wachtrij.' }); return; } setQueue(prev => { const copy = [...prev]; const [item] = copy.splice(huidig, 1); copy.push(item); return copy; }); setHuidig(prev => Math.min(prev, queue.length - 2)); setEditModus(false); } useEffect(() => { function onKey(e) { const tag = (e.target && e.target.tagName || '').toLowerCase(); if (tag === 'input' || tag === 'select' || tag === 'textarea' || e.target?.isContentEditable) return; if (e.key === 'a' || e.key === 'A') { e.preventDefault(); beoordeel('correct'); } else if (e.key === 'n' || e.key === 'N') { e.preventDefault(); beoordeel('incorrect'); } else if (e.key === 'o' || e.key === 'O') { e.preventDefault(); beoordeel('onzeker'); } else if (e.key === 's' || e.key === 'S') { e.preventDefault(); overslaan(); } else if (e.key === 'ArrowRight') { e.preventDefault(); volgende(); } else if (e.key === 'ArrowLeft') { e.preventDefault(); vorige(); } else if (e.key === 'Backspace') { e.preventDefault(); undoLaatsteActie(); } else if (e.key === 'Escape') { setEditModus(false); } } window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [r, huidig, queue, laatsteActie]); const heeftQueue = queue.length > 0; return (

Review

Snel beeld beoordelen. Vlakken pas aanpassen wanneer dat nodig is.

{voortgang} {laatsteActie && Undo}
{!enkelModus && (
{['contour','zonnepanelen','dakkapel'].map(t => ( ))}
)} {melding && {melding.tekst}} {(!enkelModus && !gemeente) ? ( ) : loading ? ( ) : !heeftQueue ? ( ) : r && (
{r.heeft_afbeelding ? ( ) : (
Geen afbeelding beschikbaar
)}
beoordeel('correct')} disabled={opslaanBezig}>A Akkoord beoordeel('incorrect')} disabled={opslaanBezig}>N Niet akkoord beoordeel('onzeker')} disabled={opslaanBezig}>O Onzeker S Overslaan Vorige setEditModus(v => !v)}> {editModus ? 'Vorm bewerken aan' : 'Vorm bewerken'}
)}
); } window.SchermReview = SchermReview;