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 { 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user