polish(stocktake-pdf): zebra rows, drop Location, Target→Max, prominent Status, wider Name

Visual layout cleanup on the Stocktake PDF:
- Zebra-stripe alternating rows (subtle slate-50 on even rows,
  white on odd) so adjacent rows scan easily.
- Drop the Location column — one less thing to fit on the page,
  and the operator has the /stock detail page for that.
- Rename the Target column header to 'Max' (user-facing naming).
- Move the Status column to the right edge and make it bold +
  colour-coded using STOCK_STATUS_STYLES[status].hex, matching
  the on-screen badge palette.
- Widen the Name column (18%→24%) so full stock-item names read
  in one line for most rows.
- Redistribute remaining widths so the table still totals 100%
  of the landscape A4 page width.

Column widths now: SKU 10 / Name 24 / Supplier 18 / Current 9 /
Min 8 / Max 9 / Status 16 / spacer 6 (= 100%).

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-08-03 18:23:11 +02:00
parent 6033aba18d
commit a35e715b21

View file

@ -8,6 +8,9 @@ export interface StockItemStocktakeDocumentProps {
generatedAtIso: string;
}
const ZEBRA_EVEN = "#f8fafc"; // slate-50 — subtle alternating row tint
const ZEBRA_ODD = "#ffffff";
const styles = StyleSheet.create({
page: { padding: 28, fontSize: 9, fontFamily: "Helvetica" },
header: { marginBottom: 18 },
@ -30,29 +33,40 @@ const styles = StyleSheet.create({
paddingVertical: 4,
paddingHorizontal: 4,
},
trEven: {
flexDirection: "row",
backgroundColor: ZEBRA_EVEN,
borderBottomWidth: 0.5,
borderBottomColor: "#ddd",
paddingVertical: 4,
paddingHorizontal: 4,
},
td: { fontSize: 9 },
tdRight: { fontSize: 9, textAlign: "right" },
// Columns mirror the /stock page table so a printed copy feels
// familiar to the operator. Landscape A4 gives 8 columns room to
// breathe — same allocation as the stock-orders list PDF.
cSku: { width: "9%" },
cName: { width: "18%" },
cSupplier: { width: "16%" },
cCurrent: { width: "8%" },
cMin: { width: "7%" },
cTarget: { width: "8%" },
cLocation: { width: "14%" },
cStatus: { width: "12%" },
spacer: { width: "8%" },
// Status is rendered with the status colour (from STOCK_STATUS_STYLES)
// and bold, so it pops as a right-side summary of the row.
tdStatus: { fontSize: 9, fontFamily: "Helvetica-Bold" },
// Columns mirror the on-screen /stock table — Location dropped, Target
// renamed to "Max". Name and Status widened so the row reads at a
// glance. Landscape A4 has plenty of horizontal room.
cSku: { width: "10%" },
cName: { width: "24%" },
cSupplier: { width: "18%" },
cCurrent: { width: "9%" },
cMin: { width: "8%" },
cMax: { width: "9%" },
cStatus: { width: "16%" },
spacer: { width: "6%" },
empty: { marginTop: 24, color: "#666", fontSize: 10, textAlign: "center" },
});
/**
* Stock list PDF — landscape A4, every active stock item in a
* table sorted by name. Columns mirror the on-screen /stock table
* (SKU, Name, Supplier, Current, Min, Target, Location, Status) so
* the printed copy is recognisable. The Status column uses the same
* `getStockStatus` derivation as the on-screen badge.
* minus Location, with Target renamed to "Max" and the Name column
* widened. Rows alternate subtle zebra tinting so adjacent rows are
* easy to scan; the Status column sits at the right edge and is
* bold + colour-coded against the on-screen badge palette.
*/
export function StockItemStocktakeDocument({
items,
@ -80,10 +94,9 @@ export function StockItemStocktakeDocument({
<Text style={[styles.th, styles.cMin, { textAlign: "right" }]}>
Min
</Text>
<Text style={[styles.th, styles.cTarget, { textAlign: "right" }]}>
Target
<Text style={[styles.th, styles.cMax, { textAlign: "right" }]}>
Max
</Text>
<Text style={[styles.th, styles.cLocation]}>Location</Text>
<Text style={[styles.th, styles.cStatus]}>Status</Text>
<Text style={[styles.th, styles.spacer]} />
</View>
@ -92,21 +105,22 @@ export function StockItemStocktakeDocument({
<Text style={styles.empty}>No active stock items.</Text>
) : (
<View style={styles.table}>
{items.map((item) => {
{items.map((item, idx) => {
const status = getStockStatus(
item.current_qty,
item.min_qty,
item.is_active,
);
const statusLabel = STOCK_STATUS_STYLES[status].label;
const statusStyle = STOCK_STATUS_STYLES[status];
const currentQty = Number(item.current_qty ?? 0);
const minQty = Number(item.min_qty ?? 0);
const targetQty =
const maxQty =
item.target_qty !== undefined && item.target_qty !== null
? Number(item.target_qty)
: null;
const rowStyle = idx % 2 === 0 ? styles.trEven : styles.tr;
return (
<View key={item.id} style={styles.tr} wrap={false}>
<View key={item.id} style={rowStyle} wrap={false}>
<Text style={[styles.td, styles.cSku]}>{item.sku || "—"}</Text>
<Text style={[styles.td, styles.cName]}>{item.name}</Text>
<Text style={[styles.td, styles.cSupplier]}>
@ -117,13 +131,14 @@ export function StockItemStocktakeDocument({
{item.unit ? ` ${item.unit}` : ""}
</Text>
<Text style={[styles.tdRight, styles.cMin]}>{minQty}</Text>
<Text style={[styles.tdRight, styles.cTarget]}>
{targetQty !== null && targetQty > 0 ? targetQty : "—"}
<Text style={[styles.tdRight, styles.cMax]}>
{maxQty !== null && maxQty > 0 ? maxQty : "—"}
</Text>
<Text style={[styles.td, styles.cLocation]}>
{item.location || "—"}
<Text
style={[styles.tdStatus, styles.cStatus, { color: statusStyle.hex }]}
>
{statusStyle.label}
</Text>
<Text style={[styles.td, styles.cStatus]}>{statusLabel}</Text>
<Text style={[styles.td, styles.spacer]} />
</View>
);