Loss-Making Orders: an Exceptions Grid, Not an Export
Whoever signs off on an order approves the price on it, not always what it will cost to fill. That number lands later, from a supplier invoice or a freight surcharge, and by the time it does, the order has shipped. So someone on the finance or ops side ends up running the same check at the end of the day: export the orders, put amount next to cost, filter to the rows where cost wins, and forward the list. Re-run it tomorrow, because today’s export is already out of date the moment one more invoice lands. A conditional row filter in the JavaScript grid itself keeps that list live instead.
A derived grid turns that export into a grid that is never out of date,
because it is never a separate step. Point a second grid at the orders grid
with a where clause, (r) => r.amount < r.cost, and it holds exactly the rows that
qualify, live, for as long as the orders grid exists. No groupBy is
needed; with none, the matching rows just pass through as themselves. Filter
the orders grid to a region or a customer and the exceptions list narrows to
match, because it reads the same filtered rows the table shows, not a copy
taken when the page loaded.
That removes the export and the end-of-day timing both. The list a finance person used to build by hand, once a day, now exists continuously: an order that was fine when it was invoiced and stops being fine the moment a freight surcharge pushes its cost past its price shows up on the next frame, not at month end when someone finally reconciles the two. Six lines of configuration replace a daily habit, and the list is never stale because there was never a copy to go stale.
Plain JavaScript
import { createGrid } from '@toclocoinc/lattice-grid';
const orderRows = [
{ id: 1, customer: 'Orion Freight', region: 'EMEA', amount: 480, cost: 410 },
{ id: 2, customer: 'Birchwood Supplies', region: 'Americas', amount: 260, cost: 310 },
{ id: 3, customer: 'Kestrel Metals', region: 'APAC', amount: 910, cost: 740 },
{ id: 4, customer: 'Harlow & Vance', region: 'EMEA', amount: 175, cost: 150 },
{ id: 5, customer: 'Union Cold Storage', region: 'Americas', amount: 640, cost: 455 },
{ id: 6, customer: 'Priya Textiles', region: 'APAC', amount: 220, cost: 265 },
{ id: 7, customer: 'Dunmore Logistics', region: 'EMEA', amount: 530, cost: 390 },
{ id: 8, customer: 'Castlegate Foods', region: 'Americas', amount: 95, cost: 140 },
{ id: 9, customer: 'Verity Chemicals', region: 'APAC', amount: 860, cost: 610 },
{ id: 10, customer: 'Oakridge Components', region: 'EMEA', amount: 310, cost: 295 },
{ id: 11, customer: 'Falkirk Machine Co', region: 'Americas', amount: 740, cost: 520 },
{ id: 12, customer: 'Solano Hardware', region: 'APAC', amount: 180, cost: 225 },
{ id: 13, customer: 'Bellhaven Textiles', region: 'EMEA', amount: 605, cost: 480 },
{ id: 14, customer: 'Marchetti Imports', region: 'Americas', amount: 340, cost: 300 },
{ id: 15, customer: 'Driftwood Supplies', region: 'APAC', amount: 515, cost: 410 },
{ id: 16, customer: 'Hallcross Metals', region: 'EMEA', amount: 175, cost: 230 },
{ id: 17, customer: 'Oxford Components', region: 'Americas', amount: 820, cost: 600 },
{ id: 18, customer: 'Pinebrook Chemicals', region: 'APAC', amount: 260, cost: 300 },
{ id: 19, customer: 'Ashgate Freight', region: 'EMEA', amount: 450, cost: 370 },
{ id: 20, customer: 'Tamarind Logistics', region: 'Americas', amount: 390, cost: 340 },
];
const columns = [
{ field: 'id', title: 'Order' },
{ field: 'customer', title: 'Customer' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number' },
{ field: 'cost', title: 'Cost', type: 'number' },
];
const orders = createGrid(document.querySelector('#orders'), {
rowKey: 'id',
columns,
rows: orderRows,
});
// A second grid holding only the orders losing money, live.
const exceptions = createGrid(document.querySelector('#exceptions'), {
columns,
source: {
mode: 'derived',
from: orders, // the grid to read
follow: 'filtered', // read whatever the filters leave
refresh: 'live', // re-derive on every change, not just idle frames
where: (r) => r.amount < r.cost,
},
});
Filter the orders grid to EMEA, and exceptions drops from six rows to
one, Hallcross Metals, with nothing wired between the two grids by hand.
Edit a row still showing a profit so its cost passes its amount, say a
freight surcharge lands on order 1, and exceptions gains that row on the
next frame, with no query re-run anywhere in the example above.
React
import React from 'react';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';
// Build the component once, at module scope, not on every render.
const LatticeGrid = createLatticeGrid({ React, createGrid });
const columns = [
{ field: 'id', title: 'Order' },
{ field: 'customer', title: 'Customer' },
{ field: 'region', title: 'Region', filter: { type: 'set' } },
{ field: 'amount', title: 'Amount', type: 'number' },
{ field: 'cost', title: 'Cost', type: 'number' },
];
function LossMakingOrders({ orderRows }) {
const [orders, setOrders] = React.useState(null);
return (
<>
<LatticeGrid
rowKey="id"
columns={columns}
rows={orderRows}
onGridReady={setOrders}
style={{ height: 320 }}
/>
{orders && (
<LatticeGrid
columns={columns}
source={{
mode: 'derived',
from: orders,
follow: 'filtered',
refresh: 'live',
where: (r) => r.amount < r.cost,
}}
style={{ height: 160 }}
/>
)}
</>
);
}
onGridReady hands back the live orders grid as soon as it mounts, which is
what the exceptions panel’s from needs; the panel itself only renders once
that instance exists. Filtering the orders grid narrows the exceptions panel
the same way it does in the plain example, because both read the grid’s
public API, not a copy of its rows.
The conditional row filter, option by option
| Option | What it does |
|---|---|
mode: 'derived' |
This grid’s rows are computed from another grid’s, not loaded from an array or a server. |
from |
The grid to read. |
follow: 'filtered' |
Read the rows the filters leave. The default. |
where |
A row predicate, applied before grouping. With no groupBy the rows pass through as themselves, which is how an exceptions list is built. |
refresh: 'live' |
Re-derive on every change to the source. The default, 'idle', coalesces changes to one re-derive per frame, which suits a summary panel; 'live' is for a list meant to catch a change the moment it happens. |
What the example actually produced
Running the plain JavaScript example above with createHeadlessGrid from the
published package, and printing exceptions’ rows, gives:
| Order | Customer | Region | Amount | Cost |
|---|---|---|---|---|
| 2 | Birchwood Supplies | Americas | 260 | 310 |
| 6 | Priya Textiles | APAC | 220 | 265 |
| 8 | Castlegate Foods | Americas | 95 | 140 |
| 12 | Solano Hardware | APAC | 180 | 225 |
| 16 | Hallcross Metals | EMEA | 175 | 230 |
| 18 | Pinebrook Chemicals | APAC | 260 | 300 |
Six rows out of twenty orders. Filtering the orders grid to EMEA leaves
exceptions with exactly one of those six, Hallcross Metals, and editing
order 1’s cost from 410 to 500, so its amount of 480 no longer covers it,
adds a seventh row to the list on the next frame, with no second read of the
orders grid anywhere in the code.
The derived and chained grids guide covers where
alongside grouping, joins and profiles, and how a derived grid can chain
from another derived grid, in full. For the source-mode framing of the same
feature, and the other data sources it sits beside, see
connect your data.