fix: BLS-Import prüft bei Dateiauswahl, Import nur per Schalter
All checks were successful
Deploy Development / deploy (push) Successful in 1m28s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 23s

Eine Datei pro Typ, sofortiger Dry-Run, Schalter startet den Write.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-09-12 14:43:59 +02:00
parent 9bb89231d0
commit 7ccae33844

View File

@ -1,8 +1,96 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { api } from '../utils/api' 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 [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 ( return (
<div className="form-row" style={{ flexDirection: 'column', alignItems: 'stretch', gap: 8, marginTop: 16 }}> <div className="form-row" style={{ flexDirection: 'column', alignItems: 'stretch', gap: 8, marginTop: 16 }}>
<label className="form-label">{label}</label> <label className="form-label">{label}</label>
@ -10,55 +98,50 @@ function FileImportBlock({ label, kind, busy, onRun }) {
type="file" type="file"
accept=".xlsx" accept=".xlsx"
className="form-input" 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> </div>
) )
} }
export default function AdminBlsImportPage() { export default function AdminBlsImportPage() {
const [status, setStatus] = useState(null) const [status, setStatus] = useState(null)
const [msg, setMsg] = useState(null)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [busy, setBusy] = useState(false)
const load = () => { const load = () => {
api.adminBlsStatus().then(setStatus).catch((e) => setError(e.message)) api.adminBlsStatus().then(setStatus).catch((e) => setError(e.message))
} }
useEffect(() => { load() }, []) 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 ( return (
<div className="card"> <div className="card">
<h1 className="page-title">BLS 4.0 importieren</h1> <h1 className="page-title">BLS 4.0 importieren</h1>
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6 }}> <p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6 }}>
Offizielle Dateien von blsdb.de (frei verfügbar, MRI). Codes bleiben erhalten. 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> </p>
{status && ( {status && (
<p style={{ fontSize: 13 }}> <p style={{ fontSize: 13 }}>
@ -67,9 +150,8 @@ export default function AdminBlsImportPage() {
</p> </p>
)} )}
{error && <p style={{ color: 'var(--danger)' }}>{error}</p>} {error && <p style={{ color: 'var(--danger)' }}>{error}</p>}
<FileImportBlock label="Components XLSX" kind="components" busy={busy} onRun={run} /> <FileImportBlock label="Components XLSX" kind="components" onImported={load} />
<FileImportBlock label="Daten XLSX" kind="foods" busy={busy} onRun={run} /> <FileImportBlock label="Daten XLSX" kind="foods" onImported={load} />
{msg && <pre style={{ whiteSpace: 'pre-wrap', fontSize: 12, background: 'var(--surface2)', padding: 12, borderRadius: 8 }}>{msg}</pre>}
</div> </div>
) )
} }