feat(stock): show pricing columns toggle on /stock
This commit is contained in:
parent
a4bfef123b
commit
f63fb7d8fc
2 changed files with 96 additions and 1 deletions
|
|
@ -302,4 +302,39 @@ describe("StockListPage", () => {
|
|||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export default function StockListPage() {
|
|||
/** Debounced search — what we actually query for. */
|
||||
const [search, setSearch] = useState("");
|
||||
const [lowStockOnly, setLowStockOnly] = useState(false);
|
||||
const [showPricingColumns, setShowPricingColumns] = useState(false);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
|
||||
const fetchItems = useCallback(async () => {
|
||||
|
|
@ -131,6 +132,22 @@ export default function StockListPage() {
|
|||
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
|
||||
// pattern as /orders and /quotes — the operator is hunting for a
|
||||
// match, not paging through.
|
||||
|
|
@ -249,6 +266,16 @@ export default function StockListPage() {
|
|||
<Label htmlFor="low-stock-toggle" className="cursor-pointer">
|
||||
Low stock only
|
||||
</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>
|
||||
|
||||
|
|
@ -296,6 +323,13 @@ export default function StockListPage() {
|
|||
<TableHead className="text-right w-24">Current</TableHead>
|
||||
<TableHead className="text-right w-24">Min</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-10" aria-label="Open" />
|
||||
</TableRow>
|
||||
|
|
@ -305,6 +339,7 @@ export default function StockListPage() {
|
|||
<StockRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
showPricingColumns={showPricingColumns}
|
||||
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
|
||||
* 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 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 (
|
||||
<TableRow
|
||||
|
|
@ -389,6 +436,19 @@ function StockRow({ item, onOpen }: { item: StockItem; onOpen: () => void }) {
|
|||
<span className="italic">—</span>
|
||||
)}
|
||||
</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>
|
||||
<StockStatusBadge status={status} />
|
||||
</TableCell>
|
||||
|
|
|
|||
Loading…
Reference in a new issue