Compare commits

...

2 Commits

Author SHA1 Message Date
e425c32134 Fix period col dropdown dark mode: custom dropdown with theme-aware colors
Replaced native <select> (macOS ignores CSS on option elements) with a
custom button+ul dropdown. Background/text/border colors are applied via
useTheme so they respond correctly to dark mode toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 00:48:25 -04:00
52be043c1d Add dim-period/cols endpoint and replace dim_period_col text input with select
GET /api/dim-period/cols queries information_schema for pf.dim_period columns
(excluding sdat/edat/drange/ndays) so the UI always reflects actual columns.

Setup col_meta editor now shows a dropdown populated from that endpoint instead
of a free-text field, preventing invalid column names like the cash source had.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 00:17:22 -04:00
3 changed files with 66 additions and 10 deletions

View File

@ -280,6 +280,22 @@ module.exports = function(pool) {
}); });
// deregister a source — does not drop existing forecast tables // deregister a source — does not drop existing forecast tables
router.get('/dim-period/cols', async (req, res) => {
try {
const result = await pool.query(`
SELECT column_name
FROM information_schema.columns
WHERE table_schema = 'pf' AND table_name = 'dim_period'
AND column_name NOT IN ('sdat', 'edat', 'drange', 'ndays')
ORDER BY ordinal_position
`);
res.json(result.rows.map(r => r.column_name));
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
});
router.delete('/sources/:id', async (req, res) => { router.delete('/sources/:id', async (req, res) => {
try { try {
const result = await pool.query( const result = await pool.query(

View File

@ -89,5 +89,6 @@ body { margin: 0; background-color: var(--bg-primary); color: var(--text-primary
.dark ::selection { background-color: var(--accent-bg); color: var(--text-primary); } .dark ::selection { background-color: var(--accent-bg); color: var(--text-primary); }
.dark input { background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color); } .dark input { background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color); }
.dark select { background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color); } .dark select { background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color); }
.dark select option { background-color: var(--bg-secondary); color: var(--text-primary); }
.dark textarea { background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color); } .dark textarea { background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color); }
.dark .bg-transparent { background-color: transparent; } .dark .bg-transparent { background-color: transparent; }

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import useTheme from '../theme.jsx'
const ROLES = ['ignore', 'dimension', 'value', 'units', 'date', 'filter'] const ROLES = ['ignore', 'dimension', 'value', 'units', 'date', 'filter']
@ -12,6 +13,7 @@ const ROLE_STYLE = {
} }
export default function Setup({ refreshSources }) { export default function Setup({ refreshSources }) {
const { dark } = useTheme()
const [tables, setTables] = useState([]) const [tables, setTables] = useState([])
const [sources, setSources] = useState([]) const [sources, setSources] = useState([])
const [selectedSource, setSelectedSource] = useState(null) const [selectedSource, setSelectedSource] = useState(null)
@ -24,12 +26,26 @@ export default function Setup({ refreshSources }) {
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [generating, setGenerating] = useState(false) const [generating, setGenerating] = useState(false)
const [msg, setMsg] = useState(null) const [msg, setMsg] = useState(null)
const [dimPeriodCols, setDimPeriodCols] = useState([])
const [openPeriodIdx, setOpenPeriodIdx] = useState(null)
const periodDropRef = useRef(null)
useEffect(() => { useEffect(() => {
fetch('/api/tables').then(r => r.json()).then(setTables).catch(console.error) fetch('/api/tables').then(r => r.json()).then(setTables).catch(console.error)
fetch('/api/dim-period/cols').then(r => r.json()).then(setDimPeriodCols).catch(console.error)
loadSources() loadSources()
}, []) }, [])
useEffect(() => {
if (openPeriodIdx === null) return
function handleClick(e) {
if (periodDropRef.current && !periodDropRef.current.contains(e.target))
setOpenPeriodIdx(null)
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [openPeriodIdx])
function loadSources() { function loadSources() {
fetch('/api/sources').then(r => r.json()).then(data => { fetch('/api/sources').then(r => r.json()).then(data => {
setSources(data) setSources(data)
@ -323,15 +339,38 @@ export default function Setup({ refreshSources }) {
className="border border-transparent hover:border-gray-200 focus:border-gray-300 rounded px-1.5 py-0.5 w-full outline-none bg-transparent disabled:opacity-20 disabled:cursor-default" className="border border-transparent hover:border-gray-200 focus:border-gray-300 rounded px-1.5 py-0.5 w-full outline-none bg-transparent disabled:opacity-20 disabled:cursor-default"
/> />
</td> </td>
<td className="px-3 py-1.5"> <td className="px-3 py-1.5 relative">
<input {(() => {
type="text" const disabled = col.role !== 'dimension' || !col.dim_group
value={col.dim_period_col || ''} const isOpen = openPeriodIdx === i
onChange={e => updateCol(i, 'dim_period_col', e.target.value || null)} return (
placeholder="—" <div ref={isOpen ? periodDropRef : null} className="relative">
disabled={col.role !== 'dimension' || !col.dim_group} <button
className="border border-transparent hover:border-gray-200 focus:border-gray-300 rounded px-1.5 py-0.5 w-full outline-none bg-transparent disabled:opacity-20 disabled:cursor-default font-mono text-xs" type="button"
/> disabled={disabled}
onClick={() => setOpenPeriodIdx(isOpen ? null : i)}
className="text-xs px-1.5 py-0.5 rounded border border-transparent hover:border-gray-200 outline-none bg-white text-gray-700 disabled:opacity-20 disabled:cursor-default font-mono w-full text-left"
>
{col.dim_period_col || '—'}
</button>
{isOpen && (
<ul style={{ backgroundColor: dark ? '#2a2c2f' : '#ffffff', color: dark ? '#c5c9d0' : '#374151', borderColor: dark ? '#3b3f46' : '#e5e7eb' }} className="absolute z-50 left-0 top-full mt-0.5 min-w-full border rounded shadow-lg text-xs font-mono max-h-48 overflow-y-auto">
<li
className="px-2 py-1 cursor-pointer hover:bg-gray-100"
onMouseDown={() => { updateCol(i, 'dim_period_col', null); setOpenPeriodIdx(null) }}
></li>
{dimPeriodCols.map(c => (
<li
key={c}
className={`px-2 py-1 cursor-pointer hover:bg-gray-100 ${col.dim_period_col === c ? 'bg-blue-50 text-blue-700' : ''}`}
onMouseDown={() => { updateCol(i, 'dim_period_col', c); setOpenPeriodIdx(null) }}
>{c}</li>
))}
</ul>
)}
</div>
)
})()}
</td> </td>
<td className="px-3 py-1.5"> <td className="px-3 py-1.5">
<input <input