import { useState, useEffect, useRef } from 'react' import { useSearchParams } from 'react-router-dom' import { api } from '../api' const FIELD_TYPES = ['text', 'numeric', 'date'] function SampleTable({ rows }) { if (!rows || rows.length === 0) return null const cols = Object.keys(rows[0]) return (
{cols.map(c => )} {rows.map((row, i) => ( {cols.map(c => ( ))} ))}
{c}
{row[c] == null ? : String(row[c])}
) } export default function Sources({ source, sources, setSources, setSource }) { const [constraintFields, setConstraintFields] = useState('') const [globalPicklist, setGlobalPicklist] = useState(true) const [schemaFields, setSchemaFields] = useState([]) const [stats, setStats] = useState(null) const [sampleRows, setSampleRows] = useState([]) const [saving, setSaving] = useState(false) const [reprocessing, setReprocessing] = useState(false) const [generating, setGenerating] = useState(false) const [result, setResult] = useState('') const [error, setError] = useState('') const [viewName, setViewName] = useState('') const [availableFields, setAvailableFields] = useState([]) const [fieldSort, setFieldSort] = useState({ col: 'key', dir: 'asc' }) const [creating, setCreating] = useState(false) const [form, setForm] = useState({ name: '', constraint_fields: '', fields: [], schema: [], importSample: true }) const [createError, setCreateError] = useState('') const [createLoading, setCreateLoading] = useState(false) const [csvFileName, setCsvFileName] = useState('') const fileRef = useRef() const [searchParams, setSearchParams] = useSearchParams() const sourceObj = sources.find(s => s.name === source) useEffect(() => { if (searchParams.get('new') === '1') { setCreating(true) setSearchParams({}) } }, [searchParams]) useEffect(() => { if (!sourceObj) return setConstraintFields(sourceObj.constraint_fields?.join(', ') || '') setGlobalPicklist(sourceObj.global_picklist !== false) setSchemaFields((sourceObj.config?.fields || []).map((f, i) => ({ seq: i + 1, ...f }))) setViewName(sourceObj.config?.fields?.length ? `dfv.${sourceObj.name}` : '') setResult('') setError('') setStats(null) setAvailableFields([]) setSampleRows([]) api.getStats(sourceObj.name).then(setStats).catch(() => {}) api.getFields(sourceObj.name).then(setAvailableFields).catch(() => {}) api.getRecords(sourceObj.name, 50).then(rows => setSampleRows(rows.map(r => r.data).filter(Boolean))).catch(() => {}) }, [source, sourceObj?.name]) async function handleSave(e) { e.preventDefault() setSaving(true) setError('') try { const constraint_fields = constraintFields.split(',').map(s => s.trim()).filter(Boolean) const fields = [...schemaFields.filter(f => f.name)].sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0)) const config = { ...(sourceObj.config || {}), fields } await api.updateSource(sourceObj.name, { constraint_fields, config, global_picklist: globalPicklist }) if (fields.length > 0) { const res = await api.generateView(sourceObj.name) if (res.success) setViewName(res.view) } const updated = await api.getSources() setSources(updated) setResult('Saved.') } catch (err) { setError(err.message) } finally { setSaving(false) } } async function handleGenerateView() { setGenerating(true) setResult('') setError('') try { const constraint_fields = constraintFields.split(',').map(s => s.trim()).filter(Boolean) const fields = [...schemaFields.filter(f => f.name)].sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0)) const config = { ...(sourceObj.config || {}), fields } await api.updateSource(sourceObj.name, { constraint_fields, config, global_picklist: globalPicklist }) const res = await api.generateView(sourceObj.name) if (res.success) { setViewName(res.view) setResult(`View created: ${res.view}`) } else { setError(res.error) } } catch (err) { setError(err.message) } finally { setGenerating(false) } } async function handleReprocess() { if (!confirm(`Reprocess all records for "${sourceObj.name}"? This will clear and reapply all transformations.`)) return setReprocessing(true) setResult('') setError('') try { const res = await api.reprocess(sourceObj.name) setResult(`Reprocessed ${res.transformed} records.`) api.getStats(sourceObj.name).then(setStats).catch(() => {}) } catch (err) { setError(err.message) } finally { setReprocessing(false) } } async function handleDelete() { if (!confirm(`Delete source "${sourceObj.name}" and all its data?`)) return try { await api.deleteSource(sourceObj.name) const updated = await api.getSources() setSources(updated) if (updated.length > 0) setSource(updated[0].name) else setSource('') } catch (err) { alert(err.message) } } async function handleSuggest(e) { const file = e.target.files[0] if (!file) return setCsvFileName(file.name) try { const suggestion = await api.suggestSource(file) setForm(f => ({ ...f, fields: suggestion.fields, constraint_fields: '', schema: suggestion.fields.map(f => ({ name: f.name, type: f.type, seq: suggestion.fields.indexOf(f) + 1 })), sampleRows: suggestion.sampleRows || [] })) } catch (err) { setCreateError(err.message) } } async function handleCreate(e) { e.preventDefault() setCreateError('') const constraintArr = form.constraint_fields.split(',').map(s => s.trim()).filter(Boolean) if (!form.name || constraintArr.length === 0) { setCreateError('Name and at least one constraint field required') return } setCreateLoading(true) try { const config = form.schema.length > 0 ? { fields: form.schema } : {} await api.createSource({ name: form.name, constraint_fields: constraintArr, config, global_picklist: form.global_picklist !== false }) if (form.schema.length > 0) { await api.generateView(form.name) } if (form.importSample && fileRef.current?.files[0]) { await api.importCSV(form.name, fileRef.current.files[0]) } const updated = await api.getSources() setSources(updated) setSource(form.name) setForm({ name: '', constraint_fields: '', fields: [], schema: [], importSample: true }) setCreating(false) } catch (err) { setCreateError(err.message) } finally { setCreateLoading(false) } } return (

{sourceObj ? sourceObj.name : 'Sources'}

{/* No source selected */} {!sourceObj && !creating && (

No sources yet. Create one to get started.

)} {/* Source detail */} {sourceObj && !creating && (
{/* Stats */} {stats && (
{stats.total_records} total {stats.transformed_records} transformed {stats.pending_records} pending
)} {/* Unified field table */} {availableFields.length > 0 && (
{[ { col: 'key', label: 'Key' }, { col: 'origin', label: 'Origin' }, { col: 'type', label: 'Type' }, { col: 'constraint', label: 'Constraint', center: true }, { col: 'inview', label: 'In view', center: true }, { col: 'seq', label: 'Seq', center: true }, ].map(({ col, label, center }) => ( ))} {[...availableFields].sort((a, b) => { const constraintList = constraintFields.split(',').map(s => s.trim()) const aSchema = schemaFields.find(sf => sf.name === a.key) const bSchema = schemaFields.find(sf => sf.name === b.key) let av, bv if (fieldSort.col === 'key') { av = a.key; bv = b.key } else if (fieldSort.col === 'origin') { av = a.origins.join(','); bv = b.origins.join(',') } else if (fieldSort.col === 'type') { av = aSchema?.type || ''; bv = bSchema?.type || '' } else if (fieldSort.col === 'constraint') { av = constraintList.includes(a.key) ? 0 : 1; bv = constraintList.includes(b.key) ? 0 : 1 } else if (fieldSort.col === 'inview') { av = aSchema ? 0 : 1; bv = bSchema ? 0 : 1 } else if (fieldSort.col === 'seq') { av = aSchema?.seq ?? 999; bv = bSchema?.seq ?? 999 } if (av < bv) return fieldSort.dir === 'asc' ? -1 : 1 if (av > bv) return fieldSort.dir === 'asc' ? 1 : -1 return 0 }).map(f => { const isRaw = f.origins.includes('raw') const constraintChecked = constraintFields.split(',').map(s => s.trim()).includes(f.key) const schemaEntry = schemaFields.find(sf => sf.name === f.key) const inView = !!schemaEntry return ( ) })}
setFieldSort(s => ({ col, dir: s.col === col && s.dir === 'asc' ? 'desc' : 'asc' }))} className={`pb-1 font-medium cursor-pointer select-none hover:text-gray-600 ${center ? 'text-center' : ''}`} > {label} {fieldSort.col === col ? (fieldSort.dir === 'asc' ? '▲' : '▼') : '⇅'}
{f.key} {f.origins.join(', ')} {inView && (
setSchemaFields(sf => sf.map(s => s.name === f.key ? { ...s, expression: e.target.value || undefined } : s) )} />
)}
{isRaw && ( { const current = constraintFields.split(',').map(s => s.trim()).filter(Boolean) const next = e.target.checked ? [...current, f.key] : current.filter(k => k !== f.key) setConstraintFields(next.join(', ')) }} /> )} { if (e.target.checked) { const nextSeq = schemaFields.length > 0 ? Math.max(...schemaFields.map(s => s.seq ?? 0)) + 1 : 1 setSchemaFields(sf => [...sf, { name: f.key, type: 'text', seq: nextSeq }]) } else { setSchemaFields(sf => sf.filter(s => s.name !== f.key)) } }} /> {inView && ( setSchemaFields(sf => sf.map(s => s.name === f.key ? { ...s, seq: parseInt(e.target.value) || 0 } : s) )} /> )}
{schemaFields.length > 0 && ( <> {viewName && ( {viewName} )} )}
)} {/* Save button when no fields loaded yet */} {availableFields.length === 0 && (
)} {/* Reprocess */}
Clears and reruns all transformation rules
{result &&

{result}

} {error &&

{error}

}
)} {/* Create form */} {creating && (

New source

setForm(f => ({ ...f, name: e.target.value }))} placeholder="e.g. chase, dcard" />
{form.fields.length > 0 && (
{form.fields.map(f => { const schemaEntry = form.schema.find(s => s.name === f.name) const inView = !!schemaEntry const currentType = schemaEntry?.type || f.type return ( ) })}
Key Type Constraint In view Seq
{f.name} {inView && ( )} s.trim()).includes(f.name)} onChange={e => { const current = form.constraint_fields.split(',').map(s => s.trim()).filter(Boolean) const next = e.target.checked ? [...current, f.name] : current.filter(n => n !== f.name) setForm(ff => ({ ...ff, constraint_fields: next.join(', ') })) }} /> { if (e.target.checked) { const nextSeq = form.schema.length > 0 ? Math.max(...form.schema.map(s => s.seq ?? 0)) + 1 : 1 setForm(ff => ({ ...ff, schema: [...ff.schema, { name: f.name, type: f.type, seq: nextSeq }] })) } else { setForm(ff => ({ ...ff, schema: ff.schema.filter(s => s.name !== f.name) })) } }} /> {inView && ( setForm(ff => ({ ...ff, schema: ff.schema.map(s => s.name === f.name ? { ...s, seq: parseInt(e.target.value) || 0 } : s) }))} /> )}
)} {form.fields.length === 0 && (
setForm(f => ({ ...f, constraint_fields: e.target.value }))} placeholder="e.g. date, amount, description" />
)}
{form.fields.length > 0 && ( )}
{createError &&

{createError}

}
)}
) }