feat(stock): missing-pricing-only filter on /stock

This commit is contained in:
Dave 2026-08-31 17:08:26 +02:00
parent f63fb7d8fc
commit 104cf4cc6c
2 changed files with 54 additions and 1 deletions

View file

@ -337,4 +337,30 @@ describe("StockListPage", () => {
expect(screen.getByText("Selling (R)")).toBeInTheDocument();
});
});
describe("Stock list — missing-pricing-only filter", () => {
beforeEach(() => {
_resetState();
_state.items = [
{ id: "aaaaaaaaaaaaaaa", sku: "B-001", name: "Has both", selling_price: 3.25, cost_price: 2.5, current_qty: 1, min_qty: 0, is_active: true, supplier: "" },
{ id: "bbbbbbbbbbbbbbb", sku: "B-002", name: "Missing both", selling_price: 0, cost_price: 0, current_qty: 1, min_qty: 0, is_active: true, supplier: "" },
];
});
afterEach(() => cleanup());
it("only shows rows where selling_price=0 AND cost_price=0", async () => {
render(<Page />);
expect(await screen.findByText("Has both")).toBeInTheDocument();
// Simulate the PB server filtering by the missing-pricing clause:
// only "Missing both" comes back from getList when the toggle is on.
mockListResponse([
{ id: "bbbbbbbbbbbbbbb", sku: "B-002", name: "Missing both", selling_price: 0, cost_price: 0, current_qty: 1, min_qty: 0, is_active: true, supplier: "" },
]);
fireEvent.click(screen.getByRole("switch", { name: /Missing pricing only/i }));
await waitFor(() => expect(screen.queryByText("Has both")).toBeNull());
expect(await screen.findByText("Missing both")).toBeInTheDocument();
});
});
});

View file

@ -87,6 +87,7 @@ export default function StockListPage() {
const [search, setSearch] = useState("");
const [lowStockOnly, setLowStockOnly] = useState(false);
const [showPricingColumns, setShowPricingColumns] = useState(false);
const [missingPricingOnly, setMissingPricingOnly] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const fetchItems = useCallback(async () => {
@ -104,6 +105,9 @@ export default function StockListPage() {
if (lowStockOnly) {
filters.push("(current_qty <= min_qty && is_active = true)");
}
if (missingPricingOnly) {
filters.push("(selling_price = 0 && cost_price = 0 && is_active = true)");
}
const result = await pb.collection("stock_items").getList<StockItem>(
1,
@ -125,7 +129,7 @@ export default function StockListPage() {
} finally {
setLoading(false);
}
}, [search, lowStockOnly]);
}, [search, lowStockOnly, missingPricingOnly]);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- data fetch on dep change
@ -195,6 +199,11 @@ export default function StockListPage() {
[items],
);
const missingPricingCount = useMemo(
() => items.filter((it) => (it.selling_price ?? 0) === 0 && (it.cost_price ?? 0) === 0).length,
[items],
);
const handleSearchChange = (v: string) => setSearchInput(v);
const clearSearch = () => setSearchInput("");
@ -215,6 +224,15 @@ export default function StockListPage() {
) : (
"Inventory levels across all stock items."
)}
{missingPricingCount > 0 && (
<span className="ml-2 inline-flex items-center">
·{" "}
<span className="font-semibold text-rose-700 dark:text-rose-400">
{missingPricingCount}
</span>{" "}
need pricing
</span>
)}
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
@ -276,6 +294,15 @@ export default function StockListPage() {
<Label htmlFor="show-pricing-toggle" className="cursor-pointer">
Show pricing columns
</Label>
<Switch
id="missing-pricing-toggle"
checked={missingPricingOnly}
onCheckedChange={(checked) => setMissingPricingOnly(checked === true)}
aria-label="Missing pricing only"
/>
<Label htmlFor="missing-pricing-toggle" className="cursor-pointer">
Missing pricing only
</Label>
</div>
</div>