Eine Datei pro Typ, sofortiger Dry-Run, Schalter startet den Write. Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
4.7 KiB
JavaScript
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>
|
|
)
|
|
}
|