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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-06 06:31:07 +02:00
1 parent f9899d5621
commit 569a43d247
22 files changed
+803 -412

No files matched your search

@@ -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;
}