diff --git a/lib/sql_generator.js b/lib/sql_generator.js index f4c3e48..90ea433 100644 --- a/lib/sql_generator.js +++ b/lib/sql_generator.js @@ -49,6 +49,9 @@ function generateSQL(source, colMeta) { } function buildBaseline() { + const baselineSelect = dataCols.map(c => + c === dateCol ? `(${q(c)} + '{{date_offset}}'::interval)::date` : q(c) + ).join(', '); return ` WITH ilog AS ( @@ -61,7 +64,7 @@ ilog AS ( ) ,ins AS ( INSERT INTO {{fc_table}} (${insertCols}) - SELECT ${selectData}, 'baseline', (SELECT id FROM ilog), '{{pf_user}}', now() + SELECT ${baselineSelect}, 'baseline', (SELECT id FROM ilog), '{{pf_user}}', now() FROM ${srcTable} WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}' RETURNING * diff --git a/public/app.js b/public/app.js index 287a5cd..71ecacf 100644 --- a/public/app.js +++ b/public/app.js @@ -375,10 +375,16 @@ function showLoadForm(op) { state.loadDataOp = op; document.getElementById('load-data-title').textContent = op === 'baseline' ? 'Load Baseline' : 'Load Reference'; - document.getElementById('load-date-from').value = ''; - document.getElementById('load-date-to').value = ''; - document.getElementById('load-note').value = ''; + document.getElementById('load-date-from').value = ''; + document.getElementById('load-date-to').value = ''; + document.getElementById('load-offset-years').value = '0'; + document.getElementById('load-offset-months').value = '0'; + document.getElementById('load-note').value = ''; document.getElementById('load-date-preview').classList.add('hidden'); + + const showOffset = op === 'baseline'; + document.getElementById('load-offset-fields').classList.toggle('hidden', !showOffset); + document.getElementById('load-data-modal').classList.remove('hidden'); document.getElementById('load-date-from').focus(); } @@ -387,31 +393,58 @@ function hideLoadModal() { document.getElementById('load-data-modal').classList.add('hidden'); } -function updateDatePreview() { - const fromVal = document.getElementById('load-date-from').value; - const toVal = document.getElementById('load-date-to').value; - const preview = document.getElementById('load-date-preview'); - const chips = document.getElementById('load-date-chips'); - const label = preview.querySelector('.load-preview-label'); - - if (!fromVal || !toVal) { preview.classList.add('hidden'); return; } - +function buildMonthList(fromVal, toVal) { const from = new Date(fromVal + 'T00:00:00'); const to = new Date(toVal + 'T00:00:00'); - if (isNaN(from) || isNaN(to) || from > to) { preview.classList.add('hidden'); return; } - + if (isNaN(from) || isNaN(to) || from > to) return null; const months = []; const cur = new Date(from.getFullYear(), from.getMonth(), 1); const end = new Date(to.getFullYear(), to.getMonth(), 1); while (cur <= end) { months.push(new Date(cur)); cur.setMonth(cur.getMonth() + 1); } + return months; +} + +function renderChips(months, fmt) { + if (months.length <= 36) { + return months.map(m => `${fmt.format(m)}`).join(''); + } + return `${months.length} months — ${fmt.format(months[0])} → ${fmt.format(months[months.length - 1])}`; +} + +function updateDatePreview() { + const fromVal = document.getElementById('load-date-from').value; + const toVal = document.getElementById('load-date-to').value; + const preview = document.getElementById('load-date-preview'); + const simple = document.getElementById('load-preview-simple'); + const offset = document.getElementById('load-preview-offset'); + + if (!fromVal || !toVal) { preview.classList.add('hidden'); return; } + + const months = buildMonthList(fromVal, toVal); + if (!months) { preview.classList.add('hidden'); return; } const fmt = new Intl.DateTimeFormat('en-US', { month: 'short', year: 'numeric' }); - label.textContent = `${months.length} month${months.length !== 1 ? 's' : ''} covered`; - if (months.length <= 36) { - chips.innerHTML = months.map(m => `${fmt.format(m)}`).join(''); + if (state.loadDataOp === 'baseline') { + const years = parseInt(document.getElementById('load-offset-years').value) || 0; + const mths = parseInt(document.getElementById('load-offset-months').value) || 0; + const projected = months.map(d => { + const p = new Date(d); + p.setFullYear(p.getFullYear() + years); + p.setMonth(p.getMonth() + mths); + return p; + }); + + document.getElementById('load-chips-source').innerHTML = renderChips(months, fmt); + document.getElementById('load-chips-projected').innerHTML = renderChips(projected, fmt); + simple.classList.add('hidden'); + offset.classList.remove('hidden'); } else { - chips.innerHTML = `${months.length} months — ${fmt.format(months[0])} → ${fmt.format(months[months.length - 1])}`; + simple.querySelector('.load-preview-label').textContent = + `${months.length} month${months.length !== 1 ? 's' : ''} covered`; + document.getElementById('load-date-chips').innerHTML = renderChips(months, fmt); + offset.classList.add('hidden'); + simple.classList.remove('hidden'); } preview.classList.remove('hidden'); @@ -429,6 +462,15 @@ async function submitLoadData() { note: document.getElementById('load-note').value.trim() || undefined }; + if (state.loadDataOp === 'baseline') { + const years = parseInt(document.getElementById('load-offset-years').value) || 0; + const months = parseInt(document.getElementById('load-offset-months').value) || 0; + const parts = []; + if (years) parts.push(`${years} year${years !== 1 ? 's' : ''}`); + if (months) parts.push(`${months} month${months !== 1 ? 's' : ''}`); + body.date_offset = parts.length ? parts.join(' ') : '0 days'; + } + try { showStatus(`Loading ${state.loadDataOp}...`, 'info'); const result = await api('POST', `/versions/${state.selectedVersionId}/${state.loadDataOp}`, body); @@ -923,6 +965,8 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('btn-load-close').addEventListener('click', hideLoadModal); document.getElementById('load-date-from').addEventListener('change', updateDatePreview); document.getElementById('load-date-to').addEventListener('change', updateDatePreview); + document.getElementById('load-offset-years').addEventListener('input', updateDatePreview); + document.getElementById('load-offset-months').addEventListener('input', updateDatePreview); // forecast view buttons document.getElementById('btn-forecast-refresh').addEventListener('click', loadForecastData); diff --git a/public/index.html b/public/index.html index 1f06ac2..6be4881 100644 --- a/public/index.html +++ b/public/index.html @@ -169,11 +169,34 @@
+
+
+ + +
+