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
@@ -9,6 +9,8 @@ export interface BottlingBatch {
|
||||
bestBeforeDate: string;
|
||||
lotNumber: string;
|
||||
createdAt: string;
|
||||
/** jarCount plus all stock movements (shop sales, withdrawals, ...). */
|
||||
remainingCount: number;
|
||||
honeyBatch: { id: string; batchNumber: string };
|
||||
product: { id: string; name: string } | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { apiFetch } from "@/lib/auth/api-client";
|
||||
import {
|
||||
CreateProductionBatchInput,
|
||||
ProductionBatch,
|
||||
ProductionBatchFilter,
|
||||
} from "./types";
|
||||
|
||||
export function fetchProductionBatches(
|
||||
filter: ProductionBatchFilter = {},
|
||||
): Promise<ProductionBatch[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (filter.productId) params.set("productId", filter.productId);
|
||||
const query = params.toString();
|
||||
return apiFetch<ProductionBatch[]>(
|
||||
`/production-batches${query ? `?${query}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchProductionBatch(id: string): Promise<ProductionBatch> {
|
||||
return apiFetch<ProductionBatch>(`/production-batches/${id}`);
|
||||
}
|
||||
|
||||
export function createProductionBatch(
|
||||
input: CreateProductionBatchInput,
|
||||
): Promise<ProductionBatch> {
|
||||
return apiFetch<ProductionBatch>("/production-batches", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteProductionBatch(id: string): Promise<{ id: string }> {
|
||||
return apiFetch<{ id: string }>(`/production-batches/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export interface ProductionBatch {
|
||||
id: string;
|
||||
productId: string;
|
||||
lotNumber: string;
|
||||
date: string;
|
||||
quantity: number;
|
||||
bestBeforeDate: string | null;
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
remainingCount: number;
|
||||
product: { id: string; name: string };
|
||||
}
|
||||
|
||||
export interface CreateProductionBatchInput {
|
||||
productId: string;
|
||||
lotNumber: string;
|
||||
date: string;
|
||||
quantity: number;
|
||||
bestBeforeDate?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ProductionBatchFilter {
|
||||
productId?: string;
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
import { ProductStatus } from "./types";
|
||||
import { ProductKind, ProductStatus, ShopCategory } from "./types";
|
||||
|
||||
export const PRODUCT_STATUS_LABELS: Record<ProductStatus, string> = {
|
||||
ACTIVE: "Aktiv",
|
||||
DISCONTINUED: "Eingestellt",
|
||||
};
|
||||
|
||||
export const PRODUCT_KIND_LABELS: Record<ProductKind, string> = {
|
||||
BOTTLED_HONEY: "Honig (Bestand je Abfüllung)",
|
||||
MANUFACTURED: "Hergestellte Ware (Bestand je Herstellungscharge)",
|
||||
};
|
||||
|
||||
export const SHOP_CATEGORY_LABELS: Record<ShopCategory, string> = {
|
||||
HONIG: "Honig",
|
||||
KERZEN: "Kerzen",
|
||||
PROPOLIS: "Propolis",
|
||||
SONSTIGES: "Sonstiges",
|
||||
};
|
||||
@@ -1,5 +1,10 @@
|
||||
export type ProductStatus = "ACTIVE" | "DISCONTINUED";
|
||||
|
||||
/** Honey: stock per bottling. Everything else: stock per production batch. */
|
||||
export type ProductKind = "BOTTLED_HONEY" | "MANUFACTURED";
|
||||
|
||||
export type ShopCategory = "HONIG" | "KERZEN" | "PROPOLIS" | "SONSTIGES";
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -9,6 +14,8 @@ export interface Product {
|
||||
weight: number | null;
|
||||
salesUnit: string | null;
|
||||
status: ProductStatus;
|
||||
kind: ProductKind;
|
||||
shopCategory: ShopCategory;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -20,6 +27,8 @@ export interface CreateProductInput {
|
||||
packagingSize?: number;
|
||||
weight?: number;
|
||||
salesUnit?: string;
|
||||
kind?: ProductKind;
|
||||
shopCategory?: ShopCategory;
|
||||
}
|
||||
|
||||
export interface UpdateProductInput extends Partial<CreateProductInput> {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { apiFetch } from "@/lib/auth/api-client";
|
||||
import {
|
||||
BatchStock,
|
||||
CorrectInput,
|
||||
StockBatchPath,
|
||||
WithdrawInput,
|
||||
} from "./types";
|
||||
|
||||
export function fetchStock(path: StockBatchPath, id: string): Promise<BatchStock> {
|
||||
return apiFetch<BatchStock>(`/${path}/${id}/stock`);
|
||||
}
|
||||
|
||||
export function withdrawStock(
|
||||
path: StockBatchPath,
|
||||
id: string,
|
||||
input: WithdrawInput,
|
||||
): Promise<BatchStock> {
|
||||
return apiFetch<BatchStock>(`/${path}/${id}/withdrawals`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function correctStock(
|
||||
path: StockBatchPath,
|
||||
id: string,
|
||||
input: CorrectInput,
|
||||
): Promise<BatchStock> {
|
||||
return apiFetch<BatchStock>(`/${path}/${id}/corrections`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { MovementType, WithdrawalReason } from "./types";
|
||||
|
||||
export const WITHDRAWAL_REASON_LABELS: Record<WithdrawalReason, string> = {
|
||||
HOFLADEN: "Hofladen",
|
||||
EIGENBEDARF: "Eigenbedarf / Geschenk",
|
||||
BRUCH: "Bruch / Verlust",
|
||||
PROBE: "Probe / Muster",
|
||||
SONSTIGES: "Sonstiges",
|
||||
};
|
||||
|
||||
export const MOVEMENT_TYPE_LABELS: Record<MovementType, string> = {
|
||||
SHOP_SALE: "Webshop-Verkauf",
|
||||
SHOP_RETURN: "Webshop-Rückbuchung",
|
||||
WITHDRAWAL: "Entnahme",
|
||||
CORRECTION: "Inventur",
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/** Collection path of a batch whose stock is tracked in the movement ledger. */
|
||||
export type StockBatchPath = "bottling-batches" | "production-batches";
|
||||
|
||||
export type WithdrawalReason =
|
||||
| "HOFLADEN"
|
||||
| "EIGENBEDARF"
|
||||
| "BRUCH"
|
||||
| "PROBE"
|
||||
| "SONSTIGES";
|
||||
|
||||
export type MovementType =
|
||||
| "SHOP_SALE"
|
||||
| "SHOP_RETURN"
|
||||
| "WITHDRAWAL"
|
||||
| "CORRECTION";
|
||||
|
||||
export interface StockSummary {
|
||||
/** Bottled or produced. */
|
||||
initial: number;
|
||||
/** Net shop sales (sales minus shop returns). */
|
||||
soldInShop: number;
|
||||
withdrawn: number;
|
||||
/** Net inventory corrections. */
|
||||
corrected: number;
|
||||
remaining: number;
|
||||
}
|
||||
|
||||
export interface StockMovement {
|
||||
id: string;
|
||||
type: MovementType;
|
||||
/** Signed: negative takes stock out. */
|
||||
quantity: number;
|
||||
reason: WithdrawalReason | null;
|
||||
notes: string | null;
|
||||
apiKeyId: string | null;
|
||||
createdAt: string;
|
||||
user: { id: string; name: string } | null;
|
||||
shopRequest: { externalId: string; kind: "SALE" | "RETURN" } | null;
|
||||
}
|
||||
|
||||
export interface BatchStock {
|
||||
summary: StockSummary;
|
||||
movements: StockMovement[];
|
||||
}
|
||||
|
||||
export interface WithdrawInput {
|
||||
quantity: number;
|
||||
reason: WithdrawalReason;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface CorrectInput {
|
||||
countedQuantity: number;
|
||||
notes: string;
|
||||
}
|
||||
Reference in new issue
Block a user