From 1054a845ef24ba8726d2c93c9ba12127fd689ff4 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Fri, 4 Sep 2026 19:44:33 +0200 Subject: [PATCH] feat: allow a honey batch to be split across multiple storage containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A batch that doesn't fit in one container (e.g. 125 kg across 5 x 25 kg buckets) had no way to be represented - HoneyBatch.storageContainerId is a single nullable FK. Adds HoneyBatchContainerAllocation as an additive join table (container + amount per batch), independent of the existing single-container/relocate() flow which still covers the simple one-container case unchanged. - GET/PUT/DELETE /honey-batches/:id/containers[/:containerId], reusing the existing honey-batches:relocate permission. - Assigning a container is an upsert (@@unique on batch+container), and the total assigned is validated against the batch's own amount when known. - New "Aufteilung auf mehrere Behälter" card on the honey batch detail page, alongside (not replacing) the existing storage card. Co-Authored-By: Claude Sonnet 5 --- .../migration.sql | 23 +++ ApiCore-Backend/prisma/schema.prisma | 35 +++- ...et-honey-batch-container-allocation.dto.ts | 9 + .../honey-batches/honey-batches.controller.ts | 39 ++++ .../honey-batches.service.spec.ts | 157 ++++++++++++++- .../honey-batches/honey-batches.service.ts | 96 +++++++++ ...honey-batch-container-allocations-card.tsx | 190 ++++++++++++++++++ .../src/app/(app)/honey-batches/[id]/page.tsx | 2 + ApiCore-Frontend/src/lib/honey-batches/api.ts | 27 +++ .../src/lib/honey-batches/types.ts | 8 + 10 files changed, 579 insertions(+), 7 deletions(-) create mode 100644 ApiCore-Backend/prisma/migrations/20260904150000_add_honey_batch_container_allocations/migration.sql create mode 100644 ApiCore-Backend/src/honey-batches/dto/set-honey-batch-container-allocation.dto.ts create mode 100644 ApiCore-Frontend/src/app/(app)/honey-batches/[id]/honey-batch-container-allocations-card.tsx diff --git a/ApiCore-Backend/prisma/migrations/20260904150000_add_honey_batch_container_allocations/migration.sql b/ApiCore-Backend/prisma/migrations/20260904150000_add_honey_batch_container_allocations/migration.sql new file mode 100644 index 0000000..8522e72 --- /dev/null +++ b/ApiCore-Backend/prisma/migrations/20260904150000_add_honey_batch_container_allocations/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "HoneyBatchContainerAllocation" ( + "id" TEXT NOT NULL, + "honeyBatchId" TEXT NOT NULL, + "storageContainerId" TEXT NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "HoneyBatchContainerAllocation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "HoneyBatchContainerAllocation_storageContainerId_idx" ON "HoneyBatchContainerAllocation"("storageContainerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "HoneyBatchContainerAllocation_honeyBatchId_storageContaine_key" ON "HoneyBatchContainerAllocation"("honeyBatchId", "storageContainerId"); + +-- AddForeignKey +ALTER TABLE "HoneyBatchContainerAllocation" ADD CONSTRAINT "HoneyBatchContainerAllocation_honeyBatchId_fkey" FOREIGN KEY ("honeyBatchId") REFERENCES "HoneyBatch"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "HoneyBatchContainerAllocation" ADD CONSTRAINT "HoneyBatchContainerAllocation_storageContainerId_fkey" FOREIGN KEY ("storageContainerId") REFERENCES "StorageContainer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/ApiCore-Backend/prisma/schema.prisma b/ApiCore-Backend/prisma/schema.prisma index 477c341..5ad443f 100644 --- a/ApiCore-Backend/prisma/schema.prisma +++ b/ApiCore-Backend/prisma/schema.prisma @@ -708,9 +708,10 @@ model HoneyBatch { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - measurements HoneyMeasurement[] - bottlingBatches BottlingBatch[] - relocations HoneyBatchRelocation[] + measurements HoneyMeasurement[] + bottlingBatches BottlingBatch[] + relocations HoneyBatchRelocation[] + containerAllocations HoneyBatchContainerAllocation[] @@index([status]) @@index([extractionId]) @@ -756,9 +757,10 @@ model StorageContainer { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - honeyBatches HoneyBatch[] - relocationsFrom HoneyBatchRelocation[] @relation("RelocationFrom") - relocationsTo HoneyBatchRelocation[] @relation("RelocationTo") + honeyBatches HoneyBatch[] + relocationsFrom HoneyBatchRelocation[] @relation("RelocationFrom") + relocationsTo HoneyBatchRelocation[] @relation("RelocationTo") + batchAllocations HoneyBatchContainerAllocation[] @@index([status]) } @@ -781,6 +783,27 @@ model HoneyBatchRelocation { @@index([honeyBatchId, relocatedAt]) } +// Aufteilung einer Honigcharge auf mehrere Lagerbehälter (z.B. 125 kg auf 5 +// Eimer à 25 kg) - additiv zum obigen "primären" storageContainerId/ +// Umlagern-Fluss auf HoneyBatch, der weiterhin den einfachen Ein-Behälter- +// Fall abdeckt; diese Tabelle bildet zusätzlich den Fall ab, dass eine +// Charge zu groß für einen einzelnen Behälter ist. @@unique macht das +// Zuweisen einer Menge zu einem Behälter ein Upsert (erneutes Zuweisen +// überschreibt die Menge, statt einen Duplikat-Eintrag anzulegen). +model HoneyBatchContainerAllocation { + id String @id @default(cuid()) + honeyBatchId String + honeyBatch HoneyBatch @relation(fields: [honeyBatchId], references: [id]) + storageContainerId String + storageContainer StorageContainer @relation(fields: [storageContainerId], references: [id]) + amount Float + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([honeyBatchId, storageContainerId]) + @@index([storageContainerId]) +} + enum StockCategory { HONEY CONSUMABLE diff --git a/ApiCore-Backend/src/honey-batches/dto/set-honey-batch-container-allocation.dto.ts b/ApiCore-Backend/src/honey-batches/dto/set-honey-batch-container-allocation.dto.ts new file mode 100644 index 0000000..a45f38c --- /dev/null +++ b/ApiCore-Backend/src/honey-batches/dto/set-honey-batch-container-allocation.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsPositive } from 'class-validator'; + +export class SetHoneyBatchContainerAllocationDto { + @ApiProperty({ description: 'Menge in kg in diesem Behälter' }) + @IsNumber() + @IsPositive() + amount!: number; +} diff --git a/ApiCore-Backend/src/honey-batches/honey-batches.controller.ts b/ApiCore-Backend/src/honey-batches/honey-batches.controller.ts index 845d8c6..c886ad8 100644 --- a/ApiCore-Backend/src/honey-batches/honey-batches.controller.ts +++ b/ApiCore-Backend/src/honey-batches/honey-batches.controller.ts @@ -1,10 +1,12 @@ import { Body, Controller, + Delete, Get, Param, Patch, Post, + Put, Query, } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; @@ -16,6 +18,7 @@ import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto'; import { UpdateHoneyBatchDto } from './dto/update-honey-batch.dto'; import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto'; import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto'; +import { SetHoneyBatchContainerAllocationDto } from './dto/set-honey-batch-container-allocation.dto'; import { HoneyBatchesService } from './honey-batches.service'; @ApiTags('honey-batches') @@ -48,6 +51,42 @@ export class HoneyBatchesController { return this.honeyBatchesService.findRelocations(id); } + @RequirePermission('honey-batches:read') + @Get(':id/containers') + listContainerAllocations(@Param('id') id: string) { + return this.honeyBatchesService.listContainerAllocations(id); + } + + @RequirePermission('honey-batches:relocate') + @Put(':id/containers/:containerId') + setContainerAllocation( + @Param('id') id: string, + @Param('containerId') containerId: string, + @Body() dto: SetHoneyBatchContainerAllocationDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.honeyBatchesService.setContainerAllocation( + id, + containerId, + dto, + actor.id, + ); + } + + @RequirePermission('honey-batches:relocate') + @Delete(':id/containers/:containerId') + removeContainerAllocation( + @Param('id') id: string, + @Param('containerId') containerId: string, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.honeyBatchesService.removeContainerAllocation( + id, + containerId, + actor.id, + ); + } + @RequirePermission('honey-batches:relocate') @Post(':id/relocate') relocate( diff --git a/ApiCore-Backend/src/honey-batches/honey-batches.service.spec.ts b/ApiCore-Backend/src/honey-batches/honey-batches.service.spec.ts index 9bc9efe..12d587e 100644 --- a/ApiCore-Backend/src/honey-batches/honey-batches.service.spec.ts +++ b/ApiCore-Backend/src/honey-batches/honey-batches.service.spec.ts @@ -1,4 +1,4 @@ -import { NotFoundException } from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { HoneyBatchesService } from './honey-batches.service'; import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; @@ -17,6 +17,12 @@ describe('HoneyBatchesService', () => { honeyMeasurement: { create: jest.Mock }; storageContainer: { findUnique: jest.Mock }; honeyBatchRelocation: { create: jest.Mock; findMany: jest.Mock }; + honeyBatchContainerAllocation: { + findMany: jest.Mock; + findUnique: jest.Mock; + upsert: jest.Mock; + delete: jest.Mock; + }; $transaction: jest.Mock; }; let auditLog: { record: jest.Mock }; @@ -57,6 +63,12 @@ describe('HoneyBatchesService', () => { honeyMeasurement: { create: jest.fn() }, storageContainer: { findUnique: jest.fn() }, honeyBatchRelocation: { create: jest.fn(), findMany: jest.fn() }, + honeyBatchContainerAllocation: { + findMany: jest.fn().mockResolvedValue([]), + findUnique: jest.fn(), + upsert: jest.fn(), + delete: jest.fn(), + }, $transaction: jest.fn(), }; auditLog = { record: jest.fn() }; @@ -199,4 +211,147 @@ describe('HoneyBatchesService', () => { expect(findArg.where.honeyBatchId).toBe('batch-1'); }); }); + + describe('setContainerAllocation', () => { + it('throws NotFoundException when the container does not exist', async () => { + prisma.honeyBatch.findUnique.mockResolvedValue(honeyBatch); + prisma.storageContainer.findUnique.mockResolvedValue(null); + + await expect( + service.setContainerAllocation( + 'batch-1', + 'missing', + { amount: 25 }, + 'user-1', + ), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('allows splitting the batch amount across multiple containers (125 kg / 5 x 25 kg)', async () => { + prisma.honeyBatch.findUnique.mockResolvedValue({ + ...honeyBatch, + amount: 125, + }); + prisma.storageContainer.findUnique.mockResolvedValue({ + id: 'bucket-5', + }); + prisma.honeyBatchContainerAllocation.findMany.mockResolvedValue([ + { storageContainerId: 'bucket-1', amount: 25 }, + { storageContainerId: 'bucket-2', amount: 25 }, + { storageContainerId: 'bucket-3', amount: 25 }, + { storageContainerId: 'bucket-4', amount: 25 }, + ]); + prisma.honeyBatchContainerAllocation.upsert.mockResolvedValue({ + id: 'allocation-5', + honeyBatchId: 'batch-1', + storageContainerId: 'bucket-5', + amount: 25, + }); + + await service.setContainerAllocation( + 'batch-1', + 'bucket-5', + { amount: 25 }, + 'user-1', + ); + + expect(prisma.honeyBatchContainerAllocation.upsert).toHaveBeenCalledWith({ + where: { + honeyBatchId_storageContainerId: { + honeyBatchId: 'batch-1', + storageContainerId: 'bucket-5', + }, + }, + create: { + honeyBatchId: 'batch-1', + storageContainerId: 'bucket-5', + amount: 25, + }, + update: { amount: 25 }, + include: { storageContainer: true }, + }); + }); + + it('rejects an allocation that would exceed the batch amount', async () => { + prisma.honeyBatch.findUnique.mockResolvedValue({ + ...honeyBatch, + amount: 100, + }); + prisma.storageContainer.findUnique.mockResolvedValue({ id: 'bucket-1' }); + prisma.honeyBatchContainerAllocation.findMany.mockResolvedValue([ + { storageContainerId: 'bucket-2', amount: 80 }, + ]); + + await expect( + service.setContainerAllocation( + 'batch-1', + 'bucket-1', + { amount: 30 }, + 'user-1', + ), + ).rejects.toBeInstanceOf(BadRequestException); + expect( + prisma.honeyBatchContainerAllocation.upsert, + ).not.toHaveBeenCalled(); + }); + + it('re-assigning the same container overwrites its own amount rather than double-counting it', async () => { + prisma.honeyBatch.findUnique.mockResolvedValue({ + ...honeyBatch, + amount: 100, + }); + prisma.storageContainer.findUnique.mockResolvedValue({ id: 'bucket-1' }); + // findMany excludes the container being set (storageContainerId: { not: ... }), + // so its own prior amount never counts against itself here. + prisma.honeyBatchContainerAllocation.findMany.mockResolvedValue([]); + prisma.honeyBatchContainerAllocation.upsert.mockResolvedValue({ + id: 'allocation-1', + honeyBatchId: 'batch-1', + storageContainerId: 'bucket-1', + amount: 90, + }); + + await expect( + service.setContainerAllocation( + 'batch-1', + 'bucket-1', + { amount: 90 }, + 'user-1', + ), + ).resolves.toBeDefined(); + }); + }); + + describe('removeContainerAllocation', () => { + it('throws NotFoundException when no allocation exists for that container', async () => { + prisma.honeyBatch.findUnique.mockResolvedValue(honeyBatch); + prisma.honeyBatchContainerAllocation.findUnique.mockResolvedValue(null); + + await expect( + service.removeContainerAllocation('batch-1', 'bucket-1', 'user-1'), + ).rejects.toBeInstanceOf(NotFoundException); + }); + + it('deletes the allocation and records an audit entry', async () => { + prisma.honeyBatch.findUnique.mockResolvedValue(honeyBatch); + prisma.honeyBatchContainerAllocation.findUnique.mockResolvedValue({ + id: 'allocation-1', + honeyBatchId: 'batch-1', + storageContainerId: 'bucket-1', + amount: 25, + }); + + await service.removeContainerAllocation('batch-1', 'bucket-1', 'user-1'); + + expect(prisma.honeyBatchContainerAllocation.delete).toHaveBeenCalledWith({ + where: { id: 'allocation-1' }, + }); + expect(auditLog.record).toHaveBeenCalledWith( + expect.objectContaining({ + entityType: 'HoneyBatchContainerAllocation', + action: 'DELETE', + }), + ); + }); + }); }); diff --git a/ApiCore-Backend/src/honey-batches/honey-batches.service.ts b/ApiCore-Backend/src/honey-batches/honey-batches.service.ts index 07de6fa..185b5db 100644 --- a/ApiCore-Backend/src/honey-batches/honey-batches.service.ts +++ b/ApiCore-Backend/src/honey-batches/honey-batches.service.ts @@ -12,6 +12,7 @@ import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto'; import { UpdateHoneyBatchDto } from './dto/update-honey-batch.dto'; import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto'; import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto'; +import { SetHoneyBatchContainerAllocationDto } from './dto/set-honey-batch-container-allocation.dto'; const honeyBatchInclude = { measurements: true, @@ -160,6 +161,101 @@ export class HoneyBatchesService { return this.findById(id); } + // Additive to the single storageContainerId/relocate() flow above: covers + // a batch too large for one container (e.g. 125 kg split across 5 x 25 kg + // buckets). @@unique on [honeyBatchId, storageContainerId] makes this an + // upsert - re-assigning the same container just overwrites its amount. + async listContainerAllocations(id: string) { + await this.findById(id); + return this.prisma.honeyBatchContainerAllocation.findMany({ + where: { honeyBatchId: id }, + include: { storageContainer: true }, + orderBy: { createdAt: 'asc' }, + }); + } + + async setContainerAllocation( + id: string, + storageContainerId: string, + dto: SetHoneyBatchContainerAllocationDto, + actingUserId: string, + ) { + const honeyBatch = await this.findById(id); + const container = await this.prisma.storageContainer.findUnique({ + where: { id: storageContainerId }, + }); + if (!container) { + throw new NotFoundException('Storage container not found'); + } + + const otherAllocations = + await this.prisma.honeyBatchContainerAllocation.findMany({ + where: { + honeyBatchId: id, + storageContainerId: { not: storageContainerId }, + }, + }); + const otherTotal = otherAllocations.reduce((sum, a) => sum + a.amount, 0); + if ( + honeyBatch.amount !== null && + otherTotal + dto.amount > honeyBatch.amount + ) { + throw new BadRequestException( + `Zugewiesene Menge (${otherTotal + dto.amount} kg) übersteigt die Chargenmenge (${honeyBatch.amount} kg)`, + ); + } + + const allocation = await this.prisma.honeyBatchContainerAllocation.upsert({ + where: { + honeyBatchId_storageContainerId: { + honeyBatchId: id, + storageContainerId, + }, + }, + create: { honeyBatchId: id, storageContainerId, amount: dto.amount }, + update: { amount: dto.amount }, + include: { storageContainer: true }, + }); + await this.auditLog.record({ + entityType: 'HoneyBatchContainerAllocation', + entityId: allocation.id, + action: 'UPDATE', + userId: actingUserId, + afterData: allocation, + }); + return allocation; + } + + async removeContainerAllocation( + id: string, + storageContainerId: string, + actingUserId: string, + ): Promise { + await this.findById(id); + const allocation = + await this.prisma.honeyBatchContainerAllocation.findUnique({ + where: { + honeyBatchId_storageContainerId: { + honeyBatchId: id, + storageContainerId, + }, + }, + }); + if (!allocation) { + throw new NotFoundException('Zuweisung nicht gefunden'); + } + await this.prisma.honeyBatchContainerAllocation.delete({ + where: { id: allocation.id }, + }); + await this.auditLog.record({ + entityType: 'HoneyBatchContainerAllocation', + entityId: allocation.id, + action: 'DELETE', + userId: actingUserId, + beforeData: allocation, + }); + } + async findRelocations(id: string) { await this.findById(id); return this.prisma.honeyBatchRelocation.findMany({ diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/honey-batch-container-allocations-card.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/honey-batch-container-allocations-card.tsx new file mode 100644 index 0000000..bb79261 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/honey-batch-container-allocations-card.tsx @@ -0,0 +1,190 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { Trash2 } from "lucide-react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { + fetchHoneyBatchContainerAllocations, + removeHoneyBatchContainerAllocation, + setHoneyBatchContainerAllocation, +} from "@/lib/honey-batches/api"; +import { HoneyBatch } from "@/lib/honey-batches/types"; +import { fetchStorageContainers } from "@/lib/storage/api"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { EmptyState } from "@/components/empty-state"; +import { Boxes } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Additive to HoneyBatchStorageCard's single-container "Umlagern" flow above +// it on the page - covers a batch too large for one container (e.g. 125 kg +// split across 5 x 25 kg buckets), which a single storageContainerId can't +// represent. +export function HoneyBatchContainerAllocationsCard({ honeyBatch }: { honeyBatch: HoneyBatch }) { + const queryClient = useQueryClient(); + const [storageContainerId, setStorageContainerId] = useState(""); + const [amount, setAmount] = useState(""); + const [error, setError] = useState(null); + + const { data: containers } = useQuery({ + queryKey: ["storage-containers"], + queryFn: fetchStorageContainers, + }); + + const { + data: allocations, + isLoading, + } = useQuery({ + queryKey: ["honey-batches", honeyBatch.id, "containers"], + queryFn: () => fetchHoneyBatchContainerAllocations(honeyBatch.id), + }); + + const assignMutation = useMutation({ + mutationFn: (input: { storageContainerId: string; amount: number }) => + setHoneyBatchContainerAllocation(honeyBatch.id, input.storageContainerId, input.amount), + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: ["honey-batches", honeyBatch.id, "containers"], + }); + setStorageContainerId(""); + setAmount(""); + setError(null); + }, + onError: () => { + setError("Zuweisung konnte nicht gespeichert werden. Übersteigt die Menge die Charge?"); + }, + }); + + const removeMutation = useMutation({ + mutationFn: (containerId: string) => + removeHoneyBatchContainerAllocation(honeyBatch.id, containerId), + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: ["honey-batches", honeyBatch.id, "containers"], + }); + toast.success("Zuweisung entfernt"); + }, + onError: () => toast.error("Zuweisung konnte nicht entfernt werden"), + }); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + setError(null); + if (!storageContainerId) { + setError("Bitte einen Lagerbehälter auswählen."); + return; + } + const parsedAmount = Number(amount); + if (!amount || parsedAmount <= 0) { + setError("Bitte eine Menge größer als 0 angeben."); + return; + } + assignMutation.mutate({ storageContainerId, amount: parsedAmount }); + } + + const totalAllocated = (allocations ?? []).reduce((sum, a) => sum + a.amount, 0); + + return ( + + + Aufteilung auf mehrere Behälter + + +

+ Für Chargen, die nicht in einen einzigen Behälter passen (z. B. 125 kg auf 5 Eimer à 25 + kg). Unabhängig vom Lagerbehälter oben. +

+ + {honeyBatch.amount !== null ? ( +

+ Zugewiesen: {totalAllocated} kg von {honeyBatch.amount} kg +

+ ) : null} + + {isLoading ?

Lädt…

: null} + {!isLoading && (!allocations || allocations.length === 0) ? ( + + ) : null} + {allocations?.map((allocation) => ( +
+
+

+ {allocation.storageContainer.code} + {allocation.storageContainer.location + ? ` · ${allocation.storageContainer.location}` + : ""} +

+

{allocation.amount} kg

+
+ +
+ ))} + +
+
+ + +
+
+ + setAmount(event.target.value)} + /> +
+ {error ? ( +

+ {error} +

+ ) : null} + +
+
+
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx index 7af0c52..894cc20 100644 --- a/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/honey-batches/[id]/page.tsx @@ -11,6 +11,7 @@ import { QrCodeDialog } from "@/components/qr-code-dialog"; import { DocumentUploadCard } from "@/components/document-upload-card"; import { EmptyState } from "@/components/empty-state"; import { HoneyBatchStorageCard } from "./honey-batch-storage-card"; +import { HoneyBatchContainerAllocationsCard } from "./honey-batch-container-allocations-card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -83,6 +84,7 @@ export default function HoneyBatchDetailPage() { +
diff --git a/ApiCore-Frontend/src/lib/honey-batches/api.ts b/ApiCore-Frontend/src/lib/honey-batches/api.ts index 9cc1138..da833ca 100644 --- a/ApiCore-Frontend/src/lib/honey-batches/api.ts +++ b/ApiCore-Frontend/src/lib/honey-batches/api.ts @@ -3,6 +3,7 @@ import { CreateHoneyBatchInput, CreateHoneyMeasurementInput, HoneyBatch, + HoneyBatchContainerAllocation, HoneyBatchRelocation, HoneyBatchStatus, RelocateHoneyBatchInput, @@ -69,3 +70,29 @@ export function relocateHoneyBatch( export function fetchHoneyBatchRelocations(id: string): Promise { return apiFetch(`/honey-batches/${id}/relocations`); } + +export function fetchHoneyBatchContainerAllocations( + id: string, +): Promise { + return apiFetch(`/honey-batches/${id}/containers`); +} + +export function setHoneyBatchContainerAllocation( + id: string, + containerId: string, + amount: number, +): Promise { + return apiFetch( + `/honey-batches/${id}/containers/${containerId}`, + { method: "PUT", body: JSON.stringify({ amount }) }, + ); +} + +export function removeHoneyBatchContainerAllocation( + id: string, + containerId: string, +): Promise { + return apiFetch(`/honey-batches/${id}/containers/${containerId}`, { + method: "DELETE", + }); +} diff --git a/ApiCore-Frontend/src/lib/honey-batches/types.ts b/ApiCore-Frontend/src/lib/honey-batches/types.ts index 89dab36..6a4705f 100644 --- a/ApiCore-Frontend/src/lib/honey-batches/types.ts +++ b/ApiCore-Frontend/src/lib/honey-batches/types.ts @@ -37,6 +37,14 @@ export interface HoneyBatch { measurements: HoneyMeasurement[]; } +export interface HoneyBatchContainerAllocation { + id: string; + honeyBatchId: string; + storageContainerId: string; + storageContainer: StorageContainerRef; + amount: number; +} + export interface HoneyBatchRelocation { id: string; fromStorageContainerId: string | null;