feat(stock): show pricing columns toggle on /stock

This commit is contained in:
Dave 2026-08-31 16:57:25 +02:00
parent a4bfef123b
commit f63fb7d8fc
2 changed files with 96 additions and 1 deletions

View file

@ -302,4 +302,39 @@ describe("StockListPage", () => {
}); });
expect(submit).not.toBeDisabled(); expect(submit).not.toBeDisabled();
}); });
describe("Stock list — show pricing columns toggle", () => {
beforeEach(() => {
_resetState();
_state.items = [
{
id: "abcdefghijklmno",
sku: "B-001",
name: "M10 Hex Bolt",
current_qty: 42,
min_qty: 10,
target_qty: 0,
is_active: true,
supplier: "",
},
];
});
it("hides pricing columns by default", async () => {
render(<Page />);
expect(screen.queryByText("Cost")).toBeNull();
expect(screen.queryByText("Markup")).toBeNull();
expect(screen.queryByText("Selling")).toBeNull();
});
it("shows pricing columns after toggling Show pricing on", async () => {
render(<Page />);
fireEvent.click(
screen.getByRole("switch", { name: /Show pricing columns/i }),
);
expect(await screen.findByText("Cost (R)")).toBeInTheDocument();
expect(screen.getByText("Markup %")).toBeInTheDocument();
expect(screen.getByText("Selling (R)")).toBeInTheDocument();
});
});
}); });

View file

@ -86,6 +86,7 @@ export default function StockListPage() {
/** Debounced search — what we actually query for. */ /** Debounced search — what we actually query for. */
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [lowStockOnly, setLowStockOnly] = useState(false); const [lowStockOnly, setLowStockOnly] = useState(false);
const [showPricingColumns, setShowPricingColumns] = useState(false);
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const fetchItems = useCallback(async () => { const fetchItems = useCallback(async () => {
@ -131,6 +132,22 @@ export default function StockListPage() {
fetchItems(); fetchItems();
}, [fetchItems]); }, [fetchItems]);
// Hydrate the "Show pricing columns" toggle from localStorage on mount.
// Defaults to false when the key is missing or the value is anything
// other than the literal "true".
useEffect(() => {
if (typeof window === "undefined") return;
const v = window.localStorage.getItem("wfdash.stock.showPricing");
if (v === "true") setShowPricingColumns(true);
}, []);
// Mirror toggle changes back to localStorage so the operator's choice
// persists across page navigations and reloads.
useEffect(() => {
if (typeof window === "undefined") return;
window.localStorage.setItem("wfdash.stock.showPricing", String(showPricingColumns));
}, [showPricingColumns]);
// Debounce the search input → 250ms after the last keystroke. Same // Debounce the search input → 250ms after the last keystroke. Same
// pattern as /orders and /quotes — the operator is hunting for a // pattern as /orders and /quotes — the operator is hunting for a
// match, not paging through. // match, not paging through.
@ -249,6 +266,16 @@ export default function StockListPage() {
<Label htmlFor="low-stock-toggle" className="cursor-pointer"> <Label htmlFor="low-stock-toggle" className="cursor-pointer">
Low stock only Low stock only
</Label> </Label>
<Switch
id="show-pricing-toggle"
checked={showPricingColumns}
onCheckedChange={(checked) => setShowPricingColumns(checked === true)}
aria-label="Show pricing columns"
data-testid="show-pricing-toggle"
/>
<Label htmlFor="show-pricing-toggle" className="cursor-pointer">
Show pricing columns
</Label>
</div> </div>
</div> </div>
@ -296,6 +323,13 @@ export default function StockListPage() {
<TableHead className="text-right w-24">Current</TableHead> <TableHead className="text-right w-24">Current</TableHead>
<TableHead className="text-right w-24">Min</TableHead> <TableHead className="text-right w-24">Min</TableHead>
<TableHead className="text-right w-24">Target</TableHead> <TableHead className="text-right w-24">Target</TableHead>
{showPricingColumns && (
<>
<TableHead className="text-right w-28">Cost (R)</TableHead>
<TableHead className="text-right w-24">Markup %</TableHead>
<TableHead className="text-right w-28">Selling (R)</TableHead>
</>
)}
<TableHead className="w-36">Status</TableHead> <TableHead className="w-36">Status</TableHead>
<TableHead className="w-10" aria-label="Open" /> <TableHead className="w-10" aria-label="Open" />
</TableRow> </TableRow>
@ -305,6 +339,7 @@ export default function StockListPage() {
<StockRow <StockRow
key={item.id} key={item.id}
item={item} item={item}
showPricingColumns={showPricingColumns}
onOpen={() => router.push(`/stock/${item.id}`)} onOpen={() => router.push(`/stock/${item.id}`)}
/> />
))} ))}
@ -341,9 +376,21 @@ export default function StockListPage() {
* so the supplier name link inside can stay a real anchor without * so the supplier name link inside can stay a real anchor without
* triggering the nested-button trap. * triggering the nested-button trap.
* ========================================================================== */ * ========================================================================== */
function StockRow({ item, onOpen }: { item: StockItem; onOpen: () => void }) { function StockRow({
item,
showPricingColumns,
onOpen,
}: {
item: StockItem;
showPricingColumns: boolean;
onOpen: () => void;
}) {
const status = getStockStatus(item.current_qty, item.min_qty, item.is_active); const status = getStockStatus(item.current_qty, item.min_qty, item.is_active);
const supplierName = item.expand?.supplier?.suppliername || "—"; const supplierName = item.expand?.supplier?.suppliername || "—";
const formatPrice = (n: number | undefined) =>
typeof n === "number" && n > 0 ? `R ${n.toFixed(2)}` : "—";
const formatPercent = (n: number | undefined) =>
typeof n === "number" && n > 0 ? `${n}%` : "—";
return ( return (
<TableRow <TableRow
@ -389,6 +436,19 @@ function StockRow({ item, onOpen }: { item: StockItem; onOpen: () => void }) {
<span className="italic">—</span> <span className="italic">—</span>
)} )}
</TableCell> </TableCell>
{showPricingColumns && (
<>
<TableCell className="text-right tabular-nums text-muted-foreground">
{formatPrice(item.cost_price)}
</TableCell>
<TableCell className="text-right tabular-nums text-muted-foreground">
{formatPercent(item.markup)}
</TableCell>
<TableCell className="text-right tabular-nums">
{formatPrice(item.selling_price)}
</TableCell>
</>
)}
<TableCell> <TableCell>
<StockStatusBadge status={status} /> <StockStatusBadge status={status} />
</TableCell> </TableCell>