feat(ui): stock card with withdraw and inventory count, production batches
- Bottling detail shows Abgefüllt / Webshop verkauft / Entnommen / Rest (negative rest flagged "Prüfen") and the full movement list; buttons "Entnehmen" (reason, note) and "Inventur korrigieren" (counted quantity, difference booked). - Rest column in the bottling list. - New pages for production batches (list, create, detail with the same stock card), sidebar entry with its own permission. - Products get "Art" (honey vs. manufactured goods) and the webshop category; bottlings only offer honey articles. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
04f93e954a
commit
f57db97578
21 files changed
+1151
-5
No files matched your search
@@ -0,0 +1,109 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { StockCard } from "./stock-card";
|
||||
import { fetchStock } from "@/lib/stock/api";
|
||||
import type { BatchStock } from "@/lib/stock/types";
|
||||
|
||||
vi.mock("@/lib/stock/api", () => ({
|
||||
fetchStock: vi.fn(),
|
||||
withdrawStock: vi.fn(),
|
||||
correctStock: vi.fn(),
|
||||
}));
|
||||
|
||||
function renderCard(canBook = true) {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<StockCard
|
||||
path="bottling-batches"
|
||||
id="b1"
|
||||
initialLabel="Abgefüllt"
|
||||
unit="Gläser"
|
||||
canBook={canBook}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
function stock(remaining: number): BatchStock {
|
||||
return {
|
||||
summary: {
|
||||
initial: 120,
|
||||
soldInShop: 30,
|
||||
withdrawn: 10,
|
||||
corrected: 0,
|
||||
remaining,
|
||||
},
|
||||
movements: [
|
||||
{
|
||||
id: "m1",
|
||||
type: "SHOP_SALE",
|
||||
quantity: -30,
|
||||
reason: null,
|
||||
notes: null,
|
||||
apiKeyId: "k1",
|
||||
createdAt: "2026-10-01T10:00:00.000Z",
|
||||
user: null,
|
||||
shopRequest: { externalId: "x", kind: "SALE" },
|
||||
},
|
||||
{
|
||||
id: "m2",
|
||||
type: "WITHDRAWAL",
|
||||
quantity: -10,
|
||||
reason: "HOFLADEN",
|
||||
notes: null,
|
||||
apiKeyId: null,
|
||||
createdAt: "2026-10-01T11:00:00.000Z",
|
||||
user: { id: "u1", name: "Nick" },
|
||||
shopRequest: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("StockCard", () => {
|
||||
beforeEach(() => vi.mocked(fetchStock).mockReset());
|
||||
|
||||
it("separates bottled, sold, withdrawn and remaining", async () => {
|
||||
vi.mocked(fetchStock).mockResolvedValue(stock(80));
|
||||
renderCard();
|
||||
|
||||
expect(await screen.findByText("Abgefüllt")).toBeInTheDocument();
|
||||
expect(screen.getByText("120")).toBeInTheDocument();
|
||||
expect(screen.getByText("Webshop verkauft")).toBeInTheDocument();
|
||||
expect(screen.getByText("80")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Prüfen")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("lists shop sales as coming from the shop and withdrawals with their reason", async () => {
|
||||
vi.mocked(fetchStock).mockResolvedValue(stock(80));
|
||||
renderCard();
|
||||
|
||||
expect(await screen.findByText("Webshop-Verkauf")).toBeInTheDocument();
|
||||
expect(screen.getByText(/· Webshop$/)).toBeInTheDocument();
|
||||
expect(screen.getByText("Entnahme · Hofladen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("flags a negative remaining stock for review", async () => {
|
||||
vi.mocked(fetchStock).mockResolvedValue(stock(-2));
|
||||
renderCard();
|
||||
|
||||
expect(await screen.findByText("Prüfen")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the booking buttons only with permission", async () => {
|
||||
vi.mocked(fetchStock).mockResolvedValue(stock(80));
|
||||
renderCard(false);
|
||||
|
||||
await screen.findByText("Abgefüllt");
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /Entnehmen/ }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /Inventur/ }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user