Food Enhancesment - Rezepte. Listen, BLS Support #107

Merged
Lars merged 19 commits from develop into main 2026-09-13 15:25:21 +02:00
Showing only changes of commit 7ccae33844 - Show all commits

View File

@ -1,8 +1,96 @@
import { useEffect, useState } from 'react'
import { api } from '../utils/api'
function FileImportBlock({ label, kind, busy, onRun }) {
function summarizeCheck(kind, res) {
if (!res) return ''
if (kind === 'components') return `${res.attributes ?? 0} Stoffe erkannt`
return `${res.foods ?? 0} Lebensmittel, ${res.attribute_columns ?? 0} Stoffspalten`
}
function ImportSwitch({ on, disabled, busy, onEnable }) {
return (
<label style={{ display: 'flex', alignItems: 'center', gap: 10, opacity: disabled ? 0.5 : 1 }}>
<button
type="button"
role="switch"
aria-checked={on}
aria-label="Import"
disabled={disabled || busy || on}
onClick={() => { if (!on && !disabled && !busy) onEnable() }}
style={{
width: 44,
height: 24,
borderRadius: 12,
border: 'none',
padding: 0,
background: on ? 'var(--accent)' : 'var(--border)',
position: 'relative',
cursor: disabled || busy || on ? 'not-allowed' : 'pointer',
}}
>
<span
style={{
position: 'absolute',
top: 2,
left: on ? 22 : 2,
width: 20,
height: 20,
borderRadius: '50%',
background: '#fff',
display: 'block',
}}
/>
</button>
<span style={{ fontSize: 14, color: 'var(--text1)' }}>
{on ? 'Importiert' : 'Import'}
</span>
</label>
)
}
function FileImportBlock({ label, kind, onImported }) {
const [file, setFile] = useState(null)
const [check, setCheck] = useState(null)
const [error, setError] = useState(null)
const [busy, setBusy] = useState(null)
const [imported, setImported] = useState(false)
const [applyResult, setApplyResult] = useState(null)
const apiFn = kind === 'components' ? api.adminBlsImportComponents : api.adminBlsImportFoods
const checkFile = async (nextFile) => {
setError(null)
setCheck(null)
setApplyResult(null)
setImported(false)
if (!nextFile) return
setBusy('check')
try {
setCheck(await apiFn(nextFile, true))
} catch (e) {
setError(e.message)
} finally {
setBusy(null)
}
}
const applyImport = async () => {
if (!file || !check) return
setBusy('apply')
setError(null)
try {
const res = await apiFn(file, false)
setApplyResult(res)
setImported(true)
onImported?.()
} catch (e) {
setError(e.message)
setImported(false)
} finally {
setBusy(null)
}
}
return (
<div className="form-row" style={{ flexDirection: 'column', alignItems: 'stretch', gap: 8, marginTop: 16 }}>
<label className="form-label">{label}</label>
@ -10,55 +98,50 @@ function FileImportBlock({ label, kind, busy, onRun }) {
type="file"
accept=".xlsx"
className="form-input"
onChange={(e) => setFile(e.target.files?.[0] || null)}
disabled={busy}
onChange={(e) => {
const next = e.target.files?.[0] || null
setFile(next)
checkFile(next)
}}
/>
{busy && <p style={{ fontSize: 13, color: 'var(--text2)', margin: 0 }}>{busy === 'apply' ? 'Importiere…' : 'Prüfe…'}</p>}
{error && <p style={{ color: 'var(--danger)', margin: 0 }}>{error}</p>}
{check && !error && (
<p style={{ fontSize: 13, color: 'var(--text2)', margin: 0 }}>
Prüfung: {summarizeCheck(kind, check)}
</p>
)}
{applyResult && (
<p style={{ fontSize: 13, color: 'var(--text2)', margin: 0 }}>
{applyResult.inserted ?? 0} neu · {applyResult.updated ?? 0} aktualisiert
</p>
)}
<ImportSwitch
on={imported}
disabled={!check || !!error}
busy={busy}
onEnable={applyImport}
/>
{file && <p style={{ fontSize: 12, color: 'var(--text2)', margin: 0 }}>{file.name}</p>}
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<button type="button" className="btn btn-secondary" disabled={!file || busy} onClick={() => onRun(kind, file, true)}>
Prüfen
</button>
<button type="button" className="btn btn-primary" disabled={!file || busy} onClick={() => onRun(kind, file, false)}>
Anwenden
</button>
</div>
</div>
)
}
export default function AdminBlsImportPage() {
const [status, setStatus] = useState(null)
const [msg, setMsg] = useState(null)
const [error, setError] = useState(null)
const [busy, setBusy] = useState(false)
const load = () => {
api.adminBlsStatus().then(setStatus).catch((e) => setError(e.message))
}
useEffect(() => { load() }, [])
const run = async (kind, file, dry) => {
setError(null)
setBusy(true)
setMsg(dry ? 'Prüfe…' : 'Importiere…')
try {
const fn = kind === 'components' ? api.adminBlsImportComponents : api.adminBlsImportFoods
const res = await fn(file, dry)
setMsg(JSON.stringify(res, null, 2))
if (!dry) load()
} catch (e) {
setError(e.message)
setMsg(null)
} finally {
setBusy(false)
}
}
return (
<div className="card">
<h1 className="page-title">BLS 4.0 importieren</h1>
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6 }}>
Offizielle Dateien von blsdb.de (frei verfügbar, MRI). Codes bleiben erhalten.
Zuerst Components, dann die Datendatei. Immer zuerst „Prüfen“.
Datei wählen prüft automatisch. Der Schalter startet den Import. Zuerst Components, dann Daten.
</p>
{status && (
<p style={{ fontSize: 13 }}>
@ -67,9 +150,8 @@ export default function AdminBlsImportPage() {
</p>
)}
{error && <p style={{ color: 'var(--danger)' }}>{error}</p>}
<FileImportBlock label="Components XLSX" kind="components" busy={busy} onRun={run} />
<FileImportBlock label="Daten XLSX" kind="foods" busy={busy} onRun={run} />
{msg && <pre style={{ whiteSpace: 'pre-wrap', fontSize: 12, background: 'var(--surface2)', padding: 12, borderRadius: 8 }}>{msg}</pre>}
<FileImportBlock label="Components XLSX" kind="components" onImported={load} />
<FileImportBlock label="Daten XLSX" kind="foods" onImported={load} />
</div>
)
}