feat: allow a honey batch to be split across multiple storage containers
Backend Image / build-and-push (push) Successful in 3m9s
Frontend Image / build-and-push (push) Successful in 2m45s

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 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-04 19:44:33 +02:00
1 parent c91dc9ebf1
commit 1054a845ef
10 files changed
+579 -7

No files matched your search

@@ -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;
+29 -6
View File
@@ -708,9 +708,10 @@ model HoneyBatch {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
measurements HoneyMeasurement[] measurements HoneyMeasurement[]
bottlingBatches BottlingBatch[] bottlingBatches BottlingBatch[]
relocations HoneyBatchRelocation[] relocations HoneyBatchRelocation[]
containerAllocations HoneyBatchContainerAllocation[]
@@index([status]) @@index([status])
@@index([extractionId]) @@index([extractionId])
@@ -756,9 +757,10 @@ model StorageContainer {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
honeyBatches HoneyBatch[] honeyBatches HoneyBatch[]
relocationsFrom HoneyBatchRelocation[] @relation("RelocationFrom") relocationsFrom HoneyBatchRelocation[] @relation("RelocationFrom")
relocationsTo HoneyBatchRelocation[] @relation("RelocationTo") relocationsTo HoneyBatchRelocation[] @relation("RelocationTo")
batchAllocations HoneyBatchContainerAllocation[]
@@index([status]) @@index([status])
} }
@@ -781,6 +783,27 @@ model HoneyBatchRelocation {
@@index([honeyBatchId, relocatedAt]) @@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 { enum StockCategory {
HONEY HONEY
CONSUMABLE CONSUMABLE
@@ -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;
}
@@ -1,10 +1,12 @@
import { import {
Body, Body,
Controller, Controller,
Delete,
Get, Get,
Param, Param,
Patch, Patch,
Post, Post,
Put,
Query, Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; 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 { UpdateHoneyBatchDto } from './dto/update-honey-batch.dto';
import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto'; import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.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'; import { HoneyBatchesService } from './honey-batches.service';
@ApiTags('honey-batches') @ApiTags('honey-batches')
@@ -48,6 +51,42 @@ export class HoneyBatchesController {
return this.honeyBatchesService.findRelocations(id); 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') @RequirePermission('honey-batches:relocate')
@Post(':id/relocate') @Post(':id/relocate')
relocate( relocate(
@@ -1,4 +1,4 @@
import { NotFoundException } from '@nestjs/common'; import { BadRequestException, NotFoundException } from '@nestjs/common';
import { HoneyBatchesService } from './honey-batches.service'; import { HoneyBatchesService } from './honey-batches.service';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { AuditLogService } from '../audit/audit-log.service'; import { AuditLogService } from '../audit/audit-log.service';
@@ -17,6 +17,12 @@ describe('HoneyBatchesService', () => {
honeyMeasurement: { create: jest.Mock }; honeyMeasurement: { create: jest.Mock };
storageContainer: { findUnique: jest.Mock }; storageContainer: { findUnique: jest.Mock };
honeyBatchRelocation: { create: jest.Mock; findMany: 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; $transaction: jest.Mock;
}; };
let auditLog: { record: jest.Mock }; let auditLog: { record: jest.Mock };
@@ -57,6 +63,12 @@ describe('HoneyBatchesService', () => {
honeyMeasurement: { create: jest.fn() }, honeyMeasurement: { create: jest.fn() },
storageContainer: { findUnique: jest.fn() }, storageContainer: { findUnique: jest.fn() },
honeyBatchRelocation: { create: jest.fn(), findMany: 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(), $transaction: jest.fn(),
}; };
auditLog = { record: jest.fn() }; auditLog = { record: jest.fn() };
@@ -199,4 +211,147 @@ describe('HoneyBatchesService', () => {
expect(findArg.where.honeyBatchId).toBe('batch-1'); 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',
}),
);
});
});
}); });
@@ -12,6 +12,7 @@ import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto';
import { UpdateHoneyBatchDto } from './dto/update-honey-batch.dto'; import { UpdateHoneyBatchDto } from './dto/update-honey-batch.dto';
import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto'; import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto'; import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto';
import { SetHoneyBatchContainerAllocationDto } from './dto/set-honey-batch-container-allocation.dto';
const honeyBatchInclude = { const honeyBatchInclude = {
measurements: true, measurements: true,
@@ -160,6 +161,101 @@ export class HoneyBatchesService {
return this.findById(id); 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<void> {
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) { async findRelocations(id: string) {
await this.findById(id); await this.findById(id);
return this.prisma.honeyBatchRelocation.findMany({ return this.prisma.honeyBatchRelocation.findMany({
@@ -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<string | null>(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<HTMLFormElement>) {
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 (
<Card>
<CardHeader>
<CardTitle>Aufteilung auf mehrere Behälter</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4 text-sm">
<p className="text-xs text-muted-foreground">
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.
</p>
{honeyBatch.amount !== null ? (
<p className="text-muted-foreground">
Zugewiesen: {totalAllocated} kg von {honeyBatch.amount} kg
</p>
) : null}
{isLoading ? <p className="text-muted-foreground">Lädt…</p> : null}
{!isLoading && (!allocations || allocations.length === 0) ? (
<EmptyState
icon={Boxes}
title="Noch keine Aufteilung"
description="Weise dieser Charge unten mehrere Behälter mit jeweiliger Menge zu."
/>
) : null}
{allocations?.map((allocation) => (
<div
key={allocation.id}
className="flex items-center justify-between rounded-md border p-3"
>
<div>
<p className="font-medium">
{allocation.storageContainer.code}
{allocation.storageContainer.location
? ` · ${allocation.storageContainer.location}`
: ""}
</p>
<p className="text-muted-foreground">{allocation.amount} kg</p>
</div>
<Button
type="button"
variant="ghost"
size="icon"
disabled={removeMutation.isPending}
onClick={() => removeMutation.mutate(allocation.storageContainerId)}
aria-label="Zuweisung entfernen"
>
<Trash2 className="size-4" />
</Button>
</div>
))}
<form className="flex flex-col gap-3 border-t border-border/60 pt-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="allocation-container">Behälter</Label>
<Select
value={storageContainerId}
onValueChange={(value) => setStorageContainerId(value ?? "")}
>
<SelectTrigger id="allocation-container">
<SelectValue placeholder="Lagerbehälter wählen" />
</SelectTrigger>
<SelectContent>
{containers?.map((container) => (
<SelectItem key={container.id} value={container.id}>
{container.code}
{container.location ? ` · ${container.location}` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="allocation-amount">Menge (kg)</Label>
<Input
id="allocation-amount"
type="number"
inputMode="decimal"
value={amount}
onChange={(event) => setAmount(event.target.value)}
/>
</div>
{error ? (
<p role="alert" className="text-sm text-destructive">
{error}
</p>
) : null}
<Button type="submit" variant="outline" disabled={assignMutation.isPending}>
{assignMutation.isPending ? "Wird zugewiesen…" : "Behälter zuweisen"}
</Button>
</form>
</CardContent>
</Card>
);
}
@@ -11,6 +11,7 @@ import { QrCodeDialog } from "@/components/qr-code-dialog";
import { DocumentUploadCard } from "@/components/document-upload-card"; import { DocumentUploadCard } from "@/components/document-upload-card";
import { EmptyState } from "@/components/empty-state"; import { EmptyState } from "@/components/empty-state";
import { HoneyBatchStorageCard } from "./honey-batch-storage-card"; import { HoneyBatchStorageCard } from "./honey-batch-storage-card";
import { HoneyBatchContainerAllocationsCard } from "./honey-batch-container-allocations-card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -83,6 +84,7 @@ export default function HoneyBatchDetailPage() {
</CardContent> </CardContent>
</Card> </Card>
<HoneyBatchStorageCard honeyBatch={honeyBatch} /> <HoneyBatchStorageCard honeyBatch={honeyBatch} />
<HoneyBatchContainerAllocationsCard honeyBatch={honeyBatch} />
</div> </div>
<div className="flex flex-col gap-4 lg:col-span-2"> <div className="flex flex-col gap-4 lg:col-span-2">
@@ -3,6 +3,7 @@ import {
CreateHoneyBatchInput, CreateHoneyBatchInput,
CreateHoneyMeasurementInput, CreateHoneyMeasurementInput,
HoneyBatch, HoneyBatch,
HoneyBatchContainerAllocation,
HoneyBatchRelocation, HoneyBatchRelocation,
HoneyBatchStatus, HoneyBatchStatus,
RelocateHoneyBatchInput, RelocateHoneyBatchInput,
@@ -69,3 +70,29 @@ export function relocateHoneyBatch(
export function fetchHoneyBatchRelocations(id: string): Promise<HoneyBatchRelocation[]> { export function fetchHoneyBatchRelocations(id: string): Promise<HoneyBatchRelocation[]> {
return apiFetch<HoneyBatchRelocation[]>(`/honey-batches/${id}/relocations`); return apiFetch<HoneyBatchRelocation[]>(`/honey-batches/${id}/relocations`);
} }
export function fetchHoneyBatchContainerAllocations(
id: string,
): Promise<HoneyBatchContainerAllocation[]> {
return apiFetch<HoneyBatchContainerAllocation[]>(`/honey-batches/${id}/containers`);
}
export function setHoneyBatchContainerAllocation(
id: string,
containerId: string,
amount: number,
): Promise<HoneyBatchContainerAllocation> {
return apiFetch<HoneyBatchContainerAllocation>(
`/honey-batches/${id}/containers/${containerId}`,
{ method: "PUT", body: JSON.stringify({ amount }) },
);
}
export function removeHoneyBatchContainerAllocation(
id: string,
containerId: string,
): Promise<void> {
return apiFetch<void>(`/honey-batches/${id}/containers/${containerId}`, {
method: "DELETE",
});
}
@@ -37,6 +37,14 @@ export interface HoneyBatch {
measurements: HoneyMeasurement[]; measurements: HoneyMeasurement[];
} }
export interface HoneyBatchContainerAllocation {
id: string;
honeyBatchId: string;
storageContainerId: string;
storageContainer: StorageContainerRef;
amount: number;
}
export interface HoneyBatchRelocation { export interface HoneyBatchRelocation {
id: string; id: string;
fromStorageContainerId: string | null; fromStorageContainerId: string | null;