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
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user