order-board
The "Order Board" page design (first built in PlusV1 for Customer Order) -- a Table view plus grouped views (By Customer / By Vendor / By Status ...) switched inside a top filter panel, page-driven ch
SKILL.md.claude/skills/order-board/SKILL.md
GitHubOrder Board -- the Customer Order page design (from PlusV1)
Name: Order Board. When the user says "use Order Board", "same design as Customer Order in PlusV1" or "make it like Customer Order", build this.
Copied from PlusV1's order-board skill (2026-10-18) and adapted to ProV1. The
shared building blocks were ported then (see "Shared pieces in ProV1"); no ProV1
page uses the design yet, so the reference implementations are PlusV1's.
Reference implementations (PlusV1 -- read the closest one, copy its structure)
Sibling repo: /Users/mohamed/Documents/MyProjects/PlusV1. Page ids there are
PlusV1's, not ProV1's -- look up the ProV1 page by Label.
| PlusV1 page | Files (under PlusV1/src/) |
Grouped views | Party drawer | Print / Preview |
|---|---|---|---|---|
| Customer Order -- the original | pages/CustomerOrderPage.jsx, pages/CustomerOrderViewDrawer.jsx |
By Customer, By Truck Type | Customer (open orders, state <= 80) | Co_CustomerOrder |
| Shipment Tracking | pages/logistics/ShipmentTrackingPage.jsx, ShipmentViews.jsx |
By Vendor / Status / Assigned To / Item, Calendar | Vendor (open shipments) | -- |
| Purchase Order | pages/purchasing/PurchaseOrderPage.jsx, PurchaseOrderViews.jsx |
By Vendor / Status / Buyer | Vendor (open POs) | PO_PRINT |
| Customer Invoice | pages/sales/CustomerInvoicePage.jsx |
By Customer / Sales Person | Customer (uncollected) | -- |
ProV1 (first Order Board page, 2026-10-18): Customer Order (page 5) --
src/pages/CustomerOrderPage.jsx + src/pages/CustomerOrderBoard.jsx
(BoardControls / GroupedView / CustomerDrawer / PreviewPanel, reusable
shapes), server SQLScript/AddCustomerOrderViewsAndFilters.sql. Views: By
Customer / By Status / By Salesperson; status checklist (LIST link on
OrderState, default New..Confirmed); customer drawer = Detail "Customer Open
Orders" (state <= 80, all dates -- the grid is still limited by the Order Date
panel default, which the server applies when a request sends no date);
CO_PRINT / CO_INVOICE print icon + stacked Preview. Read this one first in ProV1.
GridPage page-driven mode now waits for the panel's seeded defaults
(DynamicFilterBar onSeed) before its first load.
When you port one, keep ProV1's own page logic (its drawers, operations, SPs, request IDs) and take only the design -- PlusV1's page code calls PlusV1 ops. Add the first ProV1 Order Board page to this table.
Ask first (one AskUserQuestion, multiSelect)
- Which grouped views besides Table (the natural "by" fields of the record: party, status, owner / buyer / sales person, type ...).
- Which pieces: status checklist, party drawer, print icon + Preview, top filter panel. Default: all that apply.
Shared pieces in ProV1 (ported 2026-10-18)
| Piece | ProV1 API |
|---|---|
| Top filter panel | GridPage gridPresentation={{ filterLayout: 'top' }} (only filterLayout is ported -- PlusV1's columns / appearance / compare / rowStyle are not; port them with DataGrid support if a page needs them) |
| View switch slot | GridPage filterExtra={...} -> DynamicFilterBar extra={...} (shown first in the panel, also when collapsed) |
| Page-driven filters | GridPage extraFilters={[{ LinkID, Values } | { LinkID, From, To }]} + hideFilterFields={['OrderState', ...]}; DynamicFilterBar hideFields |
| Checklist | src/shared/CheckListDropdown.jsx -- { label, allText, options: [{value,label}], picked, onToggle, onClear, onOnly, minWidth } |
| Row print icon | <ReportMenu target="Form" compact reports={reports} context={{ <Key>: id }} /> |
| Stacked Preview | import { buildReportHTML, previewHTML } from '../shared/ReportMenu.jsx' -- buildReportHTML(report, pageGroupId, ctx, { pageSize, orientation }) returns { html, layout, meta, params } and throws Error with .server = true for a refusal (show its message). ProV1's 4th arg is opts, not PlusV1's extras. previewHTML(html, layout, { bg, pad }) |
| Money by currency | not shared -- each PlusV1 page defines ccyText = (t) => Object.entries(t).sort((a, b) => b[1] - a[1]).map(([c, v]) => \${compact(v)}${c ? ' ' + c : ''}`).join(' · ') || '0'`; copy it with the page |
The pieces
1. View switch -- inside the filter panel
[ ['table','Table',Icon], ['customer','By Customer',Icon], ... ]segmented buttons, orange when active; choice kept inlocalStorage(<page>View, every read/write in try/catch).- Passed as
filterExtra(Table view) /extra(grouped views), so it sits inside the filter panel in every view, next to the page-driven filters. - Table view = the generic
GridPagewithgridPresentation={{ filterLayout: 'top' }}so the panel stays on top (no jump to a sidebar when switching).
2. Page-driven filters (status checklist, single date, ETA range ...)
- Server: a
V1QueryFilter+ a panel link (UsePanel = 1) on the Base query, e.g.LISTonOrderState. Hidden from the generic widgets viahideFilterFields(GridPage) /hideFields(DynamicFilterBar). - Page: reads the link (
GetQueryFilterLinks, matchQueryField+UsePanel), the options (GetFilterOptions { LinkID }), and sends{ LinkID, Values }/{ LinkID, From, To }throughextraFilters(GridPage -- it reloads when they change, no Apply gate) or appended to the grouped view'sGetGridDataFilters. - Checklist UI:
CheckListDropdown.
3. Grouped views (<X>GroupedView)
- Same Base query rows:
GetGridData { PageGroupID, Filters: [...panel, ...extraFilters] }with its ownDynamicFilterBar layout="top"(+extra={viewSwitch},hideFields,onApply-> reload). Grouping is presentation only -- no SQL, no Group By config. - Group row: chevron, icon, key + name (party name clickable -> party drawer),
status-mix pills with counts, Preview button (if a print exists), then
right-aligned figures (count, last date, open/outstanding, total by currency
via
ccyText). - Toolbar: title + "· n groups · n records · total", Sort by segmented (Total / Count / Name / Last ...; click again reverses, arrow shows dir), search box (group key/name + record numbers), Expand all / Collapse all, and the page's own buttons (+ New, Report).
- Expanded group: a table of its records (record # as an orange underlined
button -> entity drawer, double-click row also opens it; status pill; money
right-aligned with currency; print icon cell). Row hover =
--orange-soft. - Optional: sortable column headers, expandable record lines with an "Expand lines / Collapse lines" toggle capped at 50 records (one request per record -- see the rate limit below).
4. Entity drawer
- The page's existing drawer if there is one (e.g. ProV1's
CustomerOrderViewDrawer.jsx), else a read-onlyDetailsDrawerreading Detail queries withView(KeyField = the record key). - Gate with an operation (
<X>_VIEW, BackendOperationView) -- the page checksuser.AllowedOperations; the server checks the page / query grants.
5. Party drawer ("all open records of this customer / vendor")
- A Detail query
V1QD<Party>Open<Records>= the Base viewWHERE <open states>, keyed by the party number, read withView-- so it ignores the page's filters by design. Copy the Base query'sV1UserQueryPermissionsrows onto it (same SQLFilter -- e.g. the Sales ID scopes) or users see more than on the grid. - Drawer: header (icon, "Party · open records", number + name), summary cards, status chips (All + each with count), the records table (same row component), "Preview all" when a print exists. Esc closes.
6. Print icon + Preview
- Print icon:
ReportMenu compactwithreportsfetched once (GetPageReports { PageGroupID, Target: 'Form' }). - Preview: side panel (portal) that builds each record's print with
buildReportHTML+previewHTML, two at a time, stacked iframes (sections needflexShrink: 0, re-measure height after load), chips to jump, per-record Print, IntersectionObserver for the active chip. The report must be registered in Report Master and linked to the page (Form) -- seequery-engine/reference/printed-reports.md.
Server checklist (one idempotent script SQLScript/Add<Page>ViewsAndFilters.sql)
SaveQueryFilterinserts a NEW row unless given a QueryFilterID -- always guard withIF NOT EXISTS (... FilterName = ...)(or insert theV1QueryFilter/V1QueryFilterLinkrows directly, as theRegister*.sqlscripts do).- Links guarded by
IF NOT EXISTS (... QueryField ... UsePanel = 1). - Fields used by LIST / EQUALS links or as View keys must be registered in
V1QueryFieldswith the rightDataType(numberemits unquoted). - Detail queries:
CREATE OR ALTER VIEW [PLS].[V1QD...], insert intoV1QueryMaster(QueryTypeDetail), map inV1PageQueries, register the key field. - Deploy with
.claude/skills/live-db/db.sh(approval each time). Keep literals ASCII where you can: some characters (e.g.→) arrive as?through the gateway even insideN'...'; Arabic inN'...'has worked.
Size limits -- check before shipping
- Gateway (
api/Plus.Api): 10,000 rows and 8 MB per result (Gateway:MaxResultRows/MaxResultBytes, max 100,000 / 32 MB), measured as UTF-8 JSON with non-ASCII escaped (\uXXXX= 6 bytes per Arabic char). Measure a realistic range:db.sh --raw+ sumjson.dumps(row, separators=(',',':'))bytes. Over the limit the user sees "Result exceeds the size limit. Narrow the filters." - Fix by: an explicit slim column list in the Base QuerySQL (sort in
QueryOrderBy, neverORDER BYin the SQL), heavy text moved to a per-record Detail query for the drawer, and aDATE_RANGEpanel link withFixedFrom/FixedTotokens (TODAY, MONTH_BEGIN, MONTH_END, YEAR_BEGIN, YEAR_END) so a range always applies server-side. - Gateway rate limit: 120 requests / minute per IP (hard-coded in
Program.cs, 429 "Too many requests."). Keep per-record fan-out (lines, previews) bounded -- two previews at a time, lines capped at 50 records.
Frontend rules
- Layering: party drawer
zIndex 990< entity drawer 1000-1200 < Preview 1300 (< app pop-ups;CheckListDropdownpopover 2000). View drawers close on Esc; stacked pop-ups use capture-phasestopImmediatePropagation. - Colours only via
var(--token)(src/index.css); status tones as[fg, bg]pairs (e.g.['var(--green)', 'var(--green-soft)']). npx esbuild <file> --loader:.jsx=jsx --outfile=/dev/null, thennpm run build. Shipping = push tomain(GitHub Pages rebuilds).