/* Påfylling direkte fra automat-siden (2026-09-04). * * 1. Start: velg omfang (tomme/tomflaske-hyller, seksjon, eller enkeltdører) * → dørene åpnes; kameraet tar bilde og detekterer når hver dør lukkes. * 2. Under fylling: dør-for-dør-status (før / nå / bilde / konfidens), og * operatøren kan RETTE feil deteksjon (setter lager + lagrer treningsbilde) * eller ta nytt bilde. * 3. Avslutt → rapport: hva ble tatt ut (tomflasker) og satt inn (fulle), * per type og per dør. Historikk over tidligere fyllinger. * 4. Kontroller lageret: gå gjennom alle hyller, bekreft eller rett. */ const FILL_KIND = { satt_inn: { no: 'Satt inn', tone: 'ok' }, byttet: { no: 'Tomflaske ut, full inn', tone: 'ok' }, byttet_type: { no: 'Byttet type', tone: 'info' }, tatt_ut: { no: 'Tomflaske tatt ut', tone: 'warn' }, uendret: { no: 'Uendret', tone: 'empty' }, uendret_tomflaske: { no: 'Tomflaske står igjen', tone: 'warn' }, defekt: { no: 'Defekt / mismatch', tone: 'danger' }, ubekreftet: { no: 'Ubekreftet', tone: 'danger' }, }; const fillStatusLabel = (st) => ({ full: 'Full', empty: 'Ledig', empty_bottle: 'Tomflaske', faulty: 'Defekt', unknown: 'Venter / usikker', }[st] || st || '—'); /* Bilde-thumb med versjonert URL (ny bilde-id → ny henting) */ const FillThumb = ({ url, imageId, size = 64 }) => { const [src, setSrc] = React.useState(null); const full = url ? `${url}?v=${encodeURIComponent(imageId || '')}&size=${size * 2}` : null; React.useEffect(() => { if (!full) { setSrc(null); return; } let dau = false; window.authedImageUrl(full).then(u => { if (!dau) setSrc(u); }).catch(() => { if (!dau) setSrc(null); }); return () => { dau = true; }; }, [full]); return (
{src ? : }
); }; /* Dropdown for å rette flasketype — kun typer i automatens sortiment + Tom */ const useTenantBottles = (machine) => { const [codes, setCodes] = React.useState(null); React.useEffect(() => { let dau = false; const shelfCodes = new Set((machine.shelves || []).map(s => s.bottle).filter(Boolean)); if (!machine.tenantId) { setCodes(shelfCodes); return; } window.api.listPriceOverrides(machine.tenantId, machine.id) .then(rows => { if (dau) return; rows.filter(r => r.offered).forEach(r => shelfCodes.add(r.code)); setCodes(new Set(shelfCodes)); }) .catch(() => { if (!dau) setCodes(shelfCodes); }); return () => { dau = true; }; }, [machine.id]); return (window.DATA?.BOTTLE_TYPES || []).filter(b => !codes || codes.size === 0 || codes.has(b.id)); }; const CorrectSelect = ({ bottles, value, onPick, disabled, lang }) => ( ); const FillTab = ({ machine, lang }) => { const no = lang === 'no'; const [active, setActive] = React.useState(undefined); // undefined=laster, null=ingen const [history, setHistory] = React.useState([]); const [report, setReport] = React.useState(null); const [scope, setScope] = React.useState('empty'); // empty | A | B | doors const [pick, setPick] = React.useState(() => new Set()); const [includeFaulty, setIncludeFaulty] = React.useState(false); const [busy, setBusy] = React.useState(null); const [err, setErr] = React.useState(null); const bottles = useTenantBottles(machine); const last = React.useCallback(async () => { try { const r = await window.api.fillActive(machine.id); setActive(r && r.active ? r : null); } catch (e) { setErr(e.message); } }, [machine.id]); const lastHist = React.useCallback(() => { window.api.fillHistory(machine.id).then(setHistory).catch(() => {}); }, [machine.id]); React.useEffect(() => { last(); lastHist(); }, [last, lastHist]); React.useEffect(() => { if (!active) return; const iv = setInterval(last, 3000); return () => clearInterval(iv); }, [active && active.fill_session_id, last]); const start = async () => { setBusy('start'); setErr(null); try { const opts = { include_faulty: includeFaulty }; if (scope === 'A') opts.section = 1; else if (scope === 'B') opts.section = 2; else if (scope === 'doors') { if (pick.size === 0) { setErr(no ? 'Velg minst én dør' : 'Pick at least one door'); setBusy(null); return; } opts.doors = [...pick]; } await window.api.fillStart(machine.id, opts); setReport(null); await last(); } catch (e) { setErr(e.message); } setBusy(null); }; const finish = async () => { if (!active) return; if (!confirm(no ? 'Avslutte påfyllingen og lage rapport?' : 'Finish and create report?')) return; setBusy('finish'); setErr(null); try { const r = await window.api.fillFinish(machine.id, active.fill_session_id); setReport(r.report || null); setActive(null); lastHist(); if (machine._onReload) await machine._onReload(); } catch (e) { setErr(e.message); } setBusy(null); }; const cancel = async () => { if (!active) return; if (!confirm(no ? 'Avbryte påfyllingen? Hyllene settes tilbake slik de var.' : 'Cancel refill?')) return; setBusy('cancel'); setErr(null); try { await window.api.fillCancel(machine.id, active.fill_session_id); setActive(null); if (machine._onReload) await machine._onReload(); } catch (e) { setErr(e.message); } setBusy(null); }; const correct = async (shelf, label) => { setBusy('c' + shelf.id); setErr(null); try { await window.api.confirmLabel(machine.id, shelf.id, { label, detected: shelf.bottle?.code || null, detected_score: shelf.last_score ?? null, }); await last(); } catch (e) { setErr(e.message); } setBusy(null); }; const recheck = async (shelf) => { setBusy('r' + shelf.id); setErr(null); try { await window.api.recheckShelf(machine.id, shelf.id); } catch (e) { setErr(e.message); } setBusy(null); }; // Åpne dører (på nytt): null = alle som ikke er bekreftet åpne const reopen = async (doors) => { if (!active) return; setBusy('open'); setErr(null); try { await window.api.fillOpenDoors(machine.id, active.fill_session_id, doors); await last(); } catch (e) { setErr(e.message); } setBusy(null); }; const allShelves = machine.shelves || []; const candidates = allShelves.filter(s => ['vacant', 'empty'].includes(s.state) || (includeFaulty && s.state === 'error')); return (
{err &&
❌ {err}
} {/* ===== AKTIV PÅFYLLING ===== */} {active && (

{no ? 'Påfylling pågår' : 'Refill in progress'} {no ? 'startet' : 'started'} {formatTs(active.started_at)} · {active.doors.length} {no ? 'dører' : 'doors'}

{no ? 'Dørene åpnes én etter én (ca. 2,5 s per dør per modul). Ta ut ev. tomflaske, sett inn full flaske og lukk døra — bildet tas og typen detekteres automatisk. Er deteksjonen feil, rett den her før du avslutter.' : 'Doors open one by one. Swap bottles and close each door — detection runs automatically. Correct any wrong detection before finishing.'}
{(() => { const dop = active.door_open || {}; const res = dop.doors || {}; const opened = active.doors.filter(d => (res[String(d)] || {}).opened === true); const notOpen = active.doors.filter(d => (res[String(d)] || {}).opened !== true); if (dop.opening) { return (
⏳ {no ? 'Åpner dører…' : 'Opening doors…'} {opened.length}/{active.doors.length} {no ? 'bekreftet åpne' : 'confirmed open'}
); } if (dop.done && notOpen.length > 0) { return (
⚠️ {notOpen.length} {no ? 'dør(er) er ikke bekreftet åpne' : 'door(s) not confirmed open'}: {notOpen.join(', ')}
); } return null; })()}
{active.shelves.map(s => { const p = (active.prior || {})[String(s.door)] || {}; const pb = (window.DATA?.BOTTLE_TYPES || []).find(b => b._raw?.id === p.bottle_id); const isSuper = !!(window.isSuperView && window.isSuperView()); const waiting = s.status === 'unknown'; const ds = (active.door_states || {})[String(s.door)]; const dres = ((active.door_open || {}).doors || {})[String(s.door)] || {}; const hatch = ds && ds.closed === false ? 'open' : ds && ds.closed === true ? 'closed' : 'unknown'; return ( ); })}
{no ? 'Dør' : 'Door'}{no ? 'Luke' : 'Hatch'}{no ? 'Før' : 'Before'}{no ? 'Nå' : 'Now'} {no ? 'Bilde' : 'Image'}{no ? 'Rett' : 'Correct'}
{s.door}
{hatch === 'open' ? {no ? 'Åpen' : 'Open'} : hatch === 'closed' ? {no ? 'Lukket' : 'Closed'} : {no ? 'Ukjent' : 'Unknown'}} {(active.door_open || {}).opening && dres.opened !== true && ⏳} {!(active.door_open || {}).opening && ( )}
{fillStatusLabel(p.status)}{pb ? ` · ${pb.name}` : ''} {waiting ? {no ? 'Venter på lukking / deteksjon' : 'Waiting'} : s.status === 'full' ? {no ? 'Full' : 'Full'} {s.bottle?.display_name || '—'} {isSuper && s.last_score != null && {Math.round(s.last_score * 100)} %} : s.status === 'faulty' ? {no ? 'Defekt / ventil-mismatch' : 'Faulty'} : {fillStatusLabel(s.status)}{s.bottle ? ` · ${s.bottle.display_name}` : ''}} correct(s, v)} />
{(() => { const done = active.shelves.filter(s => s.status !== 'unknown').length; return `${done}/${active.shelves.length} ${no ? 'dører ferdig' : 'doors done'}`; })()}
)} {/* ===== START ===== */} {active === null && (

{no ? 'Start påfylling' : 'Start refill'}

{[['empty', no ? 'Alle ledige + tomflaske-hyller' : 'All empty + empty-bottle shelves'], ['A', no ? 'Seksjon A (dør 1–12)' : 'Section A (1–12)'], ['B', no ? 'Seksjon B (dør 13–24)' : 'Section B (13–24)'], ['doors', no ? 'Velg dører' : 'Pick doors']].map(([k, lbl]) => ( ))}
{scope === 'doors' && (
{allShelves.map(s => ( ))}
)}
{scope === 'empty' ? `${candidates.length} ${no ? 'hyller vil åpnes' : 'shelves will open'}` : ''}
)} {/* ===== RAPPORT ===== */} {report && setReport(null)} />} {/* ===== HISTORIKK ===== */}

{no ? 'Tidligere påfyllinger' : 'Previous refills'}

{history.length === 0 ? (
{no ? 'Ingen påfyllinger registrert ennå' : 'No refills yet'}
) : (
{history.map(h => ( ))}
{no ? 'Startet' : 'Started'}Status{no ? 'Dører' : 'Doors'} {no ? 'Satt inn' : 'Inserted'}{no ? 'Tatt ut' : 'Removed'} {no ? 'Ubekreftet' : 'Unconfirmed'}
{formatTs(h.started_at)} {h.status === 'done' ? {no ? 'fullført' : 'done'} : h.status === 'active' ? {no ? 'pågår' : 'active'} : {no ? 'avbrutt' : 'cancelled'}} {h.doors} {h.summary ? h.summary.inserted_total : '—'} {h.summary ? h.summary.removed_total : '—'} 0 ? 'var(--danger)' : 'inherit' }}>{h.summary ? h.summary.unconfirmed : '—'}
)}
{/* ===== KONTROLLER LAGERET ===== */}
); }; const FillReport = ({ report, lang, onClose }) => { const no = lang === 'no'; const s = report.summary || {}; const kv = (o) => Object.entries(o || {}).map(([k, v]) => `${v} × ${k}`).join(', ') || '—'; return (

{no ? 'Påfyllingsrapport' : 'Refill report'} · {formatTs(report.started_at)}{report.finished_at ? ` → ${formatTs(report.finished_at)}` : ''}

{no ? 'Satt inn (fulle)' : 'Inserted'}
{s.inserted_total ?? 0}
{no ? 'Tatt ut (tomme)' : 'Removed'}
{s.removed_total ?? 0}
{no ? 'Dører' : 'Doors'}
{s.doors_total ?? 0}
{no ? 'Ubekreftet' : 'Unconfirmed'}
{s.unconfirmed ?? 0}
{report.by &&
{no ? 'Utført av' : 'By'}: {report.by}
}
{(report.doors || []).map(d => { const k = FILL_KIND[d.kind] || { no: d.kind, tone: 'empty' }; return ( ); })}
{no ? 'Dør' : 'Door'}{no ? 'Før' : 'Before'}{no ? 'Etter' : 'After'}{no ? 'Hendelse' : 'Event'}{no ? 'Bilde' : 'Image'}
{d.door} {fillStatusLabel(d.prior_status)}{d.prior_bottle ? ` · ${d.prior_bottle.display_name}` : ''} {fillStatusLabel(d.now_status)}{d.now_bottle ? · {d.now_bottle.display_name} : ''} {k.no}
); }; /* Gå gjennom alle hyller: bekreft eller rett hva som faktisk står der */ const ShelfReview = ({ machine, lang, bottles }) => { const no = lang === 'no'; const [rows, setRows] = React.useState(machine.shelves || []); const [busy, setBusy] = React.useState(null); const [err, setErr] = React.useState(null); const [confirmed, setConfirmed] = React.useState(() => new Set()); const refresh = React.useCallback(async () => { try { setRows(await window.api.refreshMachineShelves(machine.id)); } catch (e) { setErr(e.message); } }, [machine.id]); React.useEffect(() => { refresh(); }, [refresh]); const bekreft = async (s) => { setBusy(s.rawShelfId); setErr(null); try { await window.api.confirmLabel(machine.id, s.rawShelfId, { label: s.state === 'full' && s.bottle ? s.bottle : 'tom', detected: s.bottle || null, detected_score: s.lastScore ?? null, }); setConfirmed(prev => new Set(prev).add(s.rawShelfId)); await refresh(); } catch (e) { setErr(e.message); } setBusy(null); }; const rett = async (s, label) => { setBusy(s.rawShelfId); setErr(null); try { await window.api.confirmLabel(machine.id, s.rawShelfId, { label, detected: s.bottle || null, detected_score: s.lastScore ?? null, }); setConfirmed(prev => new Set(prev).add(s.rawShelfId)); await refresh(); if (machine._onReload) await machine._onReload(); } catch (e) { setErr(e.message); } setBusy(null); }; const setStatus = async (s, status) => { setBusy(s.rawShelfId); setErr(null); try { await window.api.overrideShelf(machine.id, s.rawShelfId, status === 'empty_bottle' ? s.bottle : null, status); await refresh(); if (machine._onReload) await machine._onReload(); } catch (e) { setErr(e.message); } setBusy(null); }; return (

{no ? 'Kontroller lageret' : 'Review inventory'}

{confirmed.size}/{rows.length} {no ? 'bekreftet' : 'confirmed'}
{no ? 'Gå gjennom hylle for hylle: stemmer bildet med typen? Bekreft, eller rett. Bekreftelser lagres som treningsdata og gjør gjenkjenningen bedre.' : 'Go through each shelf: does the image match the type? Confirm or correct. Confirmations are saved as training data.'}
{err &&
❌ {err}
}
{rows.map(s => ( ))}
{no ? 'Dør' : 'Door'}{no ? 'Bilde' : 'Image'}Status{no ? 'Innhold' : 'Contents'}
{s.rawDoor} {fillStatusLabel(s.rawStatus)} {s.bottleName || —}
rett(s, v)} /> {s.state === 'full' && s.bottle && ( )} {s.state !== 'error' ? : }
); }; Object.assign(window, { FillTab });