import { useState, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { ArrowLeft, FileSpreadsheet, Loader2 } from 'lucide-react'
import { api } from '../utils/api'
/** Ziele, die der Universal-Executor bereits schreiben kann (ohne manuelle Modul-Wahl). */
const EXECUTOR_READY = new Set(['nutrition', 'weight', 'blood_pressure'])
const MODULE_LABEL = {
nutrition: 'Ernährung',
weight: 'Gewicht',
blood_pressure: 'Blutdruck',
activity: 'Aktivität',
}
function mergeMappingChoices(detected, mapData) {
const sys = mapData.system_templates || []
const usr = mapData.user_mappings || []
const flat = [
...sys.map((m) => ({ ...m, _isSys: true })),
...usr.map((m) => ({ ...m, _isSys: false })),
]
const byId = new Map(flat.map((m) => [m.id, m]))
const out = []
const seen = new Set()
for (const d of detected || []) {
const row = byId.get(d.mapping_id)
if (row) {
out.push({
id: row.id,
module: row.module,
name: row.name,
is_system: row.is_system,
confidence: d.confidence ?? 0,
})
seen.add(row.id)
}
}
for (const row of flat) {
if (!seen.has(row.id)) {
out.push({
id: row.id,
module: row.module,
name: row.name,
is_system: row.is_system,
confidence: 0,
})
}
}
return out
}
function SampleTable({ sampleRows, columns }) {
if (!sampleRows?.length || !columns?.length) return null
const showCols = columns.slice(0, 8)
return (
{showCols.map((c) => (
{c}
))}
{sampleRows.slice(0, 5).map((row, i) => (
{showCols.map((c) => (
{row[c] ?? '—'}
))}
))}
)
}
export default function UniversalCsvImportPage() {
const navigate = useNavigate()
const [file, setFile] = useState(null)
const [analyzeResult, setAnalyzeResult] = useState(null)
const [mappingChoices, setMappingChoices] = useState([])
const [mappingId, setMappingId] = useState('')
const [loadingAnalyze, setLoadingAnalyze] = useState(false)
const [loadingImport, setLoadingImport] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(null)
const selectedChoice = useMemo(
() => mappingChoices.find((c) => String(c.id) === String(mappingId)),
[mappingChoices, mappingId],
)
const importAllowed = selectedChoice && EXECUTOR_READY.has(selectedChoice.module)
const handleAnalyze = async () => {
if (!file) {
setError('Bitte eine CSV-Datei wählen')
return
}
setLoadingAnalyze(true)
setError(null)
setSuccess(null)
setMappingId('')
try {
const res = await api.analyzeCsv(file)
setAnalyzeResult(res)
const mapData = await api.getCsvMappings()
const merged = mergeMappingChoices(res.detected_mappings || [], mapData)
setMappingChoices(merged)
const best = res.recommended || (res.detected_mappings || [])[0]
let pick = ''
if (best && merged.some((m) => m.id === best.mapping_id)) {
pick = String(best.mapping_id)
} else if (merged.length) {
const firstReady = merged.find((m) => EXECUTOR_READY.has(m.module))
pick = String((firstReady || merged[0]).id)
}
setMappingId(pick)
} catch (e) {
setError(e.message || 'Analyse fehlgeschlagen')
setAnalyzeResult(null)
setMappingChoices([])
} finally {
setLoadingAnalyze(false)
}
}
const handleImport = async () => {
if (!file || !mappingId) {
setError('Bitte Datei und Vorlage wählen')
return
}
if (!importAllowed) {
setError('Diese Vorlage kann hier noch nicht importiert werden (z. B. Aktivität → Seite „Aktivität“).')
return
}
setLoadingImport(true)
setError(null)
setSuccess(null)
try {
const res = await api.importUniversalCsv(file, Number(mappingId))
const st = res.stats || {}
const modLabel = MODULE_LABEL[res.module] || res.module || ''
setSuccess(
(modLabel ? `${modLabel}: ` : '') +
`Import fertig — ${st.imported ?? 0} neu, ${st.updated ?? 0} aktualisiert, ` +
`${st.skipped ?? 0} übersprungen, ${st.errors ?? 0} Zeilenfehler.`,
)
} catch (e) {
setError(e.message || 'Import fehlgeschlagen')
} finally {
setLoadingImport(false)
}
}
return (
navigate(-1)}
className="btn btn-secondary"
style={{
marginBottom: 16,
display: 'inline-flex',
alignItems: 'center',
gap: 8,
padding: '8px 12px',
}}
>
Zurück
CSV-Import
Datei hochladen: Die App vergleicht die Spalten-Struktur mit allen gespeicherten Vorlagen und schlägt
passende Ziele vor (Ernährung, Gewicht, Blutdruck, …). Du bestätigst nur noch die Vorlage — ohne
vorher ein „Modul“ raten zu müssen.{' '}
Später: eine Datei, mehrere Zieltabellen. Trainingseinheiten aktuell weiter unter{' '}
navigate('/activity')}
>
Aktivität
.
{error && (
{error}
)}
{success && (
{success}
)}
{analyzeResult && (
2. Erkennung & Vorschau
Trennzeichen: {analyzeResult.delimiter} · Spalten:{' '}
{analyzeResult.columns?.length ?? 0}
{analyzeResult.recommended && (
Vorschlag: {' '}
{MODULE_LABEL[analyzeResult.recommended.module] || analyzeResult.recommended.module} —{' '}
{analyzeResult.recommended.mapping_name} (
{Math.round((analyzeResult.recommended.confidence || 0) * 100)} % Übereinstimmung der Spalten)
)}
{(analyzeResult.detected_mappings || []).length > 1 && (
Weitere Treffer
{analyzeResult.detected_mappings.slice(1, 8).map((d) => (
{MODULE_LABEL[d.module] || d.module}: {d.mapping_name} ·{' '}
{Math.round((d.confidence || 0) * 100)} %
))}
)}
Import-Vorlage
setMappingId(e.target.value)}
style={{ width: '100%', marginTop: 8 }}
>
{mappingChoices.length === 0 ? (
Keine Vorlage geladen
) : (
mappingChoices.map((o) => (
{MODULE_LABEL[o.module] || o.module} — {o.name}
{o.is_system ? ' (System)' : ''}
{o.confidence > 0 ? ` · ${Math.round(o.confidence * 100)} %` : ''}
{!EXECUTOR_READY.has(o.module) ? ' · Import: noch nicht hier' : ''}
))
)}
{selectedChoice && !importAllowed && (
Diese Vorlage passt strukturell, wird aber an der passenden Stelle importiert (z. B. Aktivität →
Apple-Health-CSV dort).
)}
{loadingImport ? (
<>
Import
läuft …
>
) : (
'Import starten'
)}
)}
)
}