Add UX mockup and update spec with navigation direction
- HTML mockup with collapsible side nav, 3-step flow (Setup/Baseline/Forecast) - Canvas-based timeline preview in baseline segment form - Table peek modal, status bar, help popovers - Spec updated: 3-step mental model, AG Grid replacement note Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9084a87ea5
commit
dd993e989c
17
pf_spec.md
17
pf_spec.md
@ -662,6 +662,7 @@ DELETE FROM pf.log WHERE id = {{logid}};
|
|||||||
## Open Questions / Future Scope
|
## Open Questions / Future Scope
|
||||||
|
|
||||||
- **Baseline replay** — re-execute change log against a restated baseline (`replay: true`); v1 returns 501
|
- **Baseline replay** — re-execute change log against a restated baseline (`replay: true`); v1 returns 501
|
||||||
|
- **Arrow IPC for initial data load** — at large row counts (1M+) the `/versions/:id/data` JSON response becomes a bottleneck. Option: serve Arrow IPC binary instead of JSON; Perspective's `worker.table()` accepts Arrow buffers natively. Incremental operation rows (scale/recode/clone) can stay as JSON fed to `table.update()` since they're always small. Could be implemented with `pg` + `apache-arrow` in Node, or by adding a server-side DuckDB instance (Postgres scanner → Arrow IPC) if a caching layer is also needed.
|
||||||
- **Approval workflow** — user submits, admin approves before changes are visible to others (deferred)
|
- **Approval workflow** — user submits, admin approves before changes are visible to others (deferred)
|
||||||
- **Territory filtering** — restrict what a user can see/edit by dimension value (deferred)
|
- **Territory filtering** — restrict what a user can see/edit by dimension value (deferred)
|
||||||
- **Export** — download forecast as CSV or push results to a reporting table
|
- **Export** — download forecast as CSV or push results to a reporting table
|
||||||
@ -680,10 +681,24 @@ DELETE FROM pf.log WHERE id = {{logid}};
|
|||||||
- Baseline workbench: multi-segment additive baseline with WHERE clause editor and offset
|
- Baseline workbench: multi-segment additive baseline with WHERE clause editor and offset
|
||||||
|
|
||||||
### Known UX issues — next focus area
|
### Known UX issues — next focus area
|
||||||
- Navigation flow is clunky: user is forced to start at Sources even when a version already exists; context (source/version) is lost between views; getting to Forecast requires too many steps
|
|
||||||
|
**Primary: overall interaction is not intuitive — needs a clearer 3-step flow**
|
||||||
|
|
||||||
|
The app should present itself as three distinct phases in order:
|
||||||
|
|
||||||
|
1. **Setup source** — register a table, configure col_meta, generate SQL. One-time admin task. Once done, user should never need to return here.
|
||||||
|
2. **Build baseline** — create a version, load baseline segments, optionally load reference. Also largely a one-time setup per version.
|
||||||
|
3. **Do edits** — the Forecast view; the primary ongoing working mode. Pivot + operation panel.
|
||||||
|
|
||||||
|
Current 5-tab nav (Sources / Versions / Baseline / Forecast / Log) obscures this progression. Users have to discover the order themselves, and context (which source, which version) is lost between tabs.
|
||||||
|
|
||||||
|
**Direction:** Redesign navigation around the 3-step mental model. Steps 1 and 2 should feel like "setup" that gets out of the way; step 3 is the main working surface. Consider a wizard/stepper for first-time setup, with the Forecast view as the default landing once a version exists.
|
||||||
|
|
||||||
|
**Other issues:**
|
||||||
- No clear "current version" concept — user has to re-select source → version each session
|
- No clear "current version" concept — user has to re-select source → version each session
|
||||||
- Operation panel feedback is minimal (row count only, no before/after summary)
|
- Operation panel feedback is minimal (row count only, no before/after summary)
|
||||||
- Perspective computed date columns (Month, YearDate) extracted via split_by don't filter back to raw rows when used as slice — only native dimension columns work for slice extraction
|
- Perspective computed date columns (Month, YearDate) extracted via split_by don't filter back to raw rows when used as slice — only native dimension columns work for slice extraction
|
||||||
|
- AG Grid watermark (community edition) and non-intuitive interaction on admin grids — replace with plain `<table>` + Tailwind, same pattern used in dataflow (`/opt/dataflow/ui/src/pages/`)
|
||||||
|
|
||||||
### Branch status
|
### Branch status
|
||||||
- `baseline-workbench` — merged to origin, stable
|
- `baseline-workbench` — merged to origin, stable
|
||||||
|
|||||||
112
pf_ux_mockup.md
Normal file
112
pf_ux_mockup.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# Pivot Forecast — UX Mockup
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Pivot Forecast │
|
||||||
|
│ ① Setup ② Baseline ③ Forecast ◀ (default landing) │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
① SETUP
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
┌──── All Tables ──────────────┐ ┌──── Registered Sources ─────────┐
|
||||||
|
│ schema table rows │ │ │
|
||||||
|
│ ────── ────────── ────── │ │ sales_orders ✓ SQL ready │
|
||||||
|
│ public sales_orders 48,291 │◀─│ invoices ✓ SQL ready │
|
||||||
|
│ public invoices 12,004 │ │ + Register table │
|
||||||
|
│ public products 891 │ └──────────────────────────────────┘
|
||||||
|
│ rpt summary_mv 3,442 │
|
||||||
|
└──────────────────────────────┘ ┌──── Col Meta: sales_orders ─────┐
|
||||||
|
│ column role key label│
|
||||||
|
│ ────────── ──────── ─── ─── │
|
||||||
|
│ customer dimension ✓ │
|
||||||
|
│ channel dimension ✓ │
|
||||||
|
│ part dimension │
|
||||||
|
│ geography dimension │
|
||||||
|
│ order_date date │
|
||||||
|
│ ship_date filter │
|
||||||
|
│ status filter │
|
||||||
|
│ units units │
|
||||||
|
│ revenue value │
|
||||||
|
│ internal_id ignore │
|
||||||
|
│ │
|
||||||
|
│ [Generate SQL ▶] │
|
||||||
|
└──────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
② BASELINE
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
Source [sales_orders ▾] Version [FY2026 Plan ▾] [+ New version]
|
||||||
|
|
||||||
|
┌──── Segments ──────────────────────────────────────────────────────┐
|
||||||
|
│ # description rows by date │
|
||||||
|
│ ─ ──────────────────────────── ────── ────── ────────────── │
|
||||||
|
│ 1 FY25 actuals +1yr 41,204 paul Apr 24 │
|
||||||
|
│ 2 Open orders 3,109 paul Apr 24 [Undo] │
|
||||||
|
│ │
|
||||||
|
│ Total baseline rows: 44,313 [Clear all baseline] │
|
||||||
|
└────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──── Add Segment ────────────────────────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ Description [ ] │
|
||||||
|
│ │
|
||||||
|
│ Filters [+ Add filter] │
|
||||||
|
│ ┌─────────────────┬──────────┬─────────────────────┬───┐ │
|
||||||
|
│ │ order_date │ BETWEEN │ 2025-01-01 2025-12-31│ x │ │
|
||||||
|
│ └─────────────────┴──────────┴─────────────────────┴───┘ │
|
||||||
|
│ │
|
||||||
|
│ Date offset [1] yr [0] mo │
|
||||||
|
│ │
|
||||||
|
│ ·───────────────────────────· source │
|
||||||
|
│ Jan 2025 Dec 2025 │
|
||||||
|
│ ·───────────────────────────· projected (+1 yr) │
|
||||||
|
│ Jan 2026 Dec 2026 │
|
||||||
|
│ │
|
||||||
|
│ Note [ ] [Load Segment] │
|
||||||
|
└────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌──── Reference (optional) ──────────────────────────────────────────┐
|
||||||
|
│ Load prior-period rows for comparison in the pivot │
|
||||||
|
│ Date range [2024-01-01] to [2024-12-31] │
|
||||||
|
│ Note [ ] [Load Ref] │
|
||||||
|
└────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
③ FORECAST source: sales_orders
|
||||||
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
Version [FY2026 Plan ▾] [Refresh] [Save layout] [Reset layout]
|
||||||
|
|
||||||
|
┌──── Pivot ───────────────────────────────┐ ┌──── Operations ───────┐
|
||||||
|
│ │ │ │
|
||||||
|
│ (Perspective viewer) │ │ Slice │
|
||||||
|
│ │ │ channel = WHS │
|
||||||
|
│ channel │ Jan 2026 │ Feb 2026 │ ... │ │ geo = WEST │
|
||||||
|
│ ──────────┼──────────┼──────────┼─── │ │ │
|
||||||
|
│ DIR │ 412,000 │ 388,000 │ │ │ [Scale][Recode] │
|
||||||
|
│ WHS ◀ │ 290,000 │ 310,000 │ │ │ [Clone] │
|
||||||
|
│ ──────── │ │ │ │ │ ─────────────────── │
|
||||||
|
│ Total │ 702,000 │ 698,000 │ │ │ Value incr [ ] │
|
||||||
|
│ │ │ Units incr [ ] │
|
||||||
|
│ │ │ Pct? [ ] │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ Note [ ] │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ [Submit] │
|
||||||
|
└──────────────────────────────────────────┘ └───────────────────────┘
|
||||||
|
|
||||||
|
▼ Change log (12 entries)
|
||||||
|
┌────┬───────────┬──────────┬─────────────────────────┬────────────┐
|
||||||
|
│ id │ operation │ by │ slice │ │
|
||||||
|
│ ── │ ───────── │ ──────── │ ───────────────────── ─ │ │
|
||||||
|
│ 12 │ scale │ paul │ channel=WHS geo=WEST │ [Undo] │
|
||||||
|
│ 11 │ recode │ paul │ part=OLD-SKU │ [Undo] │
|
||||||
|
│ 10 │ scale │ paul │ channel=DIR │ [Undo] │
|
||||||
|
└────┴───────────┴──────────┴─────────────────────────┴────────────┘
|
||||||
|
```
|
||||||
888
public/mockup.html
Normal file
888
public/mockup.html
Normal file
@ -0,0 +1,888 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Pivot Forecast — Mockup</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
.help-popup { display:none; position:absolute; z-index:50; }
|
||||||
|
.help-popup.open { display:block; }
|
||||||
|
#sidebar { transition: width 150ms ease; }
|
||||||
|
#sidebar.expanded { width: 200px; }
|
||||||
|
#sidebar.collapsed { width: 48px; }
|
||||||
|
#sidebar .nav-label { transition: opacity 100ms ease; }
|
||||||
|
#sidebar.collapsed .nav-label { opacity:0; pointer-events:none; width:0; overflow:hidden; }
|
||||||
|
#sidebar.expanded .nav-label { opacity:1; }
|
||||||
|
#sidebar .app-title { transition: opacity 100ms ease; }
|
||||||
|
#sidebar.collapsed .app-title { opacity:0; pointer-events:none; width:0; overflow:hidden; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 text-sm text-gray-800 font-sans">
|
||||||
|
|
||||||
|
<!-- Help popups (global, positioned by JS) -->
|
||||||
|
<div id="help-overlay" class="fixed inset-0 z-40 hidden" onclick="closeHelp()"></div>
|
||||||
|
<div id="help-box" class="help-popup w-72 bg-gray-900 text-gray-100 text-xs rounded-lg shadow-xl p-4 leading-relaxed"></div>
|
||||||
|
|
||||||
|
<div class="flex h-screen">
|
||||||
|
|
||||||
|
<!-- Side Nav -->
|
||||||
|
<div id="sidebar" class="expanded bg-white border-r border-gray-200 flex flex-col shrink-0 overflow-hidden">
|
||||||
|
|
||||||
|
<!-- Logo / toggle -->
|
||||||
|
<div class="h-12 flex items-center px-3 border-b border-gray-100 gap-2 shrink-0">
|
||||||
|
<button onclick="toggleSidebar()" class="w-8 h-8 flex items-center justify-center rounded hover:bg-gray-100 text-gray-400 shrink-0" title="Toggle sidebar">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1" y="3" width="14" height="1.5" rx="0.75" fill="currentColor"/><rect x="1" y="7.25" width="14" height="1.5" rx="0.75" fill="currentColor"/><rect x="1" y="11.5" width="14" height="1.5" rx="0.75" fill="currentColor"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class="app-title text-xs font-semibold text-gray-600 tracking-wide uppercase whitespace-nowrap">Pivot Forecast</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nav items -->
|
||||||
|
<nav class="flex flex-col gap-0.5 p-2 flex-1">
|
||||||
|
<button onclick="show('setup')" id="tab-setup" class="nav-item flex items-center gap-3 px-2 py-2 rounded text-gray-500 hover:bg-gray-100 hover:text-gray-800 text-left w-full" title="Setup">
|
||||||
|
<!-- sliders / config icon -->
|
||||||
|
<svg class="shrink-0" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
|
||||||
|
<line x1="3" y1="5" x2="17" y2="5"/><circle cx="7" cy="5" r="1.8" fill="white" stroke="currentColor" stroke-width="1.6"/>
|
||||||
|
<line x1="3" y1="10" x2="17" y2="10"/><circle cx="13" cy="10" r="1.8" fill="white" stroke="currentColor" stroke-width="1.6"/>
|
||||||
|
<line x1="3" y1="15" x2="17" y2="15"/><circle cx="8" cy="15" r="1.8" fill="white" stroke="currentColor" stroke-width="1.6"/>
|
||||||
|
</svg>
|
||||||
|
<span class="nav-label text-sm whitespace-nowrap">Setup</span>
|
||||||
|
</button>
|
||||||
|
<button onclick="show('baseline')" id="tab-baseline" class="nav-item flex items-center gap-3 px-2 py-2 rounded text-gray-500 hover:bg-gray-100 hover:text-gray-800 text-left w-full" title="Baseline">
|
||||||
|
<!-- layers / stack icon -->
|
||||||
|
<svg class="shrink-0" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polygon points="10,2 18,7 10,12 2,7"/>
|
||||||
|
<polyline points="2,12 10,17 18,12"/>
|
||||||
|
</svg>
|
||||||
|
<span class="nav-label text-sm whitespace-nowrap">Baseline</span>
|
||||||
|
</button>
|
||||||
|
<button onclick="show('forecast')" id="tab-forecast" class="nav-item flex items-center gap-3 px-2 py-2 rounded text-gray-500 hover:bg-gray-100 hover:text-gray-800 text-left w-full" title="Forecast">
|
||||||
|
<!-- trending up / chart icon -->
|
||||||
|
<svg class="shrink-0" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="2,15 7,9 11,12 18,4"/>
|
||||||
|
<polyline points="14,4 18,4 18,8"/>
|
||||||
|
</svg>
|
||||||
|
<span class="nav-label text-sm whitespace-nowrap">Forecast</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-hidden flex flex-col">
|
||||||
|
|
||||||
|
<!-- Status bar -->
|
||||||
|
<div class="bg-white border-b border-gray-100 px-4 h-8 flex items-center gap-4 shrink-0 text-xs">
|
||||||
|
<span class="text-gray-400">Source</span>
|
||||||
|
<span class="font-medium text-gray-700">sales_orders</span>
|
||||||
|
<span class="text-gray-200">|</span>
|
||||||
|
<span class="text-gray-400">Version</span>
|
||||||
|
<span class="font-medium text-gray-700">FY2026 Plan</span>
|
||||||
|
<span class="text-gray-200">|</span>
|
||||||
|
<span class="text-gray-400">Baseline</span>
|
||||||
|
<span class="font-medium text-gray-700">44,313 rows</span>
|
||||||
|
<span class="text-gray-200">|</span>
|
||||||
|
<span class="text-gray-400">Status</span>
|
||||||
|
<span class="text-green-600 font-medium">open</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ① SETUP -->
|
||||||
|
<div id="view-setup" class="hidden h-full flex gap-0 overflow-hidden">
|
||||||
|
|
||||||
|
<!-- All Tables -->
|
||||||
|
<div class="w-64 bg-white border-r border-gray-200 flex flex-col shrink-0">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-100 text-xs font-medium text-gray-500 uppercase tracking-wide">All Tables</div>
|
||||||
|
<div class="overflow-y-auto flex-1">
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<thead class="sticky top-0 bg-gray-50">
|
||||||
|
<tr class="text-left text-gray-400 border-b border-gray-100">
|
||||||
|
<th class="px-3 py-1.5 font-medium">schema</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium">table</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium text-right">rows</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr onclick="peekTable('sales_orders')" class="border-t border-gray-50 hover:bg-blue-50 cursor-pointer bg-blue-50">
|
||||||
|
<td class="px-3 py-1.5 text-gray-400">public</td>
|
||||||
|
<td class="px-3 py-1.5 font-medium text-blue-600">sales_orders</td>
|
||||||
|
<td class="px-3 py-1.5 text-right text-gray-500">48,291</td>
|
||||||
|
</tr>
|
||||||
|
<tr onclick="peekTable('invoices')" class="border-t border-gray-50 hover:bg-blue-50 cursor-pointer">
|
||||||
|
<td class="px-3 py-1.5 text-gray-400">public</td>
|
||||||
|
<td class="px-3 py-1.5">invoices</td>
|
||||||
|
<td class="px-3 py-1.5 text-right text-gray-500">12,004</td>
|
||||||
|
</tr>
|
||||||
|
<tr onclick="peekTable('products')" class="border-t border-gray-50 hover:bg-blue-50 cursor-pointer">
|
||||||
|
<td class="px-3 py-1.5 text-gray-400">public</td>
|
||||||
|
<td class="px-3 py-1.5">products</td>
|
||||||
|
<td class="px-3 py-1.5 text-right text-gray-500">891</td>
|
||||||
|
</tr>
|
||||||
|
<tr onclick="peekTable('summary_mv')" class="border-t border-gray-50 hover:bg-blue-50 cursor-pointer">
|
||||||
|
<td class="px-3 py-1.5 text-gray-400">rpt</td>
|
||||||
|
<td class="px-3 py-1.5">summary_mv</td>
|
||||||
|
<td class="px-3 py-1.5 text-right text-gray-500">3,442</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex-1 flex flex-col gap-4 overflow-hidden min-w-0 p-4">
|
||||||
|
|
||||||
|
<div class="bg-white border border-gray-200 rounded shrink-0">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-100 text-xs font-medium text-gray-500 uppercase tracking-wide flex items-center justify-between">
|
||||||
|
<span>Registered Sources</span>
|
||||||
|
<button class="text-blue-600 hover:text-blue-700 text-xs font-medium">+ Register table</button>
|
||||||
|
</div>
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-t border-gray-50 hover:bg-gray-50 cursor-pointer bg-blue-50">
|
||||||
|
<td class="px-3 py-2 font-medium text-blue-600">sales_orders</td>
|
||||||
|
<td class="px-3 py-2 text-green-600 font-medium">✓ SQL ready</td>
|
||||||
|
<td class="px-3 py-2 text-gray-400 text-right">public</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-t border-gray-50 hover:bg-gray-50 cursor-pointer">
|
||||||
|
<td class="px-3 py-2 font-medium">invoices</td>
|
||||||
|
<td class="px-3 py-2 text-green-600 font-medium">✓ SQL ready</td>
|
||||||
|
<td class="px-3 py-2 text-gray-400 text-right">public</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white border border-gray-200 rounded flex flex-col flex-1 overflow-hidden">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-100 text-xs font-medium text-gray-500 uppercase tracking-wide flex items-center justify-between shrink-0">
|
||||||
|
<span>Col Meta — <span class="text-gray-700">sales_orders</span></span>
|
||||||
|
<button class="bg-blue-600 text-white text-xs px-3 py-1 rounded hover:bg-blue-700">Generate SQL</button>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-y-auto flex-1">
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<thead class="sticky top-0 bg-gray-50">
|
||||||
|
<tr class="text-left text-gray-400 border-b border-gray-100">
|
||||||
|
<th class="px-3 py-1.5 font-medium">column</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium">role</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium text-center">key</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium">label</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">customer</td><td class="px-3 py-1.5"><span class="bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">dimension</span></td><td class="px-3 py-1.5 text-center text-green-500">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">channel</td><td class="px-3 py-1.5"><span class="bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">dimension</span></td><td class="px-3 py-1.5 text-center text-green-500">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">part</td><td class="px-3 py-1.5"><span class="bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">dimension</span></td><td class="px-3 py-1.5 text-center text-gray-300">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">geography</td><td class="px-3 py-1.5"><span class="bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">dimension</span></td><td class="px-3 py-1.5 text-center text-gray-300">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">order_date</td><td class="px-3 py-1.5"><span class="bg-purple-50 text-purple-700 px-1.5 py-0.5 rounded">date</span></td><td class="px-3 py-1.5 text-center text-gray-300">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">ship_date</td><td class="px-3 py-1.5"><span class="bg-yellow-50 text-yellow-700 px-1.5 py-0.5 rounded">filter</span></td><td class="px-3 py-1.5 text-center text-gray-300">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">status</td><td class="px-3 py-1.5"><span class="bg-yellow-50 text-yellow-700 px-1.5 py-0.5 rounded">filter</span></td><td class="px-3 py-1.5 text-center text-gray-300">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">units</td><td class="px-3 py-1.5"><span class="bg-green-50 text-green-700 px-1.5 py-0.5 rounded">units</span></td><td class="px-3 py-1.5 text-center text-gray-300">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">revenue</td><td class="px-3 py-1.5"><span class="bg-green-50 text-green-700 px-1.5 py-0.5 rounded">value</span></td><td class="px-3 py-1.5 text-center text-gray-300">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-3 py-1.5 font-mono text-gray-700">internal_id</td><td class="px-3 py-1.5"><span class="bg-gray-100 text-gray-400 px-1.5 py-0.5 rounded">ignore</span></td><td class="px-3 py-1.5 text-center text-gray-300">✓</td><td class="px-3 py-1.5 text-gray-400">—</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ② BASELINE -->
|
||||||
|
<div id="view-baseline" class="hidden h-full overflow-y-auto">
|
||||||
|
<div class="p-4 flex flex-col gap-4">
|
||||||
|
|
||||||
|
<!-- Version bar -->
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-gray-500">Source</span>
|
||||||
|
<select class="border border-gray-200 rounded px-2 py-1 text-sm bg-white">
|
||||||
|
<option>sales_orders</option><option>invoices</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-gray-500">Version</span>
|
||||||
|
<select class="border border-gray-200 rounded px-2 py-1 text-sm bg-white">
|
||||||
|
<option>FY2026 Plan</option><option>FY2026 Conservative</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="text-blue-600 hover:text-blue-700 text-xs font-medium border border-blue-200 px-2 py-1 rounded">+ New version</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Segments loaded -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-100 text-xs font-medium text-gray-500 uppercase tracking-wide flex items-center justify-between">
|
||||||
|
<span>Segments loaded</span>
|
||||||
|
<button class="text-red-500 hover:text-red-600 text-xs normal-case font-normal">Clear all baseline</button>
|
||||||
|
</div>
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr class="text-left text-gray-400 border-b border-gray-100">
|
||||||
|
<th class="px-3 py-1.5 font-medium">#</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium">description</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium text-right">rows</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium">by</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium">date</th>
|
||||||
|
<th class="px-3 py-1.5 font-medium"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-t border-gray-50">
|
||||||
|
<td class="px-3 py-2 text-gray-400">1</td>
|
||||||
|
<td class="px-3 py-2">FY25 actuals +1yr</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">41,204</td>
|
||||||
|
<td class="px-3 py-2 text-gray-500">paul</td>
|
||||||
|
<td class="px-3 py-2 text-gray-400">Apr 24</td>
|
||||||
|
<td class="px-3 py-2 text-right"><button class="text-gray-400 hover:text-red-500 text-xs">Undo</button></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-t border-gray-50">
|
||||||
|
<td class="px-3 py-2 text-gray-400">2</td>
|
||||||
|
<td class="px-3 py-2">Open orders</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">3,109</td>
|
||||||
|
<td class="px-3 py-2 text-gray-500">paul</td>
|
||||||
|
<td class="px-3 py-2 text-gray-400">Apr 24</td>
|
||||||
|
<td class="px-3 py-2 text-right"><button class="text-gray-400 hover:text-red-500 text-xs">Undo</button></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="border-t border-gray-100 bg-gray-50">
|
||||||
|
<td colspan="2" class="px-3 py-1.5 text-gray-500 text-xs">Total baseline rows</td>
|
||||||
|
<td class="px-3 py-1.5 text-right font-mono font-medium">44,313</td>
|
||||||
|
<td colspan="3"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Segment -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-100 text-xs font-medium text-gray-500 uppercase tracking-wide flex items-center justify-between">
|
||||||
|
<span>Add Segment</span>
|
||||||
|
<button onclick="showHelp(this,'segment')" class="text-gray-300 hover:text-gray-500 text-xs normal-case font-normal">? Help</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 flex flex-col gap-4">
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<label class="text-xs text-gray-500 w-28">Description</label>
|
||||||
|
<input type="text" placeholder="e.g. FY25 actuals shifted +1yr" class="border border-gray-200 rounded px-2 py-1.5 text-sm flex-1 max-w-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-xs text-gray-500">Description</label>
|
||||||
|
<input type="text" placeholder="e.g. FY25 actuals shifted +1yr" class="border border-gray-200 rounded px-2 py-1.5 text-sm bg-white w-full max-w-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-xs text-gray-500">Filters</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button onclick="showHelp(this,'filters')" class="text-gray-300 hover:text-gray-500 text-xs">?</button>
|
||||||
|
<button class="text-blue-600 hover:text-blue-700 text-xs font-medium">+ Add filter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<select class="border border-gray-200 rounded px-2 py-1 text-xs bg-white">
|
||||||
|
<option>order_date</option><option>ship_date</option><option>status</option>
|
||||||
|
</select>
|
||||||
|
<select class="border border-gray-200 rounded px-2 py-1 text-xs bg-white">
|
||||||
|
<option>BETWEEN</option><option>=</option><option>IN</option><option>NOT IN</option>
|
||||||
|
</select>
|
||||||
|
<input id="date-from" type="text" value="2025-01-01" oninput="drawTimeline()" class="border border-gray-200 rounded px-2 py-1 text-xs w-24 font-mono bg-white" />
|
||||||
|
<span class="text-gray-400 text-xs">and</span>
|
||||||
|
<input id="date-to" type="text" value="2025-12-31" oninput="drawTimeline()" class="border border-gray-200 rounded px-2 py-1 text-xs w-24 font-mono bg-white" />
|
||||||
|
<button class="text-gray-300 hover:text-red-400 text-xs">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date offset -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-xs text-gray-500">Date offset</label>
|
||||||
|
<button onclick="showHelp(this,'offset')" class="text-gray-300 hover:text-gray-500 text-xs">?</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input id="offset-yr" type="number" value="1" min="0" oninput="drawTimeline()" class="border border-gray-200 rounded px-2 py-1 text-sm w-16 text-center bg-white" />
|
||||||
|
<span class="text-xs text-gray-500">yr</span>
|
||||||
|
<input id="offset-mo" type="number" value="0" min="0" max="11" oninput="drawTimeline()" class="border border-gray-200 rounded px-2 py-1 text-sm w-16 text-center bg-white" />
|
||||||
|
<span class="text-xs text-gray-500">mo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timeline -->
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="text-xs text-gray-500">Timeline preview</label>
|
||||||
|
<button onclick="showHelp(this,'timeline')" class="text-gray-300 hover:text-gray-500 text-xs">?</button>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white border border-gray-200 rounded p-3">
|
||||||
|
<canvas id="timeline-canvas" height="90" style="width:100%;display:block;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Note + submit -->
|
||||||
|
<div class="flex items-end gap-3">
|
||||||
|
<div class="flex flex-col gap-1 flex-1 max-w-xs">
|
||||||
|
<label class="text-xs text-gray-500">Note</label>
|
||||||
|
<input type="text" placeholder="optional" class="border border-gray-200 rounded px-2 py-1.5 text-sm bg-white" />
|
||||||
|
</div>
|
||||||
|
<button class="bg-blue-600 text-white text-xs px-5 py-2 rounded hover:bg-blue-700 shrink-0">Load Segment</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reference -->
|
||||||
|
<div class="bg-white border border-gray-200 rounded">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-100 text-xs font-medium text-gray-500 uppercase tracking-wide flex items-center justify-between">
|
||||||
|
<span>Reference <span class="text-gray-300 font-normal normal-case">optional</span></span>
|
||||||
|
<button onclick="showHelp(this,'reference')" class="text-gray-300 hover:text-gray-500 text-xs normal-case font-normal">?</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 flex items-center gap-3">
|
||||||
|
<label class="text-xs text-gray-500 w-28">Date range</label>
|
||||||
|
<input type="text" value="2024-01-01" class="border border-gray-200 rounded px-2 py-1 text-xs w-28 font-mono" />
|
||||||
|
<span class="text-xs text-gray-400">to</span>
|
||||||
|
<input type="text" value="2024-12-31" class="border border-gray-200 rounded px-2 py-1 text-xs w-28 font-mono" />
|
||||||
|
<input type="text" placeholder="note" class="border border-gray-200 rounded px-2 py-1 text-sm flex-1 max-w-xs" />
|
||||||
|
<button class="border border-gray-200 text-gray-600 text-xs px-4 py-1.5 rounded hover:bg-gray-50">Load Reference</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ③ FORECAST -->
|
||||||
|
<div id="view-forecast" class="hidden h-full flex flex-col overflow-hidden">
|
||||||
|
|
||||||
|
<div class="bg-white border-b border-gray-200 px-4 py-2 flex items-center gap-3 shrink-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-gray-500">Version</span>
|
||||||
|
<select class="border border-gray-200 rounded px-2 py-1 text-sm bg-white">
|
||||||
|
<option>FY2026 Plan</option>
|
||||||
|
<option>FY2026 Conservative</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
<button class="text-xs text-gray-500 hover:text-gray-700 border border-gray-200 px-3 py-1 rounded">Refresh</button>
|
||||||
|
<button class="text-xs text-gray-500 hover:text-gray-700 border border-gray-200 px-3 py-1 rounded">Save layout</button>
|
||||||
|
<button class="text-xs text-gray-500 hover:text-gray-700 border border-gray-200 px-3 py-1 rounded">Reset layout</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-1 overflow-hidden gap-0">
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-hidden flex flex-col bg-white border-r border-gray-200">
|
||||||
|
<div class="flex-1 overflow-hidden relative">
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<thead class="sticky top-0 bg-gray-50 border-b border-gray-200">
|
||||||
|
<tr class="text-right text-gray-500">
|
||||||
|
<th class="px-3 py-2 text-left text-gray-600 font-medium">channel</th>
|
||||||
|
<th class="px-3 py-2 font-medium">Jan 2026</th>
|
||||||
|
<th class="px-3 py-2 font-medium">Feb 2026</th>
|
||||||
|
<th class="px-3 py-2 font-medium">Mar 2026</th>
|
||||||
|
<th class="px-3 py-2 font-medium">Apr 2026</th>
|
||||||
|
<th class="px-3 py-2 font-medium">May 2026</th>
|
||||||
|
<th class="px-3 py-2 font-medium">Jun 2026</th>
|
||||||
|
<th class="px-3 py-2 font-medium text-gray-800">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-t border-gray-100 hover:bg-gray-50 cursor-pointer">
|
||||||
|
<td class="px-3 py-2 font-medium">DIR</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">412,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">388,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">425,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">401,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">390,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">410,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium">2,426,000</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-t border-gray-100 hover:bg-blue-50 cursor-pointer bg-blue-50">
|
||||||
|
<td class="px-3 py-2 font-medium text-blue-700">WHS ◂</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono text-blue-700">290,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono text-blue-700">310,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono text-blue-700">298,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono text-blue-700">315,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono text-blue-700">305,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono text-blue-700">320,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium text-blue-700">1,838,000</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-t border-gray-100 hover:bg-gray-50 cursor-pointer">
|
||||||
|
<td class="px-3 py-2 font-medium">ECOM</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">155,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">162,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">170,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">158,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">165,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono">175,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium">985,000</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-t-2 border-gray-200 bg-gray-50">
|
||||||
|
<td class="px-3 py-2 font-medium text-gray-700">Total</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium">857,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium">860,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium">893,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium">874,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium">860,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium">905,000</td>
|
||||||
|
<td class="px-3 py-2 text-right font-mono font-medium">5,249,000</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="absolute bottom-2 right-3 text-xs text-gray-300 italic">Perspective viewer</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-56 bg-white flex flex-col shrink-0">
|
||||||
|
<div class="px-3 py-2 border-b border-gray-100 text-xs font-medium text-gray-500 uppercase tracking-wide flex items-center justify-between">
|
||||||
|
<span>Operations</span>
|
||||||
|
<button onclick="showHelp(this,'operations')" class="text-gray-300 hover:text-gray-500 text-xs">?</button>
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-2 border-b border-gray-100">
|
||||||
|
<div class="text-xs text-gray-400 mb-1.5">Slice</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex items-center justify-between text-xs">
|
||||||
|
<span class="text-gray-500">channel</span>
|
||||||
|
<span class="font-mono text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded">WHS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex border-b border-gray-100 text-xs">
|
||||||
|
<button class="flex-1 py-1.5 text-center bg-blue-50 text-blue-700 font-medium border-b-2 border-blue-500">Scale</button>
|
||||||
|
<button class="flex-1 py-1.5 text-center text-gray-400 hover:text-gray-600">Recode</button>
|
||||||
|
<button class="flex-1 py-1.5 text-center text-gray-400 hover:text-gray-600">Clone</button>
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-3 flex flex-col gap-3 text-xs flex-1">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-gray-400">Value increment</label>
|
||||||
|
<input type="text" placeholder="e.g. 50000" class="border border-gray-200 rounded px-2 py-1 text-sm font-mono" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-gray-400">Units increment</label>
|
||||||
|
<input type="text" placeholder="e.g. 500" class="border border-gray-200 rounded px-2 py-1 text-sm font-mono" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="checkbox" id="pct" class="rounded" />
|
||||||
|
<label for="pct" class="text-gray-500">% of slice total</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<label class="text-gray-400">Note</label>
|
||||||
|
<input type="text" placeholder="optional" class="border border-gray-200 rounded px-2 py-1 text-sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-3 border-t border-gray-100">
|
||||||
|
<button class="w-full bg-blue-600 text-white text-xs py-1.5 rounded hover:bg-blue-700">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white border-t border-gray-200 shrink-0">
|
||||||
|
<button onclick="toggleLog()" class="w-full px-4 py-2 text-left text-xs text-gray-500 hover:bg-gray-50 flex items-center gap-2">
|
||||||
|
<span id="log-arrow">▶</span>
|
||||||
|
<span>Change log (12 entries)</span>
|
||||||
|
</button>
|
||||||
|
<div id="log-panel" class="hidden overflow-auto max-h-40">
|
||||||
|
<table class="w-full text-xs">
|
||||||
|
<thead class="bg-gray-50 sticky top-0">
|
||||||
|
<tr class="text-left text-gray-400 border-b border-gray-100">
|
||||||
|
<th class="px-4 py-1.5 font-medium">id</th>
|
||||||
|
<th class="px-4 py-1.5 font-medium">operation</th>
|
||||||
|
<th class="px-4 py-1.5 font-medium">by</th>
|
||||||
|
<th class="px-4 py-1.5 font-medium">slice</th>
|
||||||
|
<th class="px-4 py-1.5 font-medium">note</th>
|
||||||
|
<th class="px-4 py-1.5 font-medium"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-4 py-1.5 text-gray-400 font-mono">12</td><td class="px-4 py-1.5"><span class="bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">scale</span></td><td class="px-4 py-1.5 text-gray-500">paul</td><td class="px-4 py-1.5 font-mono text-gray-600">channel=WHS geo=WEST</td><td class="px-4 py-1.5 text-gray-400">10% lift Q3</td><td class="px-4 py-1.5 text-right"><button class="text-gray-300 hover:text-red-500">Undo</button></td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-4 py-1.5 text-gray-400 font-mono">11</td><td class="px-4 py-1.5"><span class="bg-orange-50 text-orange-700 px-1.5 py-0.5 rounded">recode</span></td><td class="px-4 py-1.5 text-gray-500">paul</td><td class="px-4 py-1.5 font-mono text-gray-600">part=OLD-SKU-001</td><td class="px-4 py-1.5 text-gray-400">discontinued SKU</td><td class="px-4 py-1.5 text-right"><button class="text-gray-300 hover:text-red-500">Undo</button></td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-4 py-1.5 text-gray-400 font-mono">10</td><td class="px-4 py-1.5"><span class="bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded">scale</span></td><td class="px-4 py-1.5 text-gray-500">paul</td><td class="px-4 py-1.5 font-mono text-gray-600">channel=DIR</td><td class="px-4 py-1.5 text-gray-400"></td><td class="px-4 py-1.5 text-right"><button class="text-gray-300 hover:text-red-500">Undo</button></td></tr>
|
||||||
|
<tr class="border-t border-gray-50"><td class="px-4 py-1.5 text-gray-400 font-mono">9</td><td class="px-4 py-1.5"><span class="bg-green-50 text-green-700 px-1.5 py-0.5 rounded">clone</span></td><td class="px-4 py-1.5 text-gray-500">paul</td><td class="px-4 py-1.5 font-mono text-gray-600">customer=ACME</td><td class="px-4 py-1.5 text-gray-400">new account win</td><td class="px-4 py-1.5 text-right"><button class="text-gray-300 hover:text-red-500">Undo</button></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- flex-1 content area -->
|
||||||
|
</div><!-- app shell -->
|
||||||
|
|
||||||
|
<!-- Table peek modal -->
|
||||||
|
<div id="peek-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center">
|
||||||
|
<div class="absolute inset-0 bg-black/30" onclick="closePeek()"></div>
|
||||||
|
<div class="relative bg-white rounded-lg shadow-2xl flex flex-col z-10" style="width:720px;max-width:90vw;max-height:80vh;">
|
||||||
|
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 shrink-0">
|
||||||
|
<div>
|
||||||
|
<span id="peek-title" class="text-sm font-semibold text-gray-800"></span>
|
||||||
|
<span id="peek-rowcount" class="ml-2 text-xs text-gray-400"></span>
|
||||||
|
</div>
|
||||||
|
<button onclick="closePeek()" class="text-gray-300 hover:text-gray-600 text-lg leading-none ml-4">✕</button>
|
||||||
|
</div>
|
||||||
|
<div id="peek-body" class="overflow-y-auto flex-1 text-xs"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
// ── Table peek ─────────────────────────────────────────────────
|
||||||
|
const peekData = {
|
||||||
|
sales_orders: {
|
||||||
|
schema: 'public', rows: 48291,
|
||||||
|
cols: [
|
||||||
|
{name:'customer', type:'text'},
|
||||||
|
{name:'channel', type:'text'},
|
||||||
|
{name:'part', type:'text'},
|
||||||
|
{name:'geography', type:'text'},
|
||||||
|
{name:'order_date', type:'date'},
|
||||||
|
{name:'ship_date', type:'date'},
|
||||||
|
{name:'status', type:'text'},
|
||||||
|
{name:'units', type:'numeric'},
|
||||||
|
{name:'revenue', type:'numeric'},
|
||||||
|
{name:'internal_id',type:'integer'},
|
||||||
|
],
|
||||||
|
sample: [
|
||||||
|
{customer:'ACME CORP', channel:'WHS', part:'SKU-001', geography:'WEST', order_date:'2025-03-14', ship_date:'2025-03-18', status:'SHIPPED', units:120, revenue:4800.00, internal_id:10041},
|
||||||
|
{customer:'GLOBEX INC', channel:'DIR', part:'SKU-004', geography:'EAST', order_date:'2025-03-15', ship_date:null, status:'OPEN', units:50, revenue:2250.00, internal_id:10042},
|
||||||
|
{customer:'INITECH', channel:'WHS', part:'SKU-002', geography:'CENT', order_date:'2025-03-15', ship_date:'2025-03-20', status:'SHIPPED', units:200, revenue:7600.00, internal_id:10043},
|
||||||
|
{customer:'UMBRELLA CO', channel:'ECOM',part:'SKU-007', geography:'WEST', order_date:'2025-03-16', ship_date:null, status:'PENDING', units:30, revenue:1350.00, internal_id:10044},
|
||||||
|
{customer:'ACME CORP', channel:'DIR', part:'SKU-001', geography:'EAST', order_date:'2025-03-16', ship_date:'2025-03-19', status:'SHIPPED', units:80, revenue:3200.00, internal_id:10045},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
invoices: {
|
||||||
|
schema: 'public', rows: 12004,
|
||||||
|
cols: [
|
||||||
|
{name:'invoice_id', type:'integer'},
|
||||||
|
{name:'customer', type:'text'},
|
||||||
|
{name:'invoice_date', type:'date'},
|
||||||
|
{name:'due_date', type:'date'},
|
||||||
|
{name:'amount', type:'numeric'},
|
||||||
|
{name:'status', type:'text'},
|
||||||
|
],
|
||||||
|
sample: [
|
||||||
|
{invoice_id:5001, customer:'ACME CORP', invoice_date:'2025-02-01', due_date:'2025-03-01', amount:12400.00, status:'PAID'},
|
||||||
|
{invoice_id:5002, customer:'GLOBEX INC', invoice_date:'2025-02-03', due_date:'2025-03-03', amount:8750.00, status:'OPEN'},
|
||||||
|
{invoice_id:5003, customer:'INITECH', invoice_date:'2025-02-05', due_date:'2025-03-05', amount:31200.00, status:'PAID'},
|
||||||
|
{invoice_id:5004, customer:'UMBRELLA CO', invoice_date:'2025-02-08', due_date:'2025-03-08', amount:4100.00, status:'OVERDUE'},
|
||||||
|
{invoice_id:5005, customer:'ACME CORP', invoice_date:'2025-02-10', due_date:'2025-03-10', amount:9600.00, status:'OPEN'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
schema: 'public', rows: 891,
|
||||||
|
cols: [
|
||||||
|
{name:'sku', type:'text'},
|
||||||
|
{name:'name', type:'text'},
|
||||||
|
{name:'category', type:'text'},
|
||||||
|
{name:'price', type:'numeric'},
|
||||||
|
{name:'active', type:'boolean'},
|
||||||
|
],
|
||||||
|
sample: [
|
||||||
|
{sku:'SKU-001', name:'Widget A', category:'Widgets', price:40.00, active:true},
|
||||||
|
{sku:'SKU-002', name:'Widget B', category:'Widgets', price:38.00, active:true},
|
||||||
|
{sku:'SKU-004', name:'Gadget Pro', category:'Gadgets', price:45.00, active:true},
|
||||||
|
{sku:'SKU-007', name:'Doohickey X', category:'Other', price:45.00, active:true},
|
||||||
|
{sku:'SKU-009', name:'Old Widget', category:'Widgets', price:22.00, active:false},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
summary_mv: {
|
||||||
|
schema: 'rpt', rows: 3442,
|
||||||
|
cols: [
|
||||||
|
{name:'period', type:'text'},
|
||||||
|
{name:'channel', type:'text'},
|
||||||
|
{name:'revenue', type:'numeric'},
|
||||||
|
{name:'units', type:'numeric'},
|
||||||
|
],
|
||||||
|
sample: [
|
||||||
|
{period:'2025-01', channel:'WHS', revenue:1240000, units:32100},
|
||||||
|
{period:'2025-01', channel:'DIR', revenue:980000, units:24400},
|
||||||
|
{period:'2025-01', channel:'ECOM', revenue:410000, units:10200},
|
||||||
|
{period:'2025-02', channel:'WHS', revenue:1190000, units:30800},
|
||||||
|
{period:'2025-02', channel:'DIR', revenue:1020000, units:25500},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function peekTable(name) {
|
||||||
|
const d = peekData[name]
|
||||||
|
if (!d) return
|
||||||
|
const cols = d.cols
|
||||||
|
const sample = d.sample
|
||||||
|
|
||||||
|
let html = '<div class="px-3 pt-3 pb-1 text-gray-400 uppercase tracking-wide font-medium text-xs">Columns</div>'
|
||||||
|
html += '<table class="w-full mb-3"><thead><tr class="text-left text-gray-400 border-b border-gray-100 bg-gray-50"><th class="px-3 py-1">name</th><th class="px-3 py-1">type</th></tr></thead><tbody>'
|
||||||
|
cols.forEach(c => {
|
||||||
|
html += `<tr class="border-t border-gray-50"><td class="px-3 py-1 font-mono text-gray-700">${c.name}</td><td class="px-3 py-1 text-gray-400">${c.type}</td></tr>`
|
||||||
|
})
|
||||||
|
html += '</tbody></table>'
|
||||||
|
|
||||||
|
html += '<div class="px-3 py-1 text-gray-400 uppercase tracking-wide font-medium text-xs border-t border-gray-100">Sample rows</div>'
|
||||||
|
html += '<div class="overflow-x-auto"><table class="text-xs" style="min-width:100%"><thead><tr class="text-left text-gray-400 bg-gray-50 border-b border-gray-100">'
|
||||||
|
cols.forEach(c => { html += `<th class="px-3 py-1 font-medium whitespace-nowrap">${c.name}</th>` })
|
||||||
|
html += '</tr></thead><tbody>'
|
||||||
|
sample.forEach(row => {
|
||||||
|
html += '<tr class="border-t border-gray-50">'
|
||||||
|
cols.forEach(c => {
|
||||||
|
const v = row[c.name]
|
||||||
|
html += `<td class="px-3 py-1 font-mono whitespace-nowrap ${v == null ? 'text-gray-300' : 'text-gray-600'}">${v == null ? 'null' : v}</td>`
|
||||||
|
})
|
||||||
|
html += '</tr>'
|
||||||
|
})
|
||||||
|
html += '</tbody></table></div>'
|
||||||
|
|
||||||
|
document.getElementById('peek-title').textContent = d.schema + '.' + name
|
||||||
|
document.getElementById('peek-rowcount').textContent = d.rows.toLocaleString() + ' rows'
|
||||||
|
document.getElementById('peek-body').innerHTML = html
|
||||||
|
document.getElementById('peek-modal').classList.remove('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePeek() {
|
||||||
|
document.getElementById('peek-modal').classList.add('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sidebar toggle ─────────────────────────────────────────────
|
||||||
|
function toggleSidebar() {
|
||||||
|
const sb = document.getElementById('sidebar')
|
||||||
|
const expanded = sb.classList.contains('expanded')
|
||||||
|
sb.classList.toggle('expanded', !expanded)
|
||||||
|
sb.classList.toggle('collapsed', expanded)
|
||||||
|
localStorage.setItem('sb', expanded ? 'collapsed' : 'expanded')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab switching ──────────────────────────────────────────────
|
||||||
|
function show(view) {
|
||||||
|
['setup','baseline','forecast'].forEach(v => {
|
||||||
|
document.getElementById('view-' + v).classList.add('hidden')
|
||||||
|
const btn = document.getElementById('tab-' + v)
|
||||||
|
btn.classList.remove('bg-blue-50','text-blue-700')
|
||||||
|
btn.classList.add('text-gray-500')
|
||||||
|
})
|
||||||
|
document.getElementById('view-' + view).classList.remove('hidden')
|
||||||
|
const active = document.getElementById('tab-' + view)
|
||||||
|
active.classList.add('bg-blue-50','text-blue-700')
|
||||||
|
active.classList.remove('text-gray-500')
|
||||||
|
if (view === 'baseline') setTimeout(drawTimeline, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Log drawer ─────────────────────────────────────────────────
|
||||||
|
function toggleLog() {
|
||||||
|
const panel = document.getElementById('log-panel')
|
||||||
|
const arrow = document.getElementById('log-arrow')
|
||||||
|
panel.classList.toggle('hidden')
|
||||||
|
arrow.textContent = panel.classList.contains('hidden') ? '▶' : '▼'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Timeline canvas ────────────────────────────────────────────
|
||||||
|
function parseDate(s) {
|
||||||
|
const [y,m,d] = s.split('-').map(Number)
|
||||||
|
return new Date(y, (m||1)-1, (d||1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMonths(date, months) {
|
||||||
|
const d = new Date(date)
|
||||||
|
d.setMonth(d.getMonth() + months)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTimeline() {
|
||||||
|
const from = document.getElementById('date-from').value
|
||||||
|
const to = document.getElementById('date-to').value
|
||||||
|
const yr = parseInt(document.getElementById('offset-yr').value) || 0
|
||||||
|
const mo = parseInt(document.getElementById('offset-mo').value) || 0
|
||||||
|
drawTimelineOn('timeline-canvas', from, to, yr, mo)
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTimelineOn(canvasId, fromStr, toStr, yr, mo) {
|
||||||
|
const canvas = document.getElementById(canvasId)
|
||||||
|
if (!canvas) return
|
||||||
|
const W = canvas.offsetWidth || 500
|
||||||
|
canvas.width = W * devicePixelRatio
|
||||||
|
canvas.height = 90 * devicePixelRatio
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.scale(devicePixelRatio, devicePixelRatio)
|
||||||
|
|
||||||
|
const H = 90
|
||||||
|
const PAD = { l: 8, r: 8, top: 20 }
|
||||||
|
const trackH = 22
|
||||||
|
const gap = 10
|
||||||
|
const srcY = PAD.top
|
||||||
|
const projY = srcY + trackH + gap
|
||||||
|
|
||||||
|
const srcStart = parseDate(fromStr)
|
||||||
|
const srcEnd = parseDate(toStr)
|
||||||
|
if (isNaN(srcStart) || isNaN(srcEnd)) return
|
||||||
|
|
||||||
|
const offsetMo = yr * 12 + mo
|
||||||
|
const projStart = addMonths(srcStart, offsetMo)
|
||||||
|
const projEnd = addMonths(srcEnd, offsetMo)
|
||||||
|
|
||||||
|
// window: from 1 month before srcStart to 1 month after projEnd
|
||||||
|
const winStart = addMonths(srcStart, -1)
|
||||||
|
const winEnd = addMonths(projEnd, 1)
|
||||||
|
const winMs = winEnd - winStart
|
||||||
|
const drawW = W - PAD.l - PAD.r
|
||||||
|
|
||||||
|
function xOf(date) {
|
||||||
|
return PAD.l + ((date - winStart) / winMs) * drawW
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, W, H)
|
||||||
|
|
||||||
|
// ── axis line ──
|
||||||
|
ctx.strokeStyle = '#e5e7eb'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(PAD.l, srcY - 8)
|
||||||
|
ctx.lineTo(PAD.l + drawW, srcY - 8)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// ── month ticks ──
|
||||||
|
ctx.fillStyle = '#9ca3af'
|
||||||
|
ctx.font = '9px system-ui'
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
const tickStart = new Date(winStart.getFullYear(), winStart.getMonth(), 1)
|
||||||
|
for (let d = new Date(tickStart); d <= winEnd; d = addMonths(d, 1)) {
|
||||||
|
const x = xOf(d)
|
||||||
|
if (x < PAD.l || x > PAD.l + drawW) continue
|
||||||
|
ctx.strokeStyle = '#f3f4f6'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x, srcY - 8)
|
||||||
|
ctx.lineTo(x, projY + trackH)
|
||||||
|
ctx.stroke()
|
||||||
|
// year label on Jan
|
||||||
|
if (d.getMonth() === 0) {
|
||||||
|
ctx.fillStyle = '#6b7280'
|
||||||
|
ctx.font = 'bold 9px system-ui'
|
||||||
|
ctx.fillText(d.getFullYear(), x, srcY - 10)
|
||||||
|
ctx.font = '9px system-ui'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── source band ──
|
||||||
|
const sx1 = xOf(srcStart), sx2 = xOf(srcEnd)
|
||||||
|
ctx.fillStyle = '#dbeafe'
|
||||||
|
ctx.strokeStyle = '#93c5fd'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
roundRect(ctx, sx1, srcY, sx2 - sx1, trackH, 4, true, true)
|
||||||
|
ctx.fillStyle = '#1d4ed8'
|
||||||
|
ctx.font = '10px system-ui'
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
ctx.fillText('Source ' + fromStr + ' → ' + toStr, sx1 + 6, srcY + 14)
|
||||||
|
|
||||||
|
if (offsetMo > 0) {
|
||||||
|
// ── projected band ──
|
||||||
|
const px1 = xOf(projStart), px2 = xOf(projEnd)
|
||||||
|
ctx.fillStyle = '#dcfce7'
|
||||||
|
ctx.strokeStyle = '#86efac'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
roundRect(ctx, px1, projY, px2 - px1, trackH, 4, true, true)
|
||||||
|
ctx.fillStyle = '#15803d'
|
||||||
|
ctx.font = '10px system-ui'
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
ctx.fillText('Projected ' + fmtDate(projStart) + ' → ' + fmtDate(projEnd), px1 + 6, projY + 14)
|
||||||
|
|
||||||
|
// ── offset arrow ──
|
||||||
|
const arrowY = srcY + trackH / 2
|
||||||
|
ctx.strokeStyle = '#94a3b8'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.setLineDash([3, 3])
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(sx1, arrowY)
|
||||||
|
ctx.lineTo(px1 - 2, arrowY)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.setLineDash([])
|
||||||
|
// arrowhead
|
||||||
|
ctx.fillStyle = '#94a3b8'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(px1 + 4, arrowY)
|
||||||
|
ctx.lineTo(px1 - 4, arrowY - 4)
|
||||||
|
ctx.lineTo(px1 - 4, arrowY + 4)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fill()
|
||||||
|
// label
|
||||||
|
const midX = (sx1 + px1) / 2
|
||||||
|
ctx.fillStyle = '#64748b'
|
||||||
|
ctx.font = '9px system-ui'
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
ctx.fillText('+' + (yr ? yr + 'yr ' : '') + (mo ? mo + 'mo' : ''), midX, arrowY - 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundRect(ctx, x, y, w, h, r, fill, stroke) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x + r, y)
|
||||||
|
ctx.lineTo(x + w - r, y)
|
||||||
|
ctx.quadraticCurveTo(x + w, y, x + w, y + r)
|
||||||
|
ctx.lineTo(x + w, y + h - r)
|
||||||
|
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h)
|
||||||
|
ctx.lineTo(x + r, y + h)
|
||||||
|
ctx.quadraticCurveTo(x, y + h, x, y + h - r)
|
||||||
|
ctx.lineTo(x, y + r)
|
||||||
|
ctx.quadraticCurveTo(x, y, x + r, y)
|
||||||
|
ctx.closePath()
|
||||||
|
if (fill) ctx.fill()
|
||||||
|
if (stroke) ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDate(d) {
|
||||||
|
return d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Help popovers ──────────────────────────────────────────────
|
||||||
|
const helpText = {
|
||||||
|
segment: {
|
||||||
|
title: 'What is a segment?',
|
||||||
|
body: 'A segment is one query against the source table. Each segment appends rows independently — you can layer multiple segments to build up the baseline (e.g. core actuals, open orders, special items). Each is independently undoable.'
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
title: 'Filters',
|
||||||
|
body: 'Define what rows to pull from the source table. You can use any date or filter-role column. At least one filter is required. Multiple filters are ANDed together.\n\nFor date ranges use BETWEEN. For lists use IN. For exact matches use =.'
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
title: 'Date offset',
|
||||||
|
body: 'Shifts the primary date column forward by this amount when rows are inserted. For example, with offset = 1 yr, a row with order_date 2025-03-15 is stored as 2026-03-15.\n\nLeave at 0 to keep dates as-is (useful for open orders or non-date segments).'
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
title: 'Timeline preview',
|
||||||
|
body: 'The blue band shows the source period — the date range of rows being pulled from the source table.\n\nThe green band shows where those dates will land after the offset is applied. The arrow shows the shift.'
|
||||||
|
},
|
||||||
|
reference: {
|
||||||
|
title: 'Reference rows',
|
||||||
|
body: 'Reference rows are prior-period actuals loaded for comparison only. They appear in the pivot alongside your forecast rows but are never touched by scale, recode, or clone operations.'
|
||||||
|
},
|
||||||
|
operations: {
|
||||||
|
title: 'Operations',
|
||||||
|
body: 'Click a cell in the pivot to set the slice, then choose an operation:\n\n• Scale — add or subtract value/units across the slice\n• Recode — reassign dimension values (e.g. rename a customer)\n• Clone — copy a slice to a new set of dimension values\n\nAll operations are incremental and undoable from the log.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHelp(btn, key) {
|
||||||
|
const data = helpText[key]
|
||||||
|
if (!data) return
|
||||||
|
const box = document.getElementById('help-box')
|
||||||
|
const over = document.getElementById('help-overlay')
|
||||||
|
box.innerHTML = `<div class="font-semibold text-white mb-1.5">${data.title}</div><div class="text-gray-300 whitespace-pre-line">${data.body}</div>`
|
||||||
|
const rect = btn.getBoundingClientRect()
|
||||||
|
box.style.top = (rect.bottom + 6 + window.scrollY) + 'px'
|
||||||
|
box.style.left = Math.min(rect.left, window.innerWidth - 300) + 'px'
|
||||||
|
box.classList.add('open')
|
||||||
|
over.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeHelp() {
|
||||||
|
document.getElementById('help-box').classList.remove('open')
|
||||||
|
document.getElementById('help-overlay').classList.add('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', drawTimeline)
|
||||||
|
|
||||||
|
// restore sidebar state
|
||||||
|
const sbState = localStorage.getItem('sb') || 'expanded'
|
||||||
|
const sb = document.getElementById('sidebar')
|
||||||
|
sb.classList.toggle('expanded', sbState === 'expanded')
|
||||||
|
sb.classList.toggle('collapsed', sbState === 'collapsed')
|
||||||
|
|
||||||
|
show('forecast')
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user