import { useState, useRef } from 'react' import { api } from '../api' export default function Remap() { const [search, setSearch] = useState('') const [results, setResults] = useState(null) const [searching, setSearching] = useState(false) const [selected, setSelected] = useState(null) // { col, val } const [matches, setMatches] = useState(null) // individual mappings const [loadingMatches, setLoadingMatches] = useState(false) const [toVal, setToVal] = useState('') const [applying, setApplying] = useState(false) const [msg, setMsg] = useState(null) // { text, ok } const searchRef = useRef() async function handleSearch(e) { e.preventDefault() const q = search.trim() if (!q) return setSearching(true) setResults(null) setSelected(null) setMatches(null) setMsg(null) try { const rows = await api.searchMappingOutputs(q) setResults(rows) } catch (err) { setMsg({ text: err.message, ok: false }) } finally { setSearching(false) } } async function handleSelect(row) { setSelected(row) setToVal(row.val) setMatches(null) setMsg(null) setLoadingMatches(true) try { const rows = await api.getMappingsByOutputField(row.col, row.val) setMatches(rows) } catch (err) { setMsg({ text: err.message, ok: false }) } finally { setLoadingMatches(false) } } async function handleApply() { if (!selected || !toVal.trim() || toVal === selected.val) return setApplying(true) setMsg(null) try { const { updated } = await api.remapOutputField(selected.col, selected.val, toVal.trim()) setMsg({ text: `Updated ${updated} mapping${updated !== 1 ? 's' : ''}.`, ok: true }) // Refresh match list to show new values const rows = await api.getMappingsByOutputField(selected.col, toVal.trim()) setMatches(rows) setSelected({ ...selected, val: toVal.trim() }) // Re-run search to refresh counts const refreshed = await api.searchMappingOutputs(search.trim()) setResults(refreshed) } catch (err) { setMsg({ text: err.message, ok: false }) } finally { setApplying(false) } } return (

Remap Output Values

{/* Search */}
setSearch(e.target.value)} placeholder="Search output values…" className="text-sm border border-gray-300 rounded px-3 py-1.5 w-72 focus:outline-none focus:border-blue-400" />
{/* Search results */} {results !== null && (
{results.length === 0 ? (

No matching output values found.

) : ( <>
{results.length} result{results.length !== 1 ? 's' : ''} — click one to remap
{results.map((r, i) => { const isActive = selected?.col === r.col && selected?.val === r.val return ( handleSelect(r)} className={`border-t border-gray-100 cursor-pointer transition-colors ${isActive ? 'bg-blue-50' : 'hover:bg-gray-50'}`}> ) })}
Field Value Mappings
{r.col} {r.val} {r.mapping_count}
)}
)} {/* Remap panel */} {selected && (
Remap {selected.col}
From
{selected.val}
To
setToVal(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleApply()} className="w-full text-sm font-mono border border-gray-300 rounded px-3 py-1.5 focus:outline-none focus:border-blue-400" />
{msg && (
{msg.text}
)} {/* Affected mappings */} {loadingMatches ? (

Loading…

) : matches && matches.length > 0 && (
Affected mappings
{matches.map(m => ( ))}
Source Rule Input Output
{m.source_name} {m.rule_name} {typeof m.input_value === 'string' ? m.input_value : JSON.stringify(m.input_value)} {Object.entries(m.output).map(([k, v]) => ( {k}: {v}{' '} ))}
)}
)}
) }