From 7aaf017532d1d14526947a953b5c4e9786607faf Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Thu, 17 Sep 2026 13:47:49 -0400 Subject: [PATCH] Sort the synthetic segment labels last, and drop their parentheses Unordered, '(adjustment)' sorted *first* rather than last: '(' is 0x28 and digits begin at 0x30, so it precedes '01 - Prior Year Sales'. The parenthesised labels exist to mark a value as not a real segment, which is exactly what an ordinal now does, so they get high ordinals and plain names -- 98 - Unlabeled, 99 - Adjustments. 99 rather than max(seq) + 1 so the position does not shift every time a segment is added or renumbered. Co-Authored-By: Claude Opus 5 (1M context) --- ui/src/views/Forecast.jsx | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index c942d92..1717dad 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -77,19 +77,27 @@ function sqlSafe(v) { return String(v).replace(/'/g, "''") } -// pairs: [[rawValue, ordinal], ...]. Anything unlisted falls through to the raw -// column, so it sorts after the ordered entries (digits before letters) rather -// than silently landing first. -function buildOrderExpression(sourceCol, pairs) { - const cases = pairs +// Anything unlisted falls through to the raw column.\n// pairs: [[rawValue, ordinal], ...] or [[rawValue, ordinal, displayAs], ...] where +// the third element renames the value as well as ordering it. +function buildOrderExpression(sourceCol, pairs, extra = []) { + const cases = [...pairs, ...extra] .filter(([label, ord]) => label && ord > 0 && isExprSafe(label)) .sort((a, b) => a[1] - b[1]) - .map(([label, ord]) => - `if ("${sourceCol}" == '${sqlSafe(label)}') { '${sqlSafe(orderedLabel(ord, label))}' }`) + .map(([label, ord, displayAs]) => + `if ("${sourceCol}" == '${sqlSafe(label)}') { '${sqlSafe(orderedLabel(ord, displayAs || label))}' }`) if (cases.length === 0) return null return `${cases.join('\nelse ')}\nelse { "${sourceCol}" }` } +// The server's synthetic segment labels. Unordered they sort *first*, not last: +// '(' is 0x28 and digits begin at 0x30, so '(adjustment)' precedes '01 - ...'. +// They get high ordinals instead, and lose the parentheses — those marked the +// value as not-a-real-segment, which the ordinal now does. +const SYNTHETIC_SEGMENTS = [ + ['(unlabeled load)', 98, 'Unlabeled'], + ['(adjustment)', 99, 'Adjustments'], +] + // Build both expressions from the version's bucket_order and the log's seq values. function buildOrderExpressions(bucketOrder, logMeta) { const out = {} @@ -105,7 +113,7 @@ function buildOrderExpressions(bucketOrder, logMeta) { const segs = Object.values(logMeta || {}) .filter(m => m.seq != null && ['baseline', 'reference'].includes(m.operation)) .map(m => [(m.tag || m.note || '').trim(), m.seq]) - const segExpr = buildOrderExpression('pf_segment', segs) + const segExpr = buildOrderExpression('pf_segment', segs, SYNTHETIC_SEGMENTS) if (segExpr) out[ORDER_EXPR_NAMES.segment] = segExpr return out