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) => ( ))} {sampleRows.slice(0, 5).map((row, i) => ( {showCols.map((c) => ( ))} ))}
{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 (

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{' '} .

{error && (
{error}
)} {success && (
{success}
)}
1. CSV-Datei
{ setFile(e.target.files?.[0] || null) setAnalyzeResult(null) setMappingChoices([]) setMappingId('') setSuccess(null) }} />
{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
{selectedChoice && !importAllowed && (

Diese Vorlage passt strukturell, wird aber an der passenden Stelle importiert (z. B. Aktivität → Apple-Health-CSV dort).

)}
)}
) }