/* 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;