dentira-grid-distributor
v1.0.3
Published
A React popup component for splitting and distributing invoice lines. Built with AG Grid.
Readme
dentira-grid-distributor
A React popup component for splitting and distributing invoice lines. Built with AG Grid.
Users can add, duplicate, split, merge, copy/paste rows in an Excel-like grid, then confirm to get the result as a JSON array. The popup validates that lines balance against the invoice amount before allowing confirmation.
Install
npm install dentira-grid-distributor ag-grid-community ag-grid-reactQuick Start
import { useState } from 'react';
import { DistributionGrid } from 'dentira-grid-distributor';
import 'dentira-grid-distributor/style.css';
import type { GridColumn, InvoiceContext } from 'dentira-grid-distributor';
function InvoicePage() {
const [open, setOpen] = useState(false);
const invoice: InvoiceContext = {
subtotal: 10000,
shipping: 500,
tax: 1050,
discount: 550,
invoiceAmount: 11000,
currencyCode: 'USD',
invoiceNumber: 'INV-2026-0087',
};
const columns: GridColumn[] = [
{ field: 'lineType', headerName: 'Line Type', hidden: true },
{ field: 'glAccount', headerName: 'GL Account', width: 140, required: true, type: 'select',
options: [
{ value: '5010', label: 'Cost of Goods Sold' },
{ value: '6010', label: 'Office Supplies' },
{ value: '6060', label: 'Software & Licenses' },
],
},
{ field: 'locationId', headerName: 'Location Id', width: 130, type: 'select',
options: [
{ value: 'WH-01', label: 'Warehouse 1' },
{ value: 'WH-02', label: 'Warehouse 2' },
],
},
{ field: 'sku', headerName: 'SKU', width: 130 },
{ field: 'description', headerName: 'Description', flex: 1, minWidth: 160 },
{ field: 'qty', headerName: 'QTY', width: 80, type: 'number', isQuantityField: true },
{ field: 'unitPrice', headerName: 'UNIT PRICE', width: 120, type: 'currency', isUnitPriceField: true },
{ field: 'lineTotal', headerName: 'LINE TOTAL', width: 130, type: 'currency', isSubtotalField: true, isAmountField: true, computed: true },
];
const initialRows = [
{ lineType: 'item', glAccount: '6010', locationId: 'WH-01', sku: 'SKU-1001', description: 'Office Supplies', qty: 30, unitPrice: 200, lineTotal: 6000 },
{ lineType: 'item', glAccount: '6060', locationId: 'WH-02', sku: 'SKU-2005', description: 'Software License', qty: 20, unitPrice: 200, lineTotal: 4000 },
{ lineType: 'shipping', glAccount: '', locationId: '', sku: '', description: 'Shipping', qty: 1, unitPrice: 500, lineTotal: 500 },
{ lineType: 'tax', glAccount: '', locationId: '', sku: '', description: 'Tax', qty: 1, unitPrice: 1050, lineTotal: 1050 },
{ lineType: 'discount', glAccount: '', locationId: '', sku: '', description: 'Discount', qty: 1, unitPrice: -550, lineTotal: -550 },
];
return (
<>
<button onClick={() => setOpen(true)}>Distribute Invoice</button>
<DistributionGrid
open={open}
onClose={() => setOpen(false)}
invoice={invoice}
columns={columns}
initialRows={initialRows}
onConfirm={(rows) => {
console.log(rows);
// [
// { lineNumber: 1, lineType: 'item', glAccount: '6010', sku: 'SKU-1001', ... },
// { lineNumber: 2, lineType: 'shipping', description: 'Shipping', ... },
// ]
setOpen(false);
}}
/>
</>
);
}Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| open | boolean | Yes | Show/hide the popup |
| onClose | () => void | Yes | Called on cancel, escape, or overlay click |
| onConfirm | (rows) => void | Yes | Returns { lineNumber, ...fields }[] |
| invoice | InvoiceContext | Yes | Invoice totals for balance validation |
| columns | GridColumn[] | Yes | Dynamic column definitions |
| initialRows | Record<string, any>[] | No | Pre-populate rows on open |
| allowOverDistribution | boolean | No | Allow lines total to exceed invoice amount |
| requireFullDistribution | boolean | No | Require exact balance (default true) |
| readOnly | boolean | No | Disable all editing |
| theme | object | No | Customize colors (see Theme section) |
InvoiceContext
interface InvoiceContext {
subtotal: number;
shipping: number;
tax: number;
discount: number;
invoiceAmount: number; // lines must balance to this
currencyCode: string; // e.g. 'USD', 'EUR', 'GBP', 'INR'
invoiceNumber?: string; // shown in popup header
description?: string;
quantity?: number;
}Column Definition
Columns are fully dynamic. Define any fields you need:
interface GridColumn {
field: string; // key in row data
headerName: string; // column header label
width?: number; // fixed width in px
flex?: number; // flexible width
minWidth?: number;
editable?: boolean; // default: true
pinned?: 'left' | 'right';
hidden?: boolean; // hide from grid, include in onConfirm output
required?: boolean; // row highlighted if empty
computed?: boolean; // read-only, auto-calculated
type?: 'text' | 'number' | 'currency' | 'percentage' | 'select';
options?: { value: string; label: string }[]; // for type: 'select'
// Special field flags (one column per flag):
isQuantityField?: boolean; // tracked in footer
isUnitPriceField?: boolean; // used in auto-calc
isSubtotalField?: boolean; // auto-calc: qty * unitPrice
isAmountField?: boolean; // used for balance validation
isPercentageField?: boolean; // auto-syncs with amount
}Column Examples
Text:
{ field: 'memo', headerName: 'Memo', flex: 1, minWidth: 120 }Dropdown with search:
{ field: 'glAccount', headerName: 'GL Account', width: 140, type: 'select', required: true,
options: [
{ value: '5010', label: 'COGS' },
{ value: '6010', label: 'Office Supplies' },
],
}Select columns display value - label in cells. Dropdown includes + Add New... for custom values.
Hidden (included in output, not shown in grid):
{ field: 'lineType', headerName: 'Line Type', hidden: true }Auto-calculated (qty x unitPrice):
{ field: 'qty', headerName: 'QTY', width: 80, type: 'number', isQuantityField: true },
{ field: 'unitPrice', headerName: 'UNIT PRICE', width: 120, type: 'currency', isUnitPriceField: true },
{ field: 'lineTotal', headerName: 'LINE TOTAL', width: 130, type: 'currency', isSubtotalField: true, isAmountField: true, computed: true },Edit QTY or UNIT PRICE and LINE TOTAL auto-updates. When LINE TOTAL changes via merge/split, UNIT PRICE back-calculates.
Toolbar Actions
| Button | Description | Shortcut |
|--------|-------------|----------|
| + Row | Add a blank row | |
| Duplicate | Clone selected rows with same data | Ctrl+D |
| Split Equal | Split selected rows into N equal parts (by count or by column options) | |
| Split by % | Split by custom percentages (e.g. 60,25,15) | |
| Fix Remainder | Add penny variance to last row | |
| Merge Prorate | Distribute selected rows' amount to others by proportion, remove selected | |
| Merge Equal | Distribute equally to others, remove selected | |
| Copy | Copy to clipboard (tab-separated) | Ctrl+C |
| Delete | Remove selected rows | |
| Reset | Restore to initial rows | |
| Undo / Redo | Undo/redo row operations | Ctrl+Z / Ctrl+Y |
Copy / Paste
- Copy individual rows: Select rows, Copy -- data only
- Copy all rows: Select all, Copy -- includes header for Excel
- Paste from Excel:
Ctrl+V-- adds as new rows, auto-skips header row
Shipping / Tax / Discount as Lines
Pass charges as separate rows with a lineType hidden field:
const initialRows = [
{ lineType: 'item', sku: 'SKU-001', description: 'Product A', qty: 10, unitPrice: 500, lineTotal: 5000 },
{ lineType: 'item', sku: 'SKU-002', description: 'Product B', qty: 5, unitPrice: 1000, lineTotal: 5000 },
{ lineType: 'shipping', sku: '', description: 'Shipping', qty: 1, unitPrice: 500, lineTotal: 500 },
{ lineType: 'tax', sku: '', description: 'Tax', qty: 1, unitPrice: 1050, lineTotal: 1050 },
{ lineType: 'discount', sku: '', description: 'Discount', qty: 1, unitPrice: -550, lineTotal: -550 },
];To merge a charge into product lines: select it, click Merge Prorate or Merge Equal.
Footer Validation
Shows: Rows | Total Qty | Lines Total | Invoice Amount | Variance
- Variance green when balanced, red when not
- Confirm button disabled until balanced
Theme
Customize colors via the theme prop:
<DistributionGrid
theme={{
primaryColor: '#2563eb', // confirm button
primaryHover: '#1d4ed8', // confirm hover
bgColor: '#ffffff', // modal background
headerBgColor: '#f5f6f6', // header/toolbar/footer
textColor: '#131413', // text color
borderColor: '#dde0df', // borders
overlayColor: 'rgba(0,0,0,0.5)', // backdrop
}}
// ...other props
/>All colors are optional with sensible defaults (Dentira brand green).
Supported Currencies
USD ($) EUR GBP JPY CNY INR KRW CAD AUD CHF BRL MXN SGD and more. Currency sign shown in header and footer, not in grid cells.
onConfirm Output
[
{
"lineNumber": 1,
"lineType": "item",
"glAccount": "6010",
"locationId": "WH-01",
"sku": "SKU-1001",
"description": "Office Supplies",
"qty": 30,
"unitPrice": 210,
"lineTotal": 6300
},
{
"lineNumber": 2,
"lineType": "item",
"glAccount": "6060",
"locationId": "WH-02",
"sku": "SKU-2005",
"description": "Software License",
"qty": 20,
"unitPrice": 235,
"lineTotal": 4700
}
]Each row includes lineNumber (1-based) and all column fields. Internal id is stripped. Empty rows excluded.
Requirements
- React 18+
- AG Grid Community 35+
- Node 18+
