From 569a43d24763430953ddb3624acadb482ca0375b Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Tue, 6 Oct 2026 06:31:07 +0200 Subject: [PATCH] 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) --- .../(app)/production-batches/[id]/page.tsx | 23 +++ .../app/(app)/production-batches/new/page.tsx | 160 ++++++++++++++- .../src/app/(app)/production-batches/page.tsx | 16 +- .../src/app/(app)/raw-materials/[id]/page.tsx | 136 +++++++++++++ .../src/app/(app)/raw-materials/new/page.tsx | 186 ++++++++++++++++++ .../src/app/(app)/raw-materials/page.tsx | 88 +++++++++ ApiCore-Frontend/src/app/(app)/sidebar.tsx | 29 ++- .../src/components/stock/stock-card.spec.tsx | 1 + .../src/components/stock/stock-card.tsx | 85 ++++++-- .../src/lib/production-batches/types.ts | 7 + ApiCore-Frontend/src/lib/raw-materials/api.ts | 33 ++++ .../src/lib/raw-materials/labels.spec.ts | 13 ++ .../src/lib/raw-materials/labels.ts | 14 ++ .../src/lib/raw-materials/types.ts | 36 ++++ ApiCore-Frontend/src/lib/stock/api.ts | 5 +- ApiCore-Frontend/src/lib/stock/labels.ts | 1 + ApiCore-Frontend/src/lib/stock/types.ts | 16 +- .../dto/create-raw-material-lot.dto.ts | 49 ----- .../dto/update-raw-material-lot.dto.ts | 54 ----- .../raw-materials/raw-materials.controller.ts | 105 ---------- .../src/raw-materials/raw-materials.module.ts | 11 -- .../raw-materials/raw-materials.service.ts | 147 -------------- 22 files changed, 803 insertions(+), 412 deletions(-) create mode 100644 ApiCore-Frontend/src/app/(app)/raw-materials/[id]/page.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/raw-materials/new/page.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/raw-materials/page.tsx create mode 100644 ApiCore-Frontend/src/lib/raw-materials/api.ts create mode 100644 ApiCore-Frontend/src/lib/raw-materials/labels.spec.ts create mode 100644 ApiCore-Frontend/src/lib/raw-materials/labels.ts create mode 100644 ApiCore-Frontend/src/lib/raw-materials/types.ts delete mode 100644 ApiCore-Frontend/src/raw-materials/dto/create-raw-material-lot.dto.ts delete mode 100644 ApiCore-Frontend/src/raw-materials/dto/update-raw-material-lot.dto.ts delete mode 100644 ApiCore-Frontend/src/raw-materials/raw-materials.controller.ts delete mode 100644 ApiCore-Frontend/src/raw-materials/raw-materials.module.ts delete mode 100644 ApiCore-Frontend/src/raw-materials/raw-materials.service.ts diff --git a/ApiCore-Frontend/src/app/(app)/production-batches/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/production-batches/[id]/page.tsx index 95b75d6..d9b6db5 100644 --- a/ApiCore-Frontend/src/app/(app)/production-batches/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/production-batches/[id]/page.tsx @@ -15,6 +15,10 @@ import { DetailHeader, DetailQueryFallback } from "@/components/detail-header"; import { StockCard } from "@/components/stock/stock-card"; import { useAuth } from "@/lib/auth/auth-context"; import { EditProductionBatchDialog } from "./edit-production-batch-dialog"; +import { + RAW_MATERIAL_KIND_LABELS, + formatGrams, +} from "@/lib/raw-materials/labels"; const BACK = { backHref: "/production-batches", @@ -102,6 +106,25 @@ export default function ProductionBatchDetailPage() { {batch.notes ? (

Notiz: {batch.notes}

) : null} + {batch.inputs && batch.inputs.length > 0 ? ( +
+

Hergestellt aus

+
    + {batch.inputs.map((input) => ( +
  • + + {RAW_MATERIAL_KIND_LABELS[input.rawMaterialLot.kind]}{" "} + {input.rawMaterialLot.lotNumber} + + : {formatGrams(input.quantity)} +
  • + ))} +
+
+ ) : null} ([]); + const { data: rawLots } = useQuery({ + queryKey: ["raw-material-lots"], + queryFn: () => fetchRawMaterialLots(), + }); + const availableLots = (rawLots ?? []).filter((lot) => lot.remainingCount > 0); + const updateInput = ( + index: number, + change: Partial<{ rawMaterialLotId: string; grams: string }>, + ) => + setInputs((current) => + current.map((row, i) => (i === index ? { ...row, ...change } : row)), + ); const [error, setError] = useState(null); const { data: products } = useQuery({ queryKey: ["products", { status: "ACTIVE" }], queryFn: () => fetchProducts({ status: "ACTIVE" }), }); - const manufactured = (products ?? []).filter((product) => product.kind === "MANUFACTURED"); + const manufactured = (products ?? []).filter( + (product) => product.kind === "MANUFACTURED", + ); const selected = manufactured.find((product) => product.id === productId); const mutation = useMutation({ @@ -50,8 +73,16 @@ export default function NewProductionBatchPage() { lotNumber: lotNumber.trim(), date: new Date(date).toISOString(), quantity: Number(quantity), - bestBeforeDate: bestBeforeDate ? new Date(bestBeforeDate).toISOString() : undefined, + bestBeforeDate: bestBeforeDate + ? new Date(bestBeforeDate).toISOString() + : undefined, notes: notes.trim() || undefined, + inputs: inputs + .filter((row) => row.rawMaterialLotId && Number(row.grams) > 0) + .map((row) => ({ + rawMaterialLotId: row.rawMaterialLotId, + quantity: Number(row.grams), + })), }), onSuccess: (batch) => { void queryClient.invalidateQueries({ queryKey: ["production-batches"] }); @@ -59,7 +90,8 @@ export default function NewProductionBatchPage() { router.push(`/production-batches/${batch.id}`); }, onError: (mutationError: Error) => { - const message = mutationError.message || "Charge konnte nicht angelegt werden"; + const message = + mutationError.message || "Charge konnte nicht angelegt werden"; setError(message); toast.error(message); }, @@ -84,12 +116,20 @@ export default function NewProductionBatchPage() { /> -
+
- setProductId(value ?? "")} + > - {selected?.name} + + {selected?.name} + {manufactured.map((product) => ( @@ -101,7 +141,8 @@ export default function NewProductionBatchPage() { {products && manufactured.length === 0 ? (

- Noch kein Artikel der Art „Hergestellte Ware“. Lege ihn unter Produkte an. + Noch kein Artikel der Art „Hergestellte Ware“. + Lege ihn unter Produkte an.

) : null}
@@ -138,7 +179,9 @@ export default function NewProductionBatchPage() { />
- + setBestBeforeDate(event.target.value)} />
+
+ + Verbrauchte Rohstoffe (optional) + +

+ Wird beim Anlegen vom Rohstoff abgebucht. So lässt sich später + nachverfolgen, aus welchem Wachs oder Propolis diese Charge + stammt. +

+ {inputs.map((row, index) => { + const lot = availableLots.find( + (candidate) => candidate.id === row.rawMaterialLotId, + ); + return ( +
+
+ + +
+
+ + + updateInput(index, { grams: event.target.value }) + } + /> +
+ +
+ ); + })} + + {rawLots && availableLots.length === 0 ? ( +

+ Kein Rohstoff mit Restbestand vorhanden. +

+ ) : null} +
-