feat(ui): raw material lots and production inputs
- Pages for raw material lots (beeswax, propolis): list, create in grams
with optional apiary, detail with stock card (harvested / used in
production / withdrawn / remaining) and "used in" batches.
- Production batch form takes the raw material used (lot + grams) and the
batch page shows what it was made from - traceability both ways.
- Sidebar entry "Rohstoffe".
- Removes backend files that were committed into ApiCore-Frontend/src by
mistake in e52a605 (copies of ApiCore-Backend/src/raw-materials).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
f9899d5621
commit
569a43d247
22 files changed
+803
-412
No files matched your search
@@ -9,6 +9,12 @@ export interface ProductionBatch {
|
||||
createdAt: string;
|
||||
remainingCount: number;
|
||||
product: { id: string; name: string };
|
||||
/** Raw material this batch was made from (grams). */
|
||||
inputs?: {
|
||||
id: string;
|
||||
quantity: number;
|
||||
rawMaterialLot: { id: string; lotNumber: string; kind: "BEESWAX" | "PROPOLIS" };
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface CreateProductionBatchInput {
|
||||
@@ -18,6 +24,7 @@ export interface CreateProductionBatchInput {
|
||||
quantity: number;
|
||||
bestBeforeDate?: string;
|
||||
notes?: string;
|
||||
inputs?: { rawMaterialLotId: string; quantity: number }[];
|
||||
}
|
||||
|
||||
export interface UpdateProductionBatchInput {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { apiFetch } from "@/lib/auth/api-client";
|
||||
import {
|
||||
CreateRawMaterialLotInput,
|
||||
RawMaterialKind,
|
||||
RawMaterialLot,
|
||||
} from "./types";
|
||||
|
||||
export function fetchRawMaterialLots(
|
||||
kind?: RawMaterialKind,
|
||||
): Promise<RawMaterialLot[]> {
|
||||
return apiFetch<RawMaterialLot[]>(
|
||||
`/raw-material-lots${kind ? `?kind=${kind}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchRawMaterialLot(id: string): Promise<RawMaterialLot> {
|
||||
return apiFetch<RawMaterialLot>(`/raw-material-lots/${id}`);
|
||||
}
|
||||
|
||||
export function createRawMaterialLot(
|
||||
input: CreateRawMaterialLotInput,
|
||||
): Promise<RawMaterialLot> {
|
||||
return apiFetch<RawMaterialLot>("/raw-material-lots", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteRawMaterialLot(id: string): Promise<{ id: string }> {
|
||||
return apiFetch<{ id: string }>(`/raw-material-lots/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatGrams } from "./labels";
|
||||
|
||||
describe("formatGrams", () => {
|
||||
it("shows kilograms from 1000 g on", () => {
|
||||
expect(formatGrams(4200)).toBe("4,2 kg");
|
||||
expect(formatGrams(1000)).toBe("1 kg");
|
||||
});
|
||||
|
||||
it("keeps grams below a kilogram", () => {
|
||||
expect(formatGrams(180)).toBe("180 g");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { RawMaterialKind } from "./types";
|
||||
|
||||
export const RAW_MATERIAL_KIND_LABELS: Record<RawMaterialKind, string> = {
|
||||
BEESWAX: "Bienenwachs",
|
||||
PROPOLIS: "Propolis",
|
||||
};
|
||||
|
||||
/** 4200 -> "4,2 kg", 180 -> "180 g". */
|
||||
export function formatGrams(grams: number): string {
|
||||
if (Math.abs(grams) >= 1000) {
|
||||
return `${(grams / 1000).toLocaleString("de-DE", { maximumFractionDigits: 2 })} kg`;
|
||||
}
|
||||
return `${grams.toLocaleString("de-DE")} g`;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export type RawMaterialKind = "BEESWAX" | "PROPOLIS";
|
||||
|
||||
export interface RawMaterialLot {
|
||||
id: string;
|
||||
kind: RawMaterialKind;
|
||||
lotNumber: string;
|
||||
date: string;
|
||||
/** Gewonnene Menge in Gramm. */
|
||||
quantity: number;
|
||||
apiaryId: string | null;
|
||||
harvestId: string | null;
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
/** Rest in Gramm (Ausgangsmenge plus alle Bewegungen). */
|
||||
remainingCount: number;
|
||||
apiary: { id: string; name: string } | null;
|
||||
harvest: { id: string; date: string } | null;
|
||||
usedIn: {
|
||||
id: string;
|
||||
quantity: number;
|
||||
productionBatch: {
|
||||
id: string;
|
||||
lotNumber: string;
|
||||
product: { id: string; name: string };
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface CreateRawMaterialLotInput {
|
||||
kind: RawMaterialKind;
|
||||
lotNumber: string;
|
||||
date: string;
|
||||
quantity: number;
|
||||
apiaryId?: string;
|
||||
notes?: string;
|
||||
}
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
WithdrawInput,
|
||||
} from "./types";
|
||||
|
||||
export function fetchStock(path: StockBatchPath, id: string): Promise<BatchStock> {
|
||||
export function fetchStock(
|
||||
path: StockBatchPath,
|
||||
id: string,
|
||||
): Promise<BatchStock> {
|
||||
return apiFetch<BatchStock>(`/${path}/${id}/stock`);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,4 +13,5 @@ export const MOVEMENT_TYPE_LABELS: Record<MovementType, string> = {
|
||||
SHOP_RETURN: "Webshop-Rückbuchung",
|
||||
WITHDRAWAL: "Entnahme",
|
||||
CORRECTION: "Inventur",
|
||||
CONSUMPTION: "Verbrauch in Herstellung",
|
||||
};
|
||||
@@ -1,18 +1,12 @@
|
||||
/** Collection path of a batch whose stock is tracked in the movement ledger. */
|
||||
export type StockBatchPath = "bottling-batches" | "production-batches";
|
||||
export type StockBatchPath =
|
||||
"bottling-batches" | "production-batches" | "raw-material-lots";
|
||||
|
||||
export type WithdrawalReason =
|
||||
| "HOFLADEN"
|
||||
| "EIGENBEDARF"
|
||||
| "BRUCH"
|
||||
| "PROBE"
|
||||
| "SONSTIGES";
|
||||
"HOFLADEN" | "EIGENBEDARF" | "BRUCH" | "PROBE" | "SONSTIGES";
|
||||
|
||||
export type MovementType =
|
||||
| "SHOP_SALE"
|
||||
| "SHOP_RETURN"
|
||||
| "WITHDRAWAL"
|
||||
| "CORRECTION";
|
||||
"SHOP_SALE" | "SHOP_RETURN" | "WITHDRAWAL" | "CORRECTION" | "CONSUMPTION";
|
||||
|
||||
export interface StockSummary {
|
||||
/** Bottled or produced. */
|
||||
@@ -22,6 +16,8 @@ export interface StockSummary {
|
||||
withdrawn: number;
|
||||
/** Net inventory corrections. */
|
||||
corrected: number;
|
||||
/** Raw material used in production (net of give-backs). */
|
||||
consumed: number;
|
||||
remaining: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user