feat(stock): missing-pricing-only filter on /stock
This commit is contained in:
parent
f63fb7d8fc
commit
104cf4cc6c
2 changed files with 54 additions and 1 deletions
|
|
@ -337,4 +337,30 @@ describe("StockListPage", () => {
|
||||||
expect(screen.getByText("Selling (R)")).toBeInTheDocument();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ export default function StockListPage() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [lowStockOnly, setLowStockOnly] = useState(false);
|
const [lowStockOnly, setLowStockOnly] = useState(false);
|
||||||
const [showPricingColumns, setShowPricingColumns] = useState(false);
|
const [showPricingColumns, setShowPricingColumns] = useState(false);
|
||||||
|
const [missingPricingOnly, setMissingPricingOnly] = useState(false);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
|
||||||
const fetchItems = useCallback(async () => {
|
const fetchItems = useCallback(async () => {
|
||||||
|
|
@ -104,6 +105,9 @@ export default function StockListPage() {
|
||||||
if (lowStockOnly) {
|
if (lowStockOnly) {
|
||||||
filters.push("(current_qty <= min_qty && is_active = true)");
|
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>(
|
const result = await pb.collection("stock_items").getList<StockItem>(
|
||||||
1,
|
1,
|
||||||
|
|
@ -125,7 +129,7 @@ export default function StockListPage() {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [search, lowStockOnly]);
|
}, [search, lowStockOnly, missingPricingOnly]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- data fetch on dep change
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- data fetch on dep change
|
||||||
|
|
@ -195,6 +199,11 @@ export default function StockListPage() {
|
||||||
[items],
|
[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 handleSearchChange = (v: string) => setSearchInput(v);
|
||||||
const clearSearch = () => setSearchInput("");
|
const clearSearch = () => setSearchInput("");
|
||||||
|
|
||||||
|
|
@ -215,6 +224,15 @@ export default function StockListPage() {
|
||||||
) : (
|
) : (
|
||||||
"Inventory levels across all stock items."
|
"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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<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">
|
<Label htmlFor="show-pricing-toggle" className="cursor-pointer">
|
||||||
Show pricing columns
|
Show pricing columns
|
||||||
</Label>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue