fix: Bug fixed für alle Seiten und das Profil und label
Backend Image / build-and-push (push) Successful in 2m44s
Frontend Image / build-and-push (push) Successful in 2m47s

This commit is contained in:
Nick Szaunig committed 2026-08-27 15:15:55 +02:00
1 parent 133edcd5ba
commit 0a52b29c57
52 files changed
+1290 -129

No files matched your search

@@ -14,6 +14,7 @@ import { Colony, ColonyType } from "@/lib/colonies/types";
import { COLONY_TYPES, COLONY_TYPE_LABELS } from "@/lib/colonies/labels";
import { fetchApiaries } from "@/lib/apiaries/api";
import { fetchColonies } from "@/lib/colonies/api";
import { assignQueenColony, fetchQueens } from "@/lib/queens/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -178,6 +179,70 @@ function SplitDialog({ colony }: { colony: Colony }) {
);
}
function AssignQueenDialog({ colony }: { colony: Colony }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [queenId, setQueenId] = useState("");
const { data: queens } = useQuery({ queryKey: ["queens"], queryFn: () => fetchQueens() });
const currentQueen = queens?.find((queen) => queen.id === colony.currentQueenId);
const mutation = useMutation({
mutationFn: () => assignQueenColony(queenId, { colonyId: colony.id }),
onSuccess: () => {
invalidateColony(queryClient, colony.id);
void queryClient.invalidateQueries({ queryKey: ["queens"] });
setOpen(false);
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="outline" disabled={colony.status !== "ACTIVE"}>
{currentQueen ? `Königin wechseln (${currentQueen.code})` : "Königin zuweisen"}
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Königin zuweisen</DialogTitle>
</DialogHeader>
<form
className="flex flex-col gap-4"
onSubmit={(event: FormEvent) => {
event.preventDefault();
mutation.mutate();
}}
>
<div className="flex flex-col gap-2">
<Label htmlFor="assign-queen">Königin</Label>
<Select value={queenId} onValueChange={(value) => setQueenId(value ?? "")}>
<SelectTrigger id="assign-queen">
<SelectValue placeholder="Königin wählen" />
</SelectTrigger>
<SelectContent>
{(queens ?? [])
.filter((queen) => queen.status === "ACTIVE")
.map((queen) => (
<SelectItem key={queen.id} value={queen.id}>
{queen.code}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DialogFooter>
<Button type="submit" disabled={!queenId || mutation.isPending}>
Zuweisen
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
function MergeDialog({ colony }: { colony: Colony }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
@@ -298,6 +363,7 @@ export function ColonyActions({ colony }: { colony: Colony }) {
{colony.status === "ACTIVE" ? (
<>
<MoveDialog colony={colony} />
<AssignQueenDialog colony={colony} />
<SplitDialog colony={colony} />
<MergeDialog colony={colony} />
<DissolveButton colony={colony} />
@@ -17,6 +17,7 @@ import {
import { statusTone } from "@/lib/status-tone";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
@@ -74,6 +75,8 @@ function ConditionSelect({
export function ColonyEditCard({ colony }: { colony: Colony }) {
const queryClient = useQueryClient();
const [colonyType, setColonyType] = useState<ColonyType>(colony.colonyType);
const [hiveType, setHiveType] = useState(colony.hiveType ?? "");
const [originText, setOriginText] = useState(colony.originText ?? "");
const [strength, setStrength] = useState(colony.strength?.toString() ?? "");
const [broodStatus, setBroodStatus] = useState<ColonyCondition | undefined>(
colony.broodStatus ?? undefined,
@@ -90,6 +93,8 @@ export function ColonyEditCard({ colony }: { colony: Colony }) {
mutationFn: () =>
updateColony(colony.id, {
colonyType,
hiveType: hiveType || undefined,
originText: originText || undefined,
strength: strength ? Number(strength) : undefined,
broodStatus,
feedStatus,
@@ -141,6 +146,22 @@ export function ColonyEditCard({ colony }: { colony: Colony }) {
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="colony-hive-type">Beutentyp</Label>
<Input
id="colony-hive-type"
value={hiveType}
onChange={(event) => setHiveType(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="colony-origin">Herkunft</Label>
<Input
id="colony-origin"
value={originText}
onChange={(event) => setOriginText(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="colony-strength">Volksstärke</Label>
<Select value={strength} onValueChange={(value) => setStrength(value ?? "")}>
@@ -4,6 +4,7 @@ import Link from "next/link";
import { Utensils } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchColonyFeedings, fetchColonyFeedingsSummary } from "@/lib/feedings/api";
import { FEED_TYPE_LABELS } from "@/lib/feedings/labels";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/empty-state";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -52,7 +53,8 @@ export function ColonyFeedingsCard({ colonyId }: { colonyId: string }) {
<div key={feeding.id} className="rounded-md border p-3">
<div className="flex items-center justify-between">
<p className="font-medium">
{new Date(feeding.date).toLocaleDateString("de-DE")} · {feeding.feedType}
{new Date(feeding.date).toLocaleDateString("de-DE")} ·{" "}
{FEED_TYPE_LABELS[feeding.feedType]}
</p>
<FeedingEditDialog feeding={feeding} />
</div>
@@ -4,6 +4,7 @@ import Link from "next/link";
import { Bug } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchColonyVarroaMeasurements } from "@/lib/varroa/api";
import { VARROA_METHOD_LABELS } from "@/lib/varroa/labels";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/empty-state";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -78,7 +79,8 @@ export function ColonyVarroaCard({ colonyId }: { colonyId: string }) {
<div key={measurement.id} className="rounded-md border p-3">
<div className="flex items-center justify-between">
<p className="font-medium">
{new Date(measurement.date).toLocaleDateString("de-DE")} · {measurement.method}
{new Date(measurement.date).toLocaleDateString("de-DE")} ·{" "}
{VARROA_METHOD_LABELS[measurement.method]}
</p>
<VarroaEditDialog measurement={measurement} />
</div>
@@ -5,6 +5,7 @@ import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateFeeding } from "@/lib/feedings/api";
import { Feeding, FeedType } from "@/lib/feedings/types";
import { FEED_TYPES, FEED_TYPE_LABELS } from "@/lib/feedings/labels";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -24,14 +25,6 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
const FEED_TYPE_OPTIONS: FeedType[] = [
"SUGAR_WATER",
"FEED_SYRUP",
"FEED_PASTE",
"OWN_HONEY",
"OTHER",
];
export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
@@ -96,12 +89,12 @@ export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
onValueChange={(value) => setFeedType((value ?? feedType) as FeedType)}
>
<SelectTrigger id="feeding-edit-type" className="w-full">
<SelectValue />
<SelectValue>{FEED_TYPE_LABELS[feedType]}</SelectValue>
</SelectTrigger>
<SelectContent>
{FEED_TYPE_OPTIONS.map((option) => (
{FEED_TYPES.map((option) => (
<SelectItem key={option} value={option}>
{option}
{FEED_TYPE_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -5,6 +5,7 @@ import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateVarroaMeasurement } from "@/lib/varroa/api";
import { VarroaMeasurement, VarroaMethod } from "@/lib/varroa/types";
import { VARROA_METHODS, VARROA_METHOD_LABELS } from "@/lib/varroa/labels";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -24,13 +25,6 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
const METHOD_OPTIONS: VarroaMethod[] = [
"NATURAL_MITE_FALL",
"POWDERED_SUGAR",
"WASHING_METHOD",
"DRONE_BROOD_CONTROL",
];
export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasurement }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
@@ -97,12 +91,12 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme
onValueChange={(value) => setMethod((value ?? method) as VarroaMethod)}
>
<SelectTrigger id="varroa-edit-method" className="w-full">
<SelectValue />
<SelectValue>{VARROA_METHOD_LABELS[method]}</SelectValue>
</SelectTrigger>
<SelectContent>
{METHOD_OPTIONS.map((option) => (
{VARROA_METHODS.map((option) => (
<SelectItem key={option} value={option}>
{option}
{VARROA_METHOD_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -29,6 +29,8 @@ export default function NewColonyPage() {
const [code, setCode] = useState("");
const [apiaryId, setApiaryId] = useState("");
const [colonyType, setColonyType] = useState<ColonyType>("PRODUCTION_COLONY");
const [hiveType, setHiveType] = useState("");
const [originText, setOriginText] = useState("");
const [error, setError] = useState<string | null>(null);
const mutation = useMutation({
@@ -49,7 +51,13 @@ export default function NewColonyPage() {
setError("Bitte einen Bienenstand auswählen.");
return;
}
mutation.mutate({ code, apiaryId, colonyType });
mutation.mutate({
code,
apiaryId,
colonyType,
hiveType: hiveType || undefined,
originText: originText || undefined,
});
}
return (
@@ -104,6 +112,22 @@ export default function NewColonyPage() {
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="colony-hive-type">Beutentyp</Label>
<Input
id="colony-hive-type"
value={hiveType}
onChange={(event) => setHiveType(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="colony-origin">Herkunft</Label>
<Input
id="colony-origin"
value={originText}
onChange={(event) => setOriginText(event.target.value)}
/>
</div>
{error ? (
<p role="alert" className="text-sm text-destructive">
{error}
@@ -5,6 +5,7 @@ import { Wrench } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { fetchEquipment, updateEquipment } from "@/lib/equipment/api";
import { EquipmentCondition } from "@/lib/equipment/types";
import { EQUIPMENT_CONDITIONS, EQUIPMENT_CONDITION_LABELS } from "@/lib/equipment/labels";
import { statusTone } from "@/lib/status-tone";
import { Badge } from "@/components/ui/badge";
import {
@@ -16,8 +17,6 @@ import {
} from "@/components/ui/select";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
const CONDITIONS: EquipmentCondition[] = ["NEW", "GOOD", "WORN", "DEFECTIVE"];
export default function EquipmentDetailPage() {
const params = useParams<{ id: string }>();
const queryClient = useQueryClient();
@@ -59,7 +58,9 @@ export default function EquipmentDetailPage() {
</span>
<CardTitle>{equipment.name}</CardTitle>
</div>
<Badge variant={statusTone(equipment.condition)}>{equipment.condition}</Badge>
<Badge variant={statusTone(equipment.condition)}>
{EQUIPMENT_CONDITION_LABELS[equipment.condition]}
</Badge>
</CardHeader>
<CardContent className="flex flex-col gap-3 text-sm">
<p>
@@ -85,12 +86,12 @@ export default function EquipmentDetailPage() {
}
>
<SelectTrigger className="w-40">
<SelectValue />
<SelectValue>{EQUIPMENT_CONDITION_LABELS[equipment.condition]}</SelectValue>
</SelectTrigger>
<SelectContent>
{CONDITIONS.map((condition) => (
{EQUIPMENT_CONDITIONS.map((condition) => (
<SelectItem key={condition} value={condition}>
{condition}
{EQUIPMENT_CONDITION_LABELS[condition]}
</SelectItem>
))}
</SelectContent>
@@ -4,6 +4,7 @@ import Link from "next/link";
import { Wrench } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchEquipmentList } from "@/lib/equipment/api";
import { EQUIPMENT_CONDITION_LABELS } from "@/lib/equipment/labels";
import { statusTone } from "@/lib/status-tone";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -56,7 +57,11 @@ export default function EquipmentPage() {
gradient="var(--gradient-sage)"
title={item.name}
subtitle={item.inventoryNumber}
badge={<Badge variant={statusTone(item.condition)}>{item.condition}</Badge>}
badge={
<Badge variant={statusTone(item.condition)}>
{EQUIPMENT_CONDITION_LABELS[item.condition]}
</Badge>
}
fields={[{ label: "Standort", value: item.location ?? "—" }]}
/>
))}
@@ -8,6 +8,7 @@ import { Utensils } from "lucide-react";
import { fetchColonies } from "@/lib/colonies/api";
import { createFeeding } from "@/lib/feedings/api";
import { FeedType } from "@/lib/feedings/types";
import { FEED_TYPES, FEED_TYPE_LABELS } from "@/lib/feedings/labels";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -22,14 +23,6 @@ import {
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
const FEED_TYPE_OPTIONS: FeedType[] = [
"SUGAR_WATER",
"FEED_SYRUP",
"FEED_PASTE",
"OWN_HONEY",
"OTHER",
];
function todayIsoDate(): string {
return new Date().toISOString().slice(0, 10);
}
@@ -136,12 +129,12 @@ export default function NewFeedingPage() {
onValueChange={(value) => setFeedType((value ?? "SUGAR_WATER") as FeedType)}
>
<SelectTrigger id="feeding-type" className="h-11 w-full">
<SelectValue />
<SelectValue>{FEED_TYPE_LABELS[feedType]}</SelectValue>
</SelectTrigger>
<SelectContent>
{FEED_TYPE_OPTIONS.map((option) => (
{FEED_TYPES.map((option) => (
<SelectItem key={option} value={option}>
{option}
{FEED_TYPE_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -0,0 +1,98 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateHoneyBatch } from "@/lib/honey-batches/api";
import { HoneyBatch } from "@/lib/honey-batches/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function HoneyBatchEditDialog({ honeyBatch }: { honeyBatch: HoneyBatch }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [honeyType, setHoneyType] = useState(honeyBatch.honeyType ?? "");
const [amount, setAmount] = useState(honeyBatch.amount?.toString() ?? "");
const [waterContent, setWaterContent] = useState(honeyBatch.waterContent?.toString() ?? "");
const mutation = useMutation({
mutationFn: () =>
updateHoneyBatch(honeyBatch.id, {
honeyType: honeyType || undefined,
amount: amount ? Number(amount) : undefined,
waterContent: waterContent ? Number(waterContent) : undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["honey-batches", honeyBatch.id] });
void queryClient.invalidateQueries({ queryKey: ["honey-batches"] });
setOpen(false);
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Honigcharge bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="batch-edit-honey-type">Sorte</Label>
<Input
id="batch-edit-honey-type"
value={honeyType}
onChange={(event) => setHoneyType(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="batch-edit-amount">Menge (kg)</Label>
<Input
id="batch-edit-amount"
type="number"
inputMode="decimal"
value={amount}
onChange={(event) => setAmount(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="batch-edit-water">Wassergehalt (%)</Label>
<Input
id="batch-edit-water"
type="number"
step="0.1"
value={waterContent}
onChange={(event) => setWaterContent(event.target.value)}
/>
</div>
<DialogFooter>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -5,6 +5,7 @@ import { useParams } from "next/navigation";
import { Droplet, FlaskConical } from "lucide-react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api";
import { HONEY_BATCH_STATUS_LABELS } from "@/lib/honey-batches/labels";
import { exportHoneyBatchTraceabilityPdf } from "@/lib/export/api";
import { QrCodeDialog } from "@/components/qr-code-dialog";
import { DocumentUploadCard } from "@/components/document-upload-card";
@@ -15,6 +16,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AddMeasurementDialog } from "./add-measurement-dialog";
import { ExtractionEditDialog } from "./extraction-edit-dialog";
import { HoneyBatchEditDialog } from "./honey-batch-edit-dialog";
export default function HoneyBatchDetailPage() {
const params = useParams<{ id: string }>();
@@ -56,13 +58,17 @@ export default function HoneyBatchDetailPage() {
<CardTitle className="font-mono">{honeyBatch.batchNumber}</CardTitle>
</div>
<div className="flex items-center gap-2">
<Badge>{honeyBatch.status}</Badge>
<Badge>{HONEY_BATCH_STATUS_LABELS[honeyBatch.status]}</Badge>
<HoneyBatchEditDialog honeyBatch={honeyBatch} />
<QrCodeDialog entityType="honey-batches" id={honeyBatch.id} />
</div>
</CardHeader>
<CardContent className="flex flex-col gap-3 text-sm">
{honeyBatch.honeyType ? <p>Sorte: {honeyBatch.honeyType}</p> : null}
{honeyBatch.amount !== null ? <p>Menge: {honeyBatch.amount} kg</p> : null}
{honeyBatch.waterContent !== null ? (
<p>Wassergehalt: {honeyBatch.waterContent}%</p>
) : null}
<Button
variant="outline"
size="sm"
@@ -5,6 +5,7 @@ import { Droplet } from "lucide-react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
import { HoneyBatchStatus } from "@/lib/honey-batches/types";
import { HONEY_BATCH_STATUS_LABELS } from "@/lib/honey-batches/labels";
import { exportHoneyBatchesCsv } from "@/lib/export/api";
import { statusTone } from "@/lib/status-tone";
import { Badge } from "@/components/ui/badge";
@@ -90,7 +91,11 @@ export default function HoneyBatchesPage() {
icon={Droplet}
title={<span className="font-mono">{honeyBatch.batchNumber}</span>}
subtitle={honeyBatch.honeyType ?? "—"}
badge={<Badge variant={statusTone(honeyBatch.status)}>{honeyBatch.status}</Badge>}
badge={
<Badge variant={statusTone(honeyBatch.status)}>
{HONEY_BATCH_STATUS_LABELS[honeyBatch.status]}
</Badge>
}
fields={[
{
label: "Menge",
@@ -0,0 +1,130 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateProduct } from "@/lib/products/api";
import { Product } from "@/lib/products/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function EditProductDialog({ product }: { product: Product }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [name, setName] = useState(product.name);
const [description, setDescription] = useState(product.description ?? "");
const [sku, setSku] = useState(product.sku ?? "");
const [packagingSize, setPackagingSize] = useState(product.packagingSize?.toString() ?? "");
const [weight, setWeight] = useState(product.weight?.toString() ?? "");
const [salesUnit, setSalesUnit] = useState(product.salesUnit ?? "");
const mutation = useMutation({
mutationFn: () =>
updateProduct(product.id, {
name,
description: description || undefined,
sku: sku || undefined,
packagingSize: packagingSize ? Number(packagingSize) : undefined,
weight: weight ? Number(weight) : undefined,
salesUnit: salesUnit || undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["products"] });
setOpen(false);
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Produkt bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="product-edit-name">Name</Label>
<Input
id="product-edit-name"
required
value={name}
onChange={(event) => setName(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="product-edit-description">Beschreibung</Label>
<Textarea
id="product-edit-description"
value={description}
onChange={(event) => setDescription(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="product-edit-sku">SKU</Label>
<Input
id="product-edit-sku"
value={sku}
onChange={(event) => setSku(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="product-edit-packaging-size">Gebindegröße</Label>
<Input
id="product-edit-packaging-size"
type="number"
step="0.01"
value={packagingSize}
onChange={(event) => setPackagingSize(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="product-edit-weight">Gewicht</Label>
<Input
id="product-edit-weight"
type="number"
step="0.01"
value={weight}
onChange={(event) => setWeight(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="product-edit-sales-unit">Verkaufseinheit</Label>
<Input
id="product-edit-sales-unit"
placeholder="z. B. Glas, Flasche"
value={salesUnit}
onChange={(event) => setSalesUnit(event.target.value)}
/>
</div>
<DialogFooter>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -5,6 +5,8 @@ import Link from "next/link";
import { ShoppingBag } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchProducts } from "@/lib/products/api";
import { PRODUCT_STATUS_LABELS } from "@/lib/products/labels";
import { EditProductDialog } from "./edit-product-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { PageHeader } from "@/components/page-header";
@@ -72,13 +74,14 @@ export default function ProductsPage() {
title={product.name}
badge={
<Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}>
{product.status}
{PRODUCT_STATUS_LABELS[product.status]}
</Badge>
}
fields={[
{ label: "SKU", value: product.sku ?? "—" },
{ label: "Einheit", value: product.salesUnit ?? "—" },
]}
actions={<EditProductDialog product={product} />}
/>
))}
</div>
@@ -5,6 +5,7 @@ import { Crown } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { fetchQueens, updateQueen } from "@/lib/queens/api";
import { Queen, QueenStatus } from "@/lib/queens/types";
import { QUEEN_STATUSES, QUEEN_STATUS_LABELS } from "@/lib/queens/labels";
import { statusTone } from "@/lib/status-tone";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -25,17 +26,6 @@ import {
CardTitle,
} from "@/components/ui/card";
const QUEEN_STATUSES: QueenStatus[] = [
"ACTIVE",
"RESERVE",
"UNMATED",
"MATED",
"LOST",
"REPLACED",
"SOLD",
"DECEASED",
];
export function QueenEditCard({ queen }: { queen: Queen }) {
const queryClient = useQueryClient();
const [breed, setBreed] = useState(queen.breed ?? "");
@@ -82,7 +72,7 @@ export function QueenEditCard({ queen }: { queen: Queen }) {
</span>
<CardTitle className="font-mono">{queen.code}</CardTitle>
</div>
<Badge variant={statusTone(queen.status)}>{queen.status}</Badge>
<Badge variant={statusTone(queen.status)}>{QUEEN_STATUS_LABELS[queen.status]}</Badge>
</CardHeader>
<CardContent>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
@@ -131,12 +121,12 @@ export function QueenEditCard({ queen }: { queen: Queen }) {
<Label htmlFor="queen-status">Status</Label>
<Select value={status} onValueChange={(value) => setStatus(value as QueenStatus)}>
<SelectTrigger id="queen-status">
<SelectValue />
<SelectValue>{QUEEN_STATUS_LABELS[status]}</SelectValue>
</SelectTrigger>
<SelectContent>
{QUEEN_STATUSES.map((option) => (
<SelectItem key={option} value={option}>
{option}
{QUEEN_STATUS_LABELS[option]}
</SelectItem>
))}
</SelectContent>
+7 -15
View File
@@ -6,6 +6,7 @@ import { Crown } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchQueens } from "@/lib/queens/api";
import { QueenStatus } from "@/lib/queens/types";
import { QUEEN_STATUSES, QUEEN_STATUS_LABELS } from "@/lib/queens/labels";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { PageHeader } from "@/components/page-header";
@@ -20,17 +21,6 @@ import {
SelectValue,
} from "@/components/ui/select";
const STATUS_OPTIONS: QueenStatus[] = [
"ACTIVE",
"RESERVE",
"UNMATED",
"MATED",
"LOST",
"REPLACED",
"SOLD",
"DECEASED",
];
export default function QueensPage() {
const [tab, setTab] = useState<EntityLifecycleTab>("active");
const [status, setStatus] = useState<QueenStatus | "">("");
@@ -74,13 +64,15 @@ export default function QueensPage() {
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as QueenStatus))}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
<SelectValue placeholder="Status">
{status ? QUEEN_STATUS_LABELS[status] : "Alle Status"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">Alle Status</SelectItem>
{STATUS_OPTIONS.map((option) => (
{QUEEN_STATUSES.map((option) => (
<SelectItem key={option} value={option}>
{option}
{QUEEN_STATUS_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -118,7 +110,7 @@ export default function QueensPage() {
subtitle={queen.breed ?? "—"}
badge={
<Badge variant={queen.status === "ACTIVE" ? "default" : "secondary"}>
{queen.status}
{QUEEN_STATUS_LABELS[queen.status]}
</Badge>
}
fields={[{ label: "Schlupfjahr", value: queen.hatchYear ?? "—" }]}
@@ -35,9 +35,9 @@ describe("AuditLogPage", () => {
renderWithClient(<AuditLogPage />);
expect(await screen.findByText("Apiary")).toBeInTheDocument();
expect(await screen.findByText("Bienenstand")).toBeInTheDocument();
expect(screen.getByText("apiary-1")).toBeInTheDocument();
expect(screen.getByText("CREATE")).toBeInTheDocument();
expect(screen.getByText("Erstellt")).toBeInTheDocument();
});
it("shows an error message when loading the audit log fails", async () => {
@@ -4,6 +4,7 @@ import { useState } from "react";
import { History } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchAuditLog } from "@/lib/audit-log/api";
import { auditActionLabel, auditEntityTypeLabel } from "@/lib/audit-log/labels";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { PageHeader } from "@/components/page-header";
@@ -92,9 +93,9 @@ export default function AuditLogPage() {
{entries.map((entry) => (
<TableRow key={entry.id}>
<TableCell>{new Date(entry.timestamp).toLocaleString("de-DE")}</TableCell>
<TableCell>{entry.entityType}</TableCell>
<TableCell>{auditEntityTypeLabel(entry.entityType)}</TableCell>
<TableCell className="font-mono text-xs">{entry.entityId}</TableCell>
<TableCell>{entry.action}</TableCell>
<TableCell>{auditActionLabel(entry.action)}</TableCell>
<TableCell>{entry.userId ?? "—"}</TableCell>
</TableRow>
))}
@@ -0,0 +1,207 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateTaskTemplate } from "@/lib/task-templates/api";
import { TaskTemplate, TaskTemplateRecurrenceType } from "@/lib/task-templates/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = {
YEARLY: "Jährlich",
MONTHLY: "Monatlich",
INTERVAL_DAYS: "Alle X Tage",
};
const MONTH_LABELS = [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember",
];
export function EditTaskTemplateDialog({ template }: { template: TaskTemplate }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [title, setTitle] = useState(template.title);
const [description, setDescription] = useState(template.description ?? "");
const [recurrenceType, setRecurrenceType] = useState<TaskTemplateRecurrenceType>(
template.recurrenceType,
);
const [recurrenceMonth, setRecurrenceMonth] = useState(
template.recurrenceMonth?.toString() ?? "1",
);
const [recurrenceDayOfMonth, setRecurrenceDayOfMonth] = useState(
template.recurrenceDayOfMonth?.toString() ?? "1",
);
const [intervalDays, setIntervalDays] = useState(template.intervalDays?.toString() ?? "14");
const [error, setError] = useState<string | null>(null);
const mutation = useMutation({
mutationFn: () =>
updateTaskTemplate(template.id, {
title,
description: description || undefined,
recurrenceType,
recurrenceMonth: recurrenceType === "YEARLY" ? Number(recurrenceMonth) : undefined,
recurrenceDayOfMonth:
recurrenceType === "YEARLY" || recurrenceType === "MONTHLY"
? Number(recurrenceDayOfMonth)
: undefined,
intervalDays: recurrenceType === "INTERVAL_DAYS" ? Number(intervalDays) : undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["task-templates"] });
setOpen(false);
},
onError: () => setError("Vorlage konnte nicht gespeichert werden."),
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError(null);
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Vorlage bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="template-edit-title">Titel</Label>
<Input
id="template-edit-title"
required
value={title}
onChange={(event) => setTitle(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="template-edit-description">Beschreibung</Label>
<Textarea
id="template-edit-description"
value={description}
onChange={(event) => setDescription(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="template-edit-recurrence">Wiederholung</Label>
<Select
value={recurrenceType}
onValueChange={(value) =>
setRecurrenceType((value ?? recurrenceType) as TaskTemplateRecurrenceType)
}
>
<SelectTrigger id="template-edit-recurrence" className="w-full">
<SelectValue>{RECURRENCE_LABELS[recurrenceType]}</SelectValue>
</SelectTrigger>
<SelectContent>
{(Object.keys(RECURRENCE_LABELS) as TaskTemplateRecurrenceType[]).map(
(option) => (
<SelectItem key={option} value={option}>
{RECURRENCE_LABELS[option]}
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
{recurrenceType === "YEARLY" ? (
<div className="flex flex-col gap-2">
<Label htmlFor="template-edit-month">Monat</Label>
<Select
value={recurrenceMonth}
onValueChange={(value) => setRecurrenceMonth(value ?? recurrenceMonth)}
>
<SelectTrigger id="template-edit-month" className="w-full">
<SelectValue>{MONTH_LABELS[Number(recurrenceMonth) - 1]}</SelectValue>
</SelectTrigger>
<SelectContent>
{MONTH_LABELS.map((label, index) => (
<SelectItem key={label} value={String(index + 1)}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : null}
{recurrenceType === "YEARLY" || recurrenceType === "MONTHLY" ? (
<div className="flex flex-col gap-2">
<Label htmlFor="template-edit-day">Tag im Monat</Label>
<Input
id="template-edit-day"
type="number"
min={1}
max={31}
required
value={recurrenceDayOfMonth}
onChange={(event) => setRecurrenceDayOfMonth(event.target.value)}
/>
</div>
) : null}
{recurrenceType === "INTERVAL_DAYS" ? (
<div className="flex flex-col gap-2">
<Label htmlFor="template-edit-interval">Alle wie viele Tage</Label>
<Input
id="template-edit-interval"
type="number"
min={1}
required
value={intervalDays}
onChange={(event) => setIntervalDays(event.target.value)}
/>
</div>
) : null}
{error ? (
<p role="alert" className="text-sm text-destructive">
{error}
</p>
) : null}
<DialogFooter>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -13,6 +13,7 @@ import { EmptyState } from "@/components/empty-state";
import { EntityCard } from "@/components/entity-card";
import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs";
import { CreateTaskTemplateDialog } from "./create-task-template-dialog";
import { EditTaskTemplateDialog } from "./edit-task-template-dialog";
const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = {
YEARLY: "Jährlich",
@@ -109,15 +110,18 @@ export default function TaskTemplatesPage() {
{ label: "Nächste Fälligkeit", value: formatDate(template.nextDueDate) },
]}
actions={
<Button
type="button"
variant="ghost"
size="sm"
disabled={!template.active || deactivateMutation.isPending}
onClick={() => deactivateMutation.mutate(template.id)}
>
Deaktivieren
</Button>
<>
<EditTaskTemplateDialog template={template} />
<Button
type="button"
variant="ghost"
size="sm"
disabled={!template.active || deactivateMutation.isPending}
onClick={() => deactivateMutation.mutate(template.id)}
>
Deaktivieren
</Button>
</>
}
/>
))}
@@ -4,6 +4,7 @@ import { FormEvent, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createStockItem } from "@/lib/storage/api";
import { StockCategory } from "@/lib/storage/types";
import { STOCK_CATEGORIES, STOCK_CATEGORY_LABELS } from "@/lib/storage/labels";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -23,8 +24,6 @@ import {
SelectValue,
} from "@/components/ui/select";
const CATEGORY_OPTIONS: StockCategory[] = ["HONEY", "CONSUMABLE", "EQUIPMENT_MATERIAL"];
export function CreateStockItemDialog() {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
@@ -84,12 +83,12 @@ export function CreateStockItemDialog() {
onValueChange={(value) => setCategory((value ?? "CONSUMABLE") as StockCategory)}
>
<SelectTrigger id="item-category">
<SelectValue />
<SelectValue>{STOCK_CATEGORY_LABELS[category]}</SelectValue>
</SelectTrigger>
<SelectContent>
{CATEGORY_OPTIONS.map((option) => (
{STOCK_CATEGORIES.map((option) => (
<SelectItem key={option} value={option}>
{option}
{STOCK_CATEGORY_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -0,0 +1,136 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateStockItem } from "@/lib/storage/api";
import { StockCategory, StockItem } from "@/lib/storage/types";
import { STOCK_CATEGORIES, STOCK_CATEGORY_LABELS } from "@/lib/storage/labels";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export function EditStockItemDialog({ item }: { item: StockItem }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [name, setName] = useState(item.name);
const [category, setCategory] = useState<StockCategory>(item.category);
const [unit, setUnit] = useState(item.unit);
const [minThreshold, setMinThreshold] = useState(item.minThreshold?.toString() ?? "");
const [notes, setNotes] = useState(item.notes ?? "");
const mutation = useMutation({
mutationFn: () =>
updateStockItem(item.id, {
name,
category,
unit,
minThreshold: minThreshold ? Number(minThreshold) : undefined,
notes: notes || undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["stock-items"] });
setOpen(false);
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Lagerartikel bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="item-edit-name">Name</Label>
<Input
id="item-edit-name"
required
value={name}
onChange={(event) => setName(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="item-edit-category">Kategorie</Label>
<Select
value={category}
onValueChange={(value) => setCategory((value ?? category) as StockCategory)}
>
<SelectTrigger id="item-edit-category">
<SelectValue>{STOCK_CATEGORY_LABELS[category]}</SelectValue>
</SelectTrigger>
<SelectContent>
{STOCK_CATEGORIES.map((option) => (
<SelectItem key={option} value={option}>
{STOCK_CATEGORY_LABELS[option]}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="item-edit-unit">Einheit</Label>
<Input
id="item-edit-unit"
required
value={unit}
onChange={(event) => setUnit(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="item-edit-min-threshold">Mindestbestand</Label>
<Input
id="item-edit-min-threshold"
type="number"
inputMode="decimal"
value={minThreshold}
onChange={(event) => setMinThreshold(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="item-edit-notes">Notizen</Label>
<Textarea
id="item-edit-notes"
value={notes}
onChange={(event) => setNotes(event.target.value)}
/>
</div>
<DialogFooter>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,95 @@
"use client";
import { FormEvent, useState } from "react";
import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateStorageContainer } from "@/lib/storage/api";
import { StorageContainer } from "@/lib/storage/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function EditStorageContainerDialog({ container }: { container: StorageContainer }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
const [containerType, setContainerType] = useState(container.containerType ?? "");
const [capacity, setCapacity] = useState(container.capacity?.toString() ?? "");
const [location, setLocation] = useState(container.location ?? "");
const mutation = useMutation({
mutationFn: () =>
updateStorageContainer(container.id, {
containerType: containerType || undefined,
capacity: capacity ? Number(capacity) : undefined,
location: location || undefined,
}),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["storage-containers"] });
setOpen(false);
},
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
mutation.mutate();
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={
<Button variant="ghost" size="sm">
<Pencil className="size-3.5" />
Bearbeiten
</Button>
}
/>
<DialogContent>
<DialogHeader>
<DialogTitle>Lagerbehälter bearbeiten</DialogTitle>
</DialogHeader>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="container-edit-type">Typ</Label>
<Input
id="container-edit-type"
value={containerType}
onChange={(event) => setContainerType(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="container-edit-capacity">Kapazität (kg)</Label>
<Input
id="container-edit-capacity"
type="number"
inputMode="decimal"
value={capacity}
onChange={(event) => setCapacity(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="container-edit-location">Standort</Label>
<Input
id="container-edit-location"
value={location}
onChange={(event) => setLocation(event.target.value)}
/>
</div>
<DialogFooter>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "Speichert…" : "Speichern"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
@@ -4,6 +4,7 @@ import { useState } from "react";
import { Package, Warehouse } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { fetchStockItems, fetchStorageContainers } from "@/lib/storage/api";
import { STOCK_CATEGORY_LABELS, STORAGE_CONTAINER_STATUS_LABELS } from "@/lib/storage/labels";
import { QrCodeDialog } from "@/components/qr-code-dialog";
import { PageHeader } from "@/components/page-header";
import { EmptyState } from "@/components/empty-state";
@@ -12,6 +13,8 @@ import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-
import { Badge } from "@/components/ui/badge";
import { CreateStorageContainerDialog } from "./create-storage-container-dialog";
import { CreateStockItemDialog } from "./create-stock-item-dialog";
import { EditStorageContainerDialog } from "./edit-storage-container-dialog";
import { EditStockItemDialog } from "./edit-stock-item-dialog";
import { RecordMovementDialog } from "./record-movement-dialog";
export default function StoragePage() {
@@ -76,12 +79,15 @@ export default function StoragePage() {
subtitle={container.containerType ?? "—"}
badge={
<Badge variant={container.status === "FILLED" ? "default" : "secondary"}>
{container.status}
{STORAGE_CONTAINER_STATUS_LABELS[container.status]}
</Badge>
}
fields={[{ label: "Standort", value: container.location ?? "—" }]}
actions={
<QrCodeDialog entityType="storage-containers" id={container.id} label="QR" />
<>
<EditStorageContainerDialog container={container} />
<QrCodeDialog entityType="storage-containers" id={container.id} label="QR" />
</>
}
/>
))}
@@ -111,14 +117,19 @@ export default function StoragePage() {
icon={Package}
gradient="var(--gradient-sky)"
title={item.name}
subtitle={item.category}
subtitle={STOCK_CATEGORY_LABELS[item.category]}
badge={
item.minThreshold !== null && item.quantity < item.minThreshold ? (
<Badge variant="destructive">Niedrig</Badge>
) : undefined
}
fields={[{ label: "Bestand", value: `${item.quantity} ${item.unit}` }]}
actions={<RecordMovementDialog stockItemId={item.id} />}
actions={
<>
<EditStockItemDialog item={item} />
<RecordMovementDialog stockItemId={item.id} />
</>
}
/>
))}
</div>
@@ -4,6 +4,7 @@ import { FormEvent, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { addStockMovement } from "@/lib/storage/api";
import { StockMovementType } from "@/lib/storage/types";
import { STOCK_MOVEMENT_TYPES, STOCK_MOVEMENT_TYPE_LABELS } from "@/lib/storage/labels";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -23,8 +24,6 @@ import {
SelectValue,
} from "@/components/ui/select";
const TYPE_OPTIONS: StockMovementType[] = ["IN", "OUT", "TRANSFER", "ADJUSTMENT"];
export function RecordMovementDialog({ stockItemId }: { stockItemId: string }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
@@ -71,12 +70,12 @@ export function RecordMovementDialog({ stockItemId }: { stockItemId: string }) {
onValueChange={(value) => setType((value ?? "IN") as StockMovementType)}
>
<SelectTrigger id="movement-type">
<SelectValue />
<SelectValue>{STOCK_MOVEMENT_TYPE_LABELS[type]}</SelectValue>
</SelectTrigger>
<SelectContent>
{TYPE_OPTIONS.map((option) => (
{STOCK_MOVEMENT_TYPES.map((option) => (
<SelectItem key={option} value={option}>
{option}
{STOCK_MOVEMENT_TYPE_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -7,6 +7,7 @@ import { toast } from "sonner";
import { ListTodo } from "lucide-react";
import { createTask } from "@/lib/tasks/api";
import { TaskPriority } from "@/lib/tasks/types";
import { TASK_PRIORITIES, TASK_PRIORITY_LABELS } from "@/lib/tasks/labels";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -21,8 +22,6 @@ import {
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
const PRIORITY_OPTIONS: TaskPriority[] = ["LOW", "NORMAL", "HIGH", "URGENT"];
export default function NewTaskPage() {
const router = useRouter();
const queryClient = useQueryClient();
@@ -82,12 +81,12 @@ export default function NewTaskPage() {
onValueChange={(value) => setPriority((value ?? "NORMAL") as TaskPriority)}
>
<SelectTrigger id="task-priority">
<SelectValue />
<SelectValue>{TASK_PRIORITY_LABELS[priority]}</SelectValue>
</SelectTrigger>
<SelectContent>
{PRIORITY_OPTIONS.map((option) => (
{TASK_PRIORITIES.map((option) => (
<SelectItem key={option} value={option}>
{option}
{TASK_PRIORITY_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -6,6 +6,7 @@ import { ListTodo } from "lucide-react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { completeTask, fetchTasks } from "@/lib/tasks/api";
import { TaskPriority, TaskStatus } from "@/lib/tasks/types";
import { TASK_PRIORITIES, TASK_PRIORITY_LABELS, TASK_STATUS_LABELS } from "@/lib/tasks/labels";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { PageHeader } from "@/components/page-header";
@@ -21,7 +22,6 @@ import {
} from "@/components/ui/select";
import { TaskEditDialog } from "./task-edit-dialog";
const PRIORITY_OPTIONS: TaskPriority[] = ["LOW", "NORMAL", "HIGH", "URGENT"];
const HISTORY_STATUSES: TaskStatus[] = ["DONE", "CANCELLED"];
export default function TasksPage() {
@@ -79,13 +79,15 @@ export default function TasksPage() {
}
>
<SelectTrigger className="w-40">
<SelectValue placeholder="Priorität">{priority || "Alle Prioritäten"}</SelectValue>
<SelectValue placeholder="Priorität">
{priority ? TASK_PRIORITY_LABELS[priority] : "Alle Prioritäten"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL">Alle Prioritäten</SelectItem>
{PRIORITY_OPTIONS.map((option) => (
{TASK_PRIORITIES.map((option) => (
<SelectItem key={option} value={option}>
{option}
{TASK_PRIORITY_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -133,11 +135,11 @@ export default function TasksPage() {
title={task.title}
badge={
<Badge variant={task.status === "DONE" ? "secondary" : "default"}>
{task.status}
{TASK_STATUS_LABELS[task.status]}
</Badge>
}
fields={[
{ label: "Priorität", value: task.priority },
{ label: "Priorität", value: TASK_PRIORITY_LABELS[task.priority] },
{
label: "Fällig",
value: task.dueDate ? new Date(task.dueDate).toLocaleDateString("de-DE") : "—",
@@ -5,6 +5,7 @@ import { Pencil } from "lucide-react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateTask } from "@/lib/tasks/api";
import { Task, TaskPriority } from "@/lib/tasks/types";
import { TASK_PRIORITIES, TASK_PRIORITY_LABELS } from "@/lib/tasks/labels";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -24,8 +25,6 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
const PRIORITY_OPTIONS: TaskPriority[] = ["LOW", "NORMAL", "HIGH", "URGENT"];
export function TaskEditDialog({ task }: { task: Task }) {
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
@@ -84,12 +83,12 @@ export function TaskEditDialog({ task }: { task: Task }) {
onValueChange={(value) => setPriority((value ?? priority) as TaskPriority)}
>
<SelectTrigger id="task-edit-priority" className="w-full">
<SelectValue />
<SelectValue>{TASK_PRIORITY_LABELS[priority]}</SelectValue>
</SelectTrigger>
<SelectContent>
{PRIORITY_OPTIONS.map((option) => (
{TASK_PRIORITIES.map((option) => (
<SelectItem key={option} value={option}>
{option}
{TASK_PRIORITY_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -8,6 +8,7 @@ import { Bug } from "lucide-react";
import { fetchColonies } from "@/lib/colonies/api";
import { createVarroaMeasurement } from "@/lib/varroa/api";
import { VarroaMethod } from "@/lib/varroa/types";
import { VARROA_METHODS, VARROA_METHOD_LABELS } from "@/lib/varroa/labels";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -22,13 +23,6 @@ import {
import { PageHeader } from "@/components/page-header";
import { Card, CardContent } from "@/components/ui/card";
const METHOD_OPTIONS: VarroaMethod[] = [
"NATURAL_MITE_FALL",
"POWDERED_SUGAR",
"WASHING_METHOD",
"DRONE_BROOD_CONTROL",
];
function todayIsoDate(): string {
return new Date().toISOString().slice(0, 10);
}
@@ -129,12 +123,12 @@ export default function NewVarroaMeasurementPage() {
onValueChange={(value) => setMethod((value ?? "NATURAL_MITE_FALL") as VarroaMethod)}
>
<SelectTrigger id="varroa-method" className="h-11 w-full">
<SelectValue />
<SelectValue>{VARROA_METHOD_LABELS[method]}</SelectValue>
</SelectTrigger>
<SelectContent>
{METHOD_OPTIONS.map((option) => (
{VARROA_METHODS.map((option) => (
<SelectItem key={option} value={option}>
{option}
{VARROA_METHOD_LABELS[option]}
</SelectItem>
))}
</SelectContent>
@@ -0,0 +1,53 @@
const AUDIT_ACTION_LABELS: Record<string, string> = {
CREATE: "Erstellt",
UPDATE: "Aktualisiert",
DEACTIVATE: "Deaktiviert",
DELETE: "Gelöscht",
LOGIN: "Angemeldet",
LOGIN_FAILED: "Anmeldung fehlgeschlagen",
LOGOUT: "Abgemeldet",
TOKEN_REFRESHED: "Token erneuert",
MFA_ENABLED: "MFA aktiviert",
MFA_DISABLED: "MFA deaktiviert",
};
export function auditActionLabel(action: string): string {
return AUDIT_ACTION_LABELS[action] ?? action;
}
const AUDIT_ENTITY_TYPE_LABELS: Record<string, string> = {
Apiary: "Bienenstand",
AuthSession: "Sitzung",
AutomationRule: "Automatisierungsregel",
BottlingBatch: "Abfüllung",
Colony: "Volk",
COLONY: "Volk",
Document: "Dokument",
Equipment: "Ausrüstung",
Extraction: "Schleuderung",
Feeding: "Fütterung",
Harvest: "Ernte",
HoneyBatch: "Honigcharge",
HoneyBatchRelocation: "Honigcharge-Umlagerung",
HoneyMeasurement: "Honigmessung",
Import: "Import",
Inspection: "Durchsicht",
Product: "Produkt",
Queen: "Königin",
QueenEvaluation: "Königinnenbewertung",
Role: "Rolle",
StockItem: "Lagerartikel",
StockMovement: "Bestandsbewegung",
StorageContainer: "Lagerbehälter",
SwarmControlCheck: "Schwarmkontrolle",
Task: "Aufgabe",
TaskTemplate: "Aufgabenvorlage",
Treatment: "Behandlung",
User: "Benutzer",
VarroaMeasurement: "Varroamessung",
WebhookSubscription: "Webhook",
};
export function auditEntityTypeLabel(entityType: string): string {
return AUDIT_ENTITY_TYPE_LABELS[entityType] ?? entityType;
}
@@ -0,0 +1,10 @@
import { EquipmentCondition } from "./types";
export const EQUIPMENT_CONDITIONS: EquipmentCondition[] = ["NEW", "GOOD", "WORN", "DEFECTIVE"];
export const EQUIPMENT_CONDITION_LABELS: Record<EquipmentCondition, string> = {
NEW: "Neu",
GOOD: "Gut",
WORN: "Abgenutzt",
DEFECTIVE: "Defekt",
};
@@ -0,0 +1,17 @@
import { FeedType } from "./types";
export const FEED_TYPES: FeedType[] = [
"SUGAR_WATER",
"FEED_SYRUP",
"FEED_PASTE",
"OWN_HONEY",
"OTHER",
];
export const FEED_TYPE_LABELS: Record<FeedType, string> = {
SUGAR_WATER: "Zuckerwasser",
FEED_SYRUP: "Futtersirup",
FEED_PASTE: "Futterteig",
OWN_HONEY: "Eigener Honig",
OTHER: "Sonstiges",
};
@@ -7,6 +7,7 @@ import {
HoneyBatchStatus,
RelocateHoneyBatchInput,
Traceability,
UpdateHoneyBatchInput,
} from "./types";
export interface HoneyBatchFilter {
@@ -35,6 +36,16 @@ export function createHoneyBatch(input: CreateHoneyBatchInput): Promise<HoneyBat
});
}
export function updateHoneyBatch(
id: string,
input: UpdateHoneyBatchInput,
): Promise<HoneyBatch> {
return apiFetch<HoneyBatch>(`/honey-batches/${id}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export function addHoneyMeasurement(
id: string,
input: CreateHoneyMeasurementInput,
@@ -0,0 +1,12 @@
import { HoneyBatchStatus } from "./types";
export const HONEY_BATCH_STATUS_LABELS: Record<HoneyBatchStatus, string> = {
RAW: "Roh",
EXTRACTED: "Geschleudert",
CLARIFIED: "Geklärt",
CREAMED: "Cremehonig",
STORED: "Gelagert",
BOTTLED: "Abgefüllt",
SOLD: "Verkauft",
CONSUMED: "Verbraucht",
};
@@ -81,6 +81,12 @@ export interface CreateHoneyBatchInput {
waterContent?: number;
}
export interface UpdateHoneyBatchInput {
honeyType?: string;
amount?: number;
waterContent?: number;
}
export interface CreateHoneyMeasurementInput {
waterContent: number;
measuredAt: string;
@@ -0,0 +1,6 @@
import { ProductStatus } from "./types";
export const PRODUCT_STATUS_LABELS: Record<ProductStatus, string> = {
ACTIVE: "Aktiv",
DISCONTINUED: "Eingestellt",
};
+24
View File
@@ -0,0 +1,24 @@
import { QueenStatus } from "./types";
// Werte und Reihenfolge aus docs/lastenheft.md Abschnitt 4.5 ("Moegliche Status").
export const QUEEN_STATUSES: QueenStatus[] = [
"ACTIVE",
"RESERVE",
"UNMATED",
"MATED",
"LOST",
"REPLACED",
"SOLD",
"DECEASED",
];
export const QUEEN_STATUS_LABELS: Record<QueenStatus, string> = {
ACTIVE: "Aktiv",
RESERVE: "Reserve",
UNMATED: "Unbegattet",
MATED: "Begattet",
LOST: "Verloren",
REPLACED: "Ersetzt",
SOLD: "Verkauft",
DECEASED: "Verstorben",
};
+22
View File
@@ -5,6 +5,8 @@ import {
CreateStorageContainerInput,
StockItem,
StorageContainer,
UpdateStockItemInput,
UpdateStorageContainerInput,
} from "./types";
export function fetchStorageContainers(): Promise<StorageContainer[]> {
@@ -20,6 +22,16 @@ export function createStorageContainer(
});
}
export function updateStorageContainer(
id: string,
input: UpdateStorageContainerInput,
): Promise<StorageContainer> {
return apiFetch<StorageContainer>(`/storage-containers/${id}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export function fetchStockItems(): Promise<StockItem[]> {
return apiFetch<StockItem[]>("/stock-items");
}
@@ -31,6 +43,16 @@ export function createStockItem(input: CreateStockItemInput): Promise<StockItem>
});
}
export function updateStockItem(
id: string,
input: UpdateStockItemInput,
): Promise<StockItem> {
return apiFetch<StockItem>(`/stock-items/${id}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export function addStockMovement(
id: string,
input: CreateStockMovementInput,
@@ -0,0 +1,25 @@
import { StockCategory, StockMovementType, StorageContainerStatus } from "./types";
export const STORAGE_CONTAINER_STATUS_LABELS: Record<StorageContainerStatus, string> = {
EMPTY: "Leer",
FILLED: "Gefüllt",
CLEANING: "Reinigung",
OUT_OF_SERVICE: "Außer Betrieb",
};
export const STOCK_CATEGORIES: StockCategory[] = ["HONEY", "CONSUMABLE", "EQUIPMENT_MATERIAL"];
export const STOCK_CATEGORY_LABELS: Record<StockCategory, string> = {
HONEY: "Honig",
CONSUMABLE: "Verbrauchsmaterial",
EQUIPMENT_MATERIAL: "Ausrüstungsmaterial",
};
export const STOCK_MOVEMENT_TYPES: StockMovementType[] = ["IN", "OUT", "TRANSFER", "ADJUSTMENT"];
export const STOCK_MOVEMENT_TYPE_LABELS: Record<StockMovementType, string> = {
IN: "Eingang",
OUT: "Ausgang",
TRANSFER: "Umlagerung",
ADJUSTMENT: "Korrektur",
};
+14
View File
@@ -41,6 +41,12 @@ export interface CreateStorageContainerInput {
location?: string;
}
export interface UpdateStorageContainerInput {
containerType?: string;
capacity?: number;
location?: string;
}
export interface CreateStockItemInput {
name: string;
category: StockCategory;
@@ -50,6 +56,14 @@ export interface CreateStockItemInput {
notes?: string;
}
export interface UpdateStockItemInput {
name?: string;
category?: StockCategory;
unit?: string;
minThreshold?: number;
notes?: string;
}
export interface CreateStockMovementInput {
type: StockMovementType;
quantity: number;
+19
View File
@@ -0,0 +1,19 @@
import { TaskPriority, TaskStatus } from "./types";
export const TASK_STATUSES: TaskStatus[] = ["OPEN", "IN_PROGRESS", "DONE", "CANCELLED"];
export const TASK_STATUS_LABELS: Record<TaskStatus, string> = {
OPEN: "Offen",
IN_PROGRESS: "In Bearbeitung",
DONE: "Erledigt",
CANCELLED: "Abgebrochen",
};
export const TASK_PRIORITIES: TaskPriority[] = ["LOW", "NORMAL", "HIGH", "URGENT"];
export const TASK_PRIORITY_LABELS: Record<TaskPriority, string> = {
LOW: "Niedrig",
NORMAL: "Normal",
HIGH: "Hoch",
URGENT: "Dringend",
};
+15
View File
@@ -0,0 +1,15 @@
import { VarroaMethod } from "./types";
export const VARROA_METHODS: VarroaMethod[] = [
"NATURAL_MITE_FALL",
"POWDERED_SUGAR",
"WASHING_METHOD",
"DRONE_BROOD_CONTROL",
];
export const VARROA_METHOD_LABELS: Record<VarroaMethod, string> = {
NATURAL_MITE_FALL: "Natürlicher Milbenfall",
POWDERED_SUGAR: "Puderzuckermethode",
WASHING_METHOD: "Waschmethode",
DRONE_BROOD_CONTROL: "Drohnenbrutentnahme",
};