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>
This commit is contained in:
parent
9bb89231d0
commit
7ccae33844
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user