Drop Shift years
Nobody asked for it. It was my own addition to the plans panel, guessing that cloning a definition forward a year would be the common case; the actual edit wanted was two dates moved by five days, typed by hand. A bulk rewrite of filter values is a lot of machinery to leave sitting next to Run all on the chance it is ever right. Never used, so nothing to unwind: every plan on the cloned version still carries the template's filters verbatim apart from the two dates edited in the form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
738e177793
commit
fe456d4e90
10
CLAUDE.md
10
CLAUDE.md
@ -31,7 +31,7 @@ routes/
|
||||
operations.js scale, recode, clone, undo — the core forecast ops
|
||||
log.js GET /api/versions/:id/log, DELETE /api/log/:logid
|
||||
layouts.js Named pivot layouts — list per version, create, patch, delete
|
||||
plans.js Planned load segments — list, create, patch, delete, shift-years
|
||||
plans.js Planned load segments — list, create, patch, delete
|
||||
lib/
|
||||
sql_generator.js buildFilterClause, token substitution helpers
|
||||
auth.js scrypt hash/verify, requireAuth, sessionUser; `node lib/auth.js hash` CLI
|
||||
@ -376,14 +376,6 @@ from its `baseline`/`reference` log entries when it does not — which is every
|
||||
version built before plans existed. `pf.log.params` already stores `filters`,
|
||||
`raw_where` and `date_offset`, so this is a read, not a reconstruction.
|
||||
|
||||
**`POST /versions/:id/plans/shift-years`** adds a whole number of years to every
|
||||
*unrun* plan's `date_offset` and to any bare four-digit `20xx` in its filter
|
||||
**values**. Values only, and only a bare year: a season reads `2027`, a month
|
||||
reads `04 - Sep`, and a part number that happens to look like a year would be
|
||||
corrupted by anything looser. Cloning a definition forward a year is the reason
|
||||
plans exist, and doing it by hand across eight segments is where the mistakes
|
||||
come from.
|
||||
|
||||
**Run all stops at the first failure.** The segments of a definition are not
|
||||
independent — a baseline followed by references that date-shift off it — so
|
||||
carrying on past a broken one builds something nobody described.
|
||||
|
||||
@ -121,64 +121,5 @@ module.exports = function(pool) {
|
||||
}
|
||||
});
|
||||
|
||||
// Shift every unrun plan's date_offset and any four-digit year in its filter
|
||||
// values by a whole number of years. Cloning a definition forward is the
|
||||
// reason plans exist, and doing it by hand across eight segments is where
|
||||
// the mistakes come from.
|
||||
//
|
||||
// Years in filter *values* only, and only a bare four-digit 20xx: a season
|
||||
// reads "2027", a month reads "04 - Sep", and a part number that happens to
|
||||
// look like a year would be corrupted by anything looser.
|
||||
router.post('/versions/:id/plans/shift-years', async (req, res) => {
|
||||
const versionId = parseInt(req.params.id);
|
||||
const years = parseInt(req.body?.years);
|
||||
if (!Number.isInteger(years) || years === 0) {
|
||||
return res.status(400).json({ error: 'years must be a non-zero whole number' });
|
||||
}
|
||||
const bumpYear = (v) => (typeof v === 'string' && /^20\d{2}$/.test(v.trim()))
|
||||
? String(parseInt(v, 10) + years)
|
||||
: v;
|
||||
const bumpOffset = (off) => {
|
||||
// '0 days' stays put: an offset of nothing is not a date to shift.
|
||||
const m = /^\s*(-?\d+)\s+year/.exec(off || '');
|
||||
if (!m) return off;
|
||||
return `${parseInt(m[1], 10) + years} years`;
|
||||
};
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const { rows } = await client.query(
|
||||
`SELECT * FROM pf.load_plan
|
||||
WHERE version_id = $1 AND run_logid IS NULL
|
||||
ORDER BY seq, id`,
|
||||
[versionId]
|
||||
);
|
||||
const out = [];
|
||||
for (const p of rows) {
|
||||
const filters = Array.isArray(p.filters)
|
||||
? p.filters.map(group => group.map(f => ({
|
||||
...f,
|
||||
values: Array.isArray(f.values) ? f.values.map(bumpYear) : f.values
|
||||
})))
|
||||
: p.filters;
|
||||
const upd = await client.query(
|
||||
`UPDATE pf.load_plan
|
||||
SET filters = $2::jsonb, date_offset = $3, updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING *`,
|
||||
[p.id, filters ? JSON.stringify(filters) : null, bumpOffset(p.date_offset)]
|
||||
);
|
||||
out.push(upd.rows[0]);
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
res.json({ shifted: out.length, plans: out });
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK').catch(() => {});
|
||||
res.status(500).json({ error: err.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
@ -102,7 +102,6 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio
|
||||
const [plans, setPlans] = useState([])
|
||||
const [editingPlanId, setEditingPlanId] = useState(null)
|
||||
const [runningAll, setRunningAll] = useState(false)
|
||||
const [shiftYears, setShiftYears] = useState('1')
|
||||
|
||||
// segment form
|
||||
const [segType, setSegType] = useState('baseline')
|
||||
@ -277,22 +276,6 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio
|
||||
flash(`Loaded ${done} segment${done === 1 ? '' : 's'}`)
|
||||
}
|
||||
|
||||
async function shiftPlanYears() {
|
||||
const years = parseInt(shiftYears)
|
||||
if (!Number.isInteger(years) || years === 0) { flash('Enter a whole number of years', 'error'); return }
|
||||
try {
|
||||
const res = await fetch(`/api/versions/${versionId}/plans/shift-years`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ years }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) { flash(data.error, 'error'); return }
|
||||
loadPlans()
|
||||
flash(`Shifted ${data.shifted} plan${data.shifted === 1 ? '' : 's'} by ${years > 0 ? '+' : ''}${years} year${Math.abs(years) === 1 ? '' : 's'}`)
|
||||
} catch (err) { flash(err.message, 'error') }
|
||||
}
|
||||
|
||||
async function deletePlan(plan) {
|
||||
await fetch(`/api/plans/${plan.id}`, { method: 'DELETE' })
|
||||
if (editingPlanId === plan.id) cancelEdit()
|
||||
@ -687,14 +670,6 @@ export default function Baseline({ sources = [], sourceId, versions = [], versio
|
||||
{plans.filter(p => !p.run_logid).length} of {plans.length} not yet loaded
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-3 normal-case font-normal">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-gray-400">Shift years</span>
|
||||
<input value={shiftYears} onChange={e => setShiftYears(e.target.value)}
|
||||
className="border border-gray-200 rounded px-1 py-0.5 w-12 text-center" />
|
||||
<button onClick={shiftPlanYears}
|
||||
title="Add this many years to every unrun plan's date offset, and to any bare four-digit year in its filter values"
|
||||
className="text-blue-600 hover:text-blue-700">Apply</button>
|
||||
</div>
|
||||
<button onClick={runAllPlans}
|
||||
disabled={runningAll || submitting || !plans.some(p => !p.run_logid)}
|
||||
className="border border-blue-200 text-blue-600 hover:text-blue-700 rounded
|
||||
|
||||
Loading…
Reference in New Issue
Block a user