mitai-jinkendo/frontend/src/pages/AdminBlsImportPage.jsx
Lars 7ccae33844
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
fix: BLS-Import prüft bei Dateiauswahl, Import nur per Schalter
Eine Datei pro Typ, sofortiger Dry-Run, Schalter startet den Write.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 14:43:59 +02:00

158 lines
4.7 KiB
JavaScript

import { useEffect, useState } from 'react'
import { api } from '../utils/api'
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>
<input
type="file"
accept=".xlsx"
className="form-input"
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}
/>
</div>
)
}
export default function AdminBlsImportPage() {
const [status, setStatus] = useState(null)
const [error, setError] = useState(null)
const load = () => {
api.adminBlsStatus().then(setStatus).catch((e) => setError(e.message))
}
useEffect(() => { load() }, [])
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.
Datei wählen prüft automatisch. Der Schalter startet den Import. Zuerst Components, dann Daten.
</p>
{status && (
<p style={{ fontSize: 13 }}>
{status.official_foods} offizielle Lebensmittel · {status.official_attributes} Stoffe ·
{status.manual_foods} manuell · {status.source}
</p>
)}
{error && <p style={{ color: 'var(--danger)' }}>{error}</p>}
<FileImportBlock label="Components XLSX" kind="components" onImported={load} />
<FileImportBlock label="Daten XLSX" kind="foods" onImported={load} />
</div>
)
}