Compare commits
4 Commits
master
...
incrementa
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d8b052eb6 | |||
| ddd16bc7a0 | |||
| 10441a4761 | |||
| cfee3e96b9 |
@ -49,6 +49,9 @@ function generateSQL(source, colMeta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildBaseline() {
|
function buildBaseline() {
|
||||||
|
const baselineSelect = dataCols.map(c =>
|
||||||
|
c === dateCol ? `(${q(c)} + '{{date_offset}}'::interval)::date` : q(c)
|
||||||
|
).join(', ');
|
||||||
return `
|
return `
|
||||||
WITH
|
WITH
|
||||||
ilog AS (
|
ilog AS (
|
||||||
@ -61,7 +64,7 @@ ilog AS (
|
|||||||
)
|
)
|
||||||
,ins AS (
|
,ins AS (
|
||||||
INSERT INTO {{fc_table}} (${insertCols})
|
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}
|
FROM ${srcTable}
|
||||||
WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}'
|
WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}'
|
||||||
RETURNING *
|
RETURNING *
|
||||||
@ -84,7 +87,7 @@ ilog AS (
|
|||||||
WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}'
|
WHERE ${q(dateCol)} BETWEEN '{{date_from}}' AND '{{date_to}}'
|
||||||
RETURNING *
|
RETURNING *
|
||||||
)
|
)
|
||||||
SELECT count(*) AS rows_affected FROM ins`.trim();
|
SELECT * FROM ins`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildScale() {
|
function buildScale() {
|
||||||
@ -124,7 +127,7 @@ ilog AS (
|
|||||||
FROM base
|
FROM base
|
||||||
RETURNING *
|
RETURNING *
|
||||||
)
|
)
|
||||||
SELECT count(*) AS rows_affected FROM ins`.trim();
|
SELECT * FROM ins`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRecode() {
|
function buildRecode() {
|
||||||
@ -146,16 +149,16 @@ ilog AS (
|
|||||||
SELECT ${dimsJoined}, ${q(dateCol)}, ${effectiveValue ? `-${q(effectiveValue)}` : '0'}, ${effectiveUnits ? `-${q(effectiveUnits)}` : '0'},
|
SELECT ${dimsJoined}, ${q(dateCol)}, ${effectiveValue ? `-${q(effectiveValue)}` : '0'}, ${effectiveUnits ? `-${q(effectiveUnits)}` : '0'},
|
||||||
'recode', (SELECT id FROM ilog), '{{pf_user}}', now()
|
'recode', (SELECT id FROM ilog), '{{pf_user}}', now()
|
||||||
FROM src
|
FROM src
|
||||||
RETURNING id
|
RETURNING *
|
||||||
)
|
)
|
||||||
,ins AS (
|
,ins AS (
|
||||||
INSERT INTO {{fc_table}} (${insertCols})
|
INSERT INTO {{fc_table}} (${insertCols})
|
||||||
SELECT {{set_clause}}, ${q(dateCol)}, ${effectiveValue ? q(effectiveValue) : '0'}, ${effectiveUnits ? q(effectiveUnits) : '0'},
|
SELECT {{set_clause}}, ${q(dateCol)}, ${effectiveValue ? q(effectiveValue) : '0'}, ${effectiveUnits ? q(effectiveUnits) : '0'},
|
||||||
'recode', (SELECT id FROM ilog), '{{pf_user}}', now()
|
'recode', (SELECT id FROM ilog), '{{pf_user}}', now()
|
||||||
FROM src
|
FROM src
|
||||||
RETURNING id
|
RETURNING *
|
||||||
)
|
)
|
||||||
SELECT (SELECT count(*) FROM neg) + (SELECT count(*) FROM ins) AS rows_affected`.trim();
|
SELECT * FROM neg UNION ALL SELECT * FROM ins`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildClone() {
|
function buildClone() {
|
||||||
@ -179,7 +182,7 @@ ilog AS (
|
|||||||
{{exclude_clause}}
|
{{exclude_clause}}
|
||||||
RETURNING *
|
RETURNING *
|
||||||
)
|
)
|
||||||
SELECT count(*) AS rows_affected FROM ins`.trim();
|
SELECT * FROM ins`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUndo() {
|
function buildUndo() {
|
||||||
|
|||||||
121
public/app.js
121
public/app.js
@ -375,10 +375,81 @@ function showLoadForm(op) {
|
|||||||
state.loadDataOp = op;
|
state.loadDataOp = op;
|
||||||
document.getElementById('load-data-title').textContent =
|
document.getElementById('load-data-title').textContent =
|
||||||
op === 'baseline' ? 'Load Baseline' : 'Load Reference';
|
op === 'baseline' ? 'Load Baseline' : 'Load Reference';
|
||||||
document.getElementById('load-data-form').classList.remove('hidden');
|
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();
|
document.getElementById('load-date-from').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hideLoadModal() {
|
||||||
|
document.getElementById('load-data-modal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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 => `<span class="date-chip">${fmt.format(m)}</span>`).join('');
|
||||||
|
}
|
||||||
|
return `<span class="date-chip-summary">${months.length} months — ${fmt.format(months[0])} → ${fmt.format(months[months.length - 1])}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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' });
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
async function submitLoadData() {
|
async function submitLoadData() {
|
||||||
const date_from = document.getElementById('load-date-from').value;
|
const date_from = document.getElementById('load-date-from').value;
|
||||||
const date_to = document.getElementById('load-date-to').value;
|
const date_to = document.getElementById('load-date-to').value;
|
||||||
@ -391,10 +462,19 @@ async function submitLoadData() {
|
|||||||
note: document.getElementById('load-note').value.trim() || undefined
|
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 {
|
try {
|
||||||
showStatus(`Loading ${state.loadDataOp}...`, 'info');
|
showStatus(`Loading ${state.loadDataOp}...`, 'info');
|
||||||
const result = await api('POST', `/versions/${state.selectedVersionId}/${state.loadDataOp}`, body);
|
const result = await api('POST', `/versions/${state.selectedVersionId}/${state.loadDataOp}`, body);
|
||||||
document.getElementById('load-data-form').classList.add('hidden');
|
hideLoadModal();
|
||||||
showStatus(`${state.loadDataOp} loaded — ${result.rows_affected} rows`, 'success');
|
showStatus(`${state.loadDataOp} loaded — ${result.rows_affected} rows`, 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showStatus(err.message, 'error');
|
showStatus(err.message, 'error');
|
||||||
@ -451,6 +531,17 @@ function openForecast() {
|
|||||||
/* ============================================================
|
/* ============================================================
|
||||||
FORECAST VIEW — data loading
|
FORECAST VIEW — data loading
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
function parseNumericRows(rows) {
|
||||||
|
const numericCols = state.colMeta
|
||||||
|
.filter(c => c.role === 'value' || c.role === 'units')
|
||||||
|
.map(c => c.cname);
|
||||||
|
return rows.map(row => {
|
||||||
|
const r = { ...row };
|
||||||
|
numericCols.forEach(col => { if (r[col] != null) r[col] = parseFloat(r[col]); });
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadForecastData() {
|
async function loadForecastData() {
|
||||||
if (!state.version) return;
|
if (!state.version) return;
|
||||||
document.getElementById('forecast-label').textContent =
|
document.getElementById('forecast-label').textContent =
|
||||||
@ -462,14 +553,7 @@ async function loadForecastData() {
|
|||||||
}
|
}
|
||||||
showStatus('Loading forecast data...', 'info');
|
showStatus('Loading forecast data...', 'info');
|
||||||
const rawData = await api('GET', `/versions/${state.version.id}/data`);
|
const rawData = await api('GET', `/versions/${state.version.id}/data`);
|
||||||
const numericCols = state.colMeta
|
const data = parseNumericRows(rawData);
|
||||||
.filter(c => c.role === 'value' || c.role === 'units')
|
|
||||||
.map(c => c.cname);
|
|
||||||
const data = rawData.map(row => {
|
|
||||||
const r = { ...row };
|
|
||||||
numericCols.forEach(col => { if (r[col] != null) r[col] = parseFloat(r[col]); });
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
initPivotGrid(data);
|
initPivotGrid(data);
|
||||||
showStatus(`Loaded ${data.length.toLocaleString()} rows`, 'success');
|
showStatus(`Loaded ${data.length.toLocaleString()} rows`, 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -695,7 +779,7 @@ async function submitScale() {
|
|||||||
document.getElementById('scale-value-incr').value = '';
|
document.getElementById('scale-value-incr').value = '';
|
||||||
document.getElementById('scale-units-incr').value = '';
|
document.getElementById('scale-units-incr').value = '';
|
||||||
document.getElementById('scale-note').value = '';
|
document.getElementById('scale-note').value = '';
|
||||||
await loadForecastData();
|
state.grids.pivot.applyTransaction({ add: parseNumericRows(result.rows) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showStatus(err.message, 'error');
|
showStatus(err.message, 'error');
|
||||||
}
|
}
|
||||||
@ -721,7 +805,7 @@ async function submitRecode() {
|
|||||||
});
|
});
|
||||||
showStatus(`Recode applied — ${result.rows_affected} rows inserted`, 'success');
|
showStatus(`Recode applied — ${result.rows_affected} rows inserted`, 'success');
|
||||||
document.querySelectorAll('#recode-fields input[data-col], #recode-fields select[data-col]').forEach(i => { i.value = ''; });
|
document.querySelectorAll('#recode-fields input[data-col], #recode-fields select[data-col]').forEach(i => { i.value = ''; });
|
||||||
await loadForecastData();
|
state.grids.pivot.applyTransaction({ add: parseNumericRows(result.rows) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showStatus(err.message, 'error');
|
showStatus(err.message, 'error');
|
||||||
}
|
}
|
||||||
@ -750,7 +834,7 @@ async function submitClone() {
|
|||||||
showStatus(`Clone applied — ${result.rows_affected} rows inserted`, 'success');
|
showStatus(`Clone applied — ${result.rows_affected} rows inserted`, 'success');
|
||||||
document.querySelectorAll('#clone-fields input[data-col], #clone-fields select[data-col]').forEach(i => { i.value = ''; });
|
document.querySelectorAll('#clone-fields input[data-col], #clone-fields select[data-col]').forEach(i => { i.value = ''; });
|
||||||
document.getElementById('clone-scale').value = '1';
|
document.getElementById('clone-scale').value = '1';
|
||||||
await loadForecastData();
|
state.grids.pivot.applyTransaction({ add: parseNumericRows(result.rows) });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showStatus(err.message, 'error');
|
showStatus(err.message, 'error');
|
||||||
}
|
}
|
||||||
@ -780,6 +864,8 @@ function renderLogGrid(logs) {
|
|||||||
{ field: 'operation', headerName: 'Operation', width: 90 },
|
{ field: 'operation', headerName: 'Operation', width: 90 },
|
||||||
{ field: 'slice', headerName: 'Slice', flex: 1,
|
{ field: 'slice', headerName: 'Slice', flex: 1,
|
||||||
valueFormatter: p => p.value ? JSON.stringify(p.value) : '' },
|
valueFormatter: p => p.value ? JSON.stringify(p.value) : '' },
|
||||||
|
{ field: 'params', headerName: 'Params', flex: 1,
|
||||||
|
valueFormatter: p => p.value ? JSON.stringify(p.value) : '' },
|
||||||
{ field: 'note', headerName: 'Note', flex: 1 },
|
{ field: 'note', headerName: 'Note', flex: 1 },
|
||||||
{
|
{
|
||||||
headerName: '',
|
headerName: '',
|
||||||
@ -875,9 +961,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.getElementById('vbtn-toggle').addEventListener('click', toggleVersionStatus);
|
document.getElementById('vbtn-toggle').addEventListener('click', toggleVersionStatus);
|
||||||
document.getElementById('vbtn-delete').addEventListener('click', deleteVersion);
|
document.getElementById('vbtn-delete').addEventListener('click', deleteVersion);
|
||||||
document.getElementById('btn-load-submit').addEventListener('click', submitLoadData);
|
document.getElementById('btn-load-submit').addEventListener('click', submitLoadData);
|
||||||
document.getElementById('btn-load-cancel').addEventListener('click', () => {
|
document.getElementById('btn-load-cancel').addEventListener('click', hideLoadModal);
|
||||||
document.getElementById('load-data-form').classList.add('hidden');
|
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
|
// forecast view buttons
|
||||||
document.getElementById('btn-forecast-refresh').addEventListener('click', loadForecastData);
|
document.getElementById('btn-forecast-refresh').addEventListener('click', loadForecastData);
|
||||||
|
|||||||
@ -94,18 +94,6 @@
|
|||||||
<button class="btn" id="vbtn-toggle">Close Version</button>
|
<button class="btn" id="vbtn-toggle">Close Version</button>
|
||||||
<button class="btn btn-danger" id="vbtn-delete">Delete</button>
|
<button class="btn btn-danger" id="vbtn-delete">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="load-data-form" class="inline-form hidden">
|
|
||||||
<h3 id="load-data-title">Load Baseline</h3>
|
|
||||||
<div class="form-row">
|
|
||||||
<label>Date From<input type="date" id="load-date-from" /></label>
|
|
||||||
<label>Date To<input type="date" id="load-date-to" /></label>
|
|
||||||
<label>Note<input type="text" id="load-note" placeholder="optional" /></label>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button id="btn-load-submit" class="btn btn-primary">Load</button>
|
|
||||||
<button id="btn-load-cancel" class="btn">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== FORECAST VIEW ===== -->
|
<!-- ===== FORECAST VIEW ===== -->
|
||||||
@ -170,6 +158,54 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Load baseline / reference modal -->
|
||||||
|
<div id="load-data-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal load-data-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span id="load-data-title">Load Baseline</span>
|
||||||
|
<button id="btn-load-close" class="btn-icon">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="load-data-body">
|
||||||
|
<div class="load-form-fields">
|
||||||
|
<label>Date From<input type="date" id="load-date-from" /></label>
|
||||||
|
<label>Date To<input type="date" id="load-date-to" /></label>
|
||||||
|
<div id="load-offset-fields">
|
||||||
|
<div class="load-offset-row">
|
||||||
|
<label>Offset Years<input type="number" id="load-offset-years" min="0" value="0" /></label>
|
||||||
|
<label>Offset Months<input type="number" id="load-offset-months" min="0" value="0" /></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label>Note<input type="text" id="load-note" placeholder="optional" /></label>
|
||||||
|
</div>
|
||||||
|
<div id="load-date-preview" class="load-date-preview hidden">
|
||||||
|
<!-- reference: single chip list -->
|
||||||
|
<div id="load-preview-simple">
|
||||||
|
<div class="load-preview-label"></div>
|
||||||
|
<div id="load-date-chips" class="date-chips"></div>
|
||||||
|
</div>
|
||||||
|
<!-- baseline: before → after -->
|
||||||
|
<div id="load-preview-offset" class="hidden">
|
||||||
|
<div class="load-preview-columns">
|
||||||
|
<div class="load-preview-col">
|
||||||
|
<div class="load-preview-label">Source</div>
|
||||||
|
<div id="load-chips-source" class="date-chips"></div>
|
||||||
|
</div>
|
||||||
|
<div class="load-preview-arrow">→</div>
|
||||||
|
<div class="load-preview-col">
|
||||||
|
<div class="load-preview-label">Projected</div>
|
||||||
|
<div id="load-chips-projected" class="date-chips"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="btn-load-submit" class="btn btn-primary">Load</button>
|
||||||
|
<button id="btn-load-cancel" class="btn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Table preview modal -->
|
<!-- Table preview modal -->
|
||||||
<div id="modal-overlay" class="modal-overlay hidden">
|
<div id="modal-overlay" class="modal-overlay hidden">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
|
|||||||
@ -312,6 +312,99 @@ body {
|
|||||||
#modal-body { padding: 16px 18px; overflow-y: auto; flex: 1; font-size: 12px; }
|
#modal-body { padding: 16px 18px; overflow-y: auto; flex: 1; font-size: 12px; }
|
||||||
.modal-footer { padding: 10px 18px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 8px; }
|
.modal-footer { padding: 10px 18px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 8px; }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
LOAD BASELINE / REFERENCE MODAL
|
||||||
|
============================================================ */
|
||||||
|
.load-data-modal { width: 480px; max-height: 80vh; }
|
||||||
|
|
||||||
|
#load-data-body {
|
||||||
|
padding: 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-form-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-form-fields label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #555;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-form-fields input[type=date],
|
||||||
|
.load-form-fields input[type=text] {
|
||||||
|
border: 1px solid #dce1e7;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-form-fields input[type=date]:focus,
|
||||||
|
.load-form-fields input[type=text]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2980b9;
|
||||||
|
box-shadow: 0 0 0 2px rgba(41,128,185,.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-offset-row { display: flex; gap: 12px; }
|
||||||
|
.load-offset-row label { flex: 1; }
|
||||||
|
|
||||||
|
.load-date-preview { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.load-date-preview.hidden { display: none; }
|
||||||
|
|
||||||
|
.load-preview-columns {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.load-preview-col { flex: 1; display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.load-preview-arrow {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #aaa;
|
||||||
|
padding-top: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-preview-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #7f8c8d;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-chip {
|
||||||
|
background: #eaf4fb;
|
||||||
|
color: #1a6fa8;
|
||||||
|
border: 1px solid #c5dff0;
|
||||||
|
padding: 3px 9px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-chip-summary {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.preview-section h4 { font-size: 12px; margin-bottom: 6px; color: #555; }
|
.preview-section h4 { font-size: 12px; margin-bottom: 6px; color: #555; }
|
||||||
.preview-section + .preview-section { margin-top: 16px; }
|
.preview-section + .preview-section { margin-top: 16px; }
|
||||||
.preview-table { border-collapse: collapse; width: 100%; font-size: 11px; }
|
.preview-table { border-collapse: collapse; width: 100%; font-size: 11px; }
|
||||||
|
|||||||
@ -77,29 +77,31 @@ module.exports = function(pool) {
|
|||||||
// load baseline rows from source table for a date range
|
// load baseline rows from source table for a date range
|
||||||
// deletes existing iter='baseline' rows before inserting (handled inside stored SQL)
|
// deletes existing iter='baseline' rows before inserting (handled inside stored SQL)
|
||||||
router.post('/versions/:id/baseline', async (req, res) => {
|
router.post('/versions/:id/baseline', async (req, res) => {
|
||||||
const { date_from, date_to, pf_user, note, replay } = req.body;
|
const { date_from, date_to, date_offset, pf_user, note, replay } = req.body;
|
||||||
if (!date_from || !date_to) {
|
if (!date_from || !date_to) {
|
||||||
return res.status(400).json({ error: 'date_from and date_to are required' });
|
return res.status(400).json({ error: 'date_from and date_to are required' });
|
||||||
}
|
}
|
||||||
if (replay) {
|
if (replay) {
|
||||||
return res.status(501).json({ error: 'replay is not yet implemented' });
|
return res.status(501).json({ error: 'replay is not yet implemented' });
|
||||||
}
|
}
|
||||||
|
const dateOffset = date_offset || '0 days';
|
||||||
try {
|
try {
|
||||||
const ctx = await getContext(parseInt(req.params.id), 'baseline');
|
const ctx = await getContext(parseInt(req.params.id), 'baseline');
|
||||||
if (!guardOpen(ctx.version, res)) return;
|
if (!guardOpen(ctx.version, res)) return;
|
||||||
|
|
||||||
const sql = applyTokens(ctx.sql, {
|
const sql = applyTokens(ctx.sql, {
|
||||||
fc_table: ctx.table,
|
fc_table: ctx.table,
|
||||||
version_id: ctx.version.id,
|
version_id: ctx.version.id,
|
||||||
pf_user: esc(pf_user || ''),
|
pf_user: esc(pf_user || ''),
|
||||||
note: esc(note || ''),
|
note: esc(note || ''),
|
||||||
params: esc(JSON.stringify({ date_from, date_to })),
|
params: esc(JSON.stringify({ date_from, date_to, date_offset: dateOffset })),
|
||||||
date_from: esc(date_from),
|
date_from: esc(date_from),
|
||||||
date_to: esc(date_to)
|
date_to: esc(date_to),
|
||||||
|
date_offset: esc(dateOffset)
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await runSQL(sql);
|
const result = await runSQL(sql);
|
||||||
res.json(result.rows[0]);
|
res.json({ rows_affected: result.rows.length });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err.status || 500).json({ error: err.message });
|
res.status(err.status || 500).json({ error: err.message });
|
||||||
@ -127,7 +129,7 @@ module.exports = function(pool) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await runSQL(sql);
|
const result = await runSQL(sql);
|
||||||
res.json(result.rows[0]);
|
res.json({ rows: result.rows, rows_affected: result.rows.length });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err.status || 500).json({ error: err.message });
|
res.status(err.status || 500).json({ error: err.message });
|
||||||
@ -183,7 +185,7 @@ module.exports = function(pool) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await runSQL(sql);
|
const result = await runSQL(sql);
|
||||||
res.json(result.rows[0]);
|
res.json({ rows: result.rows, rows_affected: result.rows.length });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err.status || 500).json({ error: err.message });
|
res.status(err.status || 500).json({ error: err.message });
|
||||||
@ -218,7 +220,7 @@ module.exports = function(pool) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await runSQL(sql);
|
const result = await runSQL(sql);
|
||||||
res.json(result.rows[0]);
|
res.json({ rows: result.rows, rows_affected: result.rows.length });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err.status || 500).json({ error: err.message });
|
res.status(err.status || 500).json({ error: err.message });
|
||||||
@ -255,7 +257,7 @@ module.exports = function(pool) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await runSQL(sql);
|
const result = await runSQL(sql);
|
||||||
res.json(result.rows[0]);
|
res.json({ rows: result.rows, rows_affected: result.rows.length });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err.status || 500).json({ error: err.message });
|
res.status(err.status || 500).json({ error: err.message });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user