Lattice Grid Buy a licence

blog

Loss-Making Orders: an Exceptions Grid, Not an Export

Building…
Loading a live grid…

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.

Read next

  • Units and Revenue by SKU: Unnest Order Lines

    Flatten a nested array into a JavaScript table: unnest each order's line items and total units and revenue by SKU, in a derived grid that follows the orders grid's filters.

  • Overdue Orders: a Derived Grid That Maintains Itself

    Build an overdue orders exception report as a derived data grid from your orders grid, filtered with where, so it never goes stale when someone filters the table.

  • Top Customers by Revenue: the 80% Cutoff

    Pareto analysis in JavaScript: a top-customers grid from your orders grid, kept until the running revenue share crosses 80%, and re-applied whenever the orders are filtered.

All posts RSS