fix: Bug fixed für alle Seiten und das Profil und label
This commit is contained in:
1 parent
133edcd5ba
commit
0a52b29c57
52 files changed
+1280
-119
No files matched your search
@@ -157,6 +157,7 @@ export const PERMISSION_CATALOG = [
|
||||
[
|
||||
['read', 'Ansehen'],
|
||||
['create', 'Anlegen'],
|
||||
['update', 'Bearbeiten'],
|
||||
['relocate', 'Umlagern'],
|
||||
['measure', 'Messwerte erfassen'],
|
||||
],
|
||||
@@ -168,6 +169,7 @@ export const PERMISSION_CATALOG = [
|
||||
[
|
||||
['read', 'Ansehen'],
|
||||
['create', 'Anlegen'],
|
||||
['update', 'Bearbeiten'],
|
||||
['move', 'Bewegung buchen'],
|
||||
],
|
||||
1400,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PartialType, PickType } from '@nestjs/swagger';
|
||||
import { CreateHoneyBatchDto } from './create-honey-batch.dto';
|
||||
|
||||
export class UpdateHoneyBatchDto extends PartialType(
|
||||
PickType(CreateHoneyBatchDto, [
|
||||
'honeyType',
|
||||
'amount',
|
||||
'waterContent',
|
||||
] as const),
|
||||
) {}
|
||||
@@ -1,10 +1,19 @@
|
||||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { HoneyBatchStatus } from '../../generated/prisma';
|
||||
import { RequirePermission } from '../auth/decorators/require-permission.decorator';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||
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 { HoneyBatchesService } from './honey-batches.service';
|
||||
@@ -58,6 +67,16 @@ export class HoneyBatchesController {
|
||||
return this.honeyBatchesService.create(dto, actor.id);
|
||||
}
|
||||
|
||||
@RequirePermission('honey-batches:update')
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateHoneyBatchDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
) {
|
||||
return this.honeyBatchesService.update(id, dto, actor.id);
|
||||
}
|
||||
|
||||
@RequirePermission('honey-batches:measure')
|
||||
@Post(':id/measurements')
|
||||
addMeasurement(
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||
import { HoneyBatchStatus } from '../../generated/prisma';
|
||||
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';
|
||||
|
||||
@@ -68,6 +69,24 @@ export class HoneyBatchesService {
|
||||
return created;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateHoneyBatchDto, actingUserId: string) {
|
||||
const before = await this.findById(id);
|
||||
await this.prisma.honeyBatch.update({
|
||||
where: { id },
|
||||
data: { ...dto },
|
||||
});
|
||||
const updated = await this.findById(id);
|
||||
await this.auditLog.record({
|
||||
entityType: 'HoneyBatch',
|
||||
entityId: id,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: updated,
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
async addMeasurement(
|
||||
id: string,
|
||||
dto: CreateHoneyMeasurementDto,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { PartialType, PickType } from '@nestjs/swagger';
|
||||
import { CreateStockItemDto } from './create-stock-item.dto';
|
||||
|
||||
export class UpdateStockItemDto extends PartialType(
|
||||
PickType(CreateStockItemDto, [
|
||||
'name',
|
||||
'category',
|
||||
'unit',
|
||||
'minThreshold',
|
||||
'notes',
|
||||
] as const),
|
||||
) {}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PartialType, PickType } from '@nestjs/swagger';
|
||||
import { CreateStorageContainerDto } from './create-storage-container.dto';
|
||||
|
||||
export class UpdateStorageContainerDto extends PartialType(
|
||||
PickType(CreateStorageContainerDto, [
|
||||
'containerType',
|
||||
'capacity',
|
||||
'location',
|
||||
] as const),
|
||||
) {}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { RequirePermission } from '../auth/decorators/require-permission.decorator';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||
import { CreateStorageContainerDto } from './dto/create-storage-container.dto';
|
||||
import { UpdateStorageContainerDto } from './dto/update-storage-container.dto';
|
||||
import { CreateStockItemDto } from './dto/create-stock-item.dto';
|
||||
import { UpdateStockItemDto } from './dto/update-stock-item.dto';
|
||||
import { CreateStockMovementDto } from './dto/create-stock-movement.dto';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
@@ -29,6 +31,16 @@ export class StorageController {
|
||||
return this.storageService.createContainer(dto, actor.id);
|
||||
}
|
||||
|
||||
@RequirePermission('storage:update')
|
||||
@Patch('storage-containers/:id')
|
||||
updateContainer(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateStorageContainerDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
) {
|
||||
return this.storageService.updateContainer(id, dto, actor.id);
|
||||
}
|
||||
|
||||
@RequirePermission('storage:read')
|
||||
@Get('stock-items')
|
||||
findAllStockItems() {
|
||||
@@ -50,6 +62,16 @@ export class StorageController {
|
||||
return this.storageService.createStockItem(dto, actor.id);
|
||||
}
|
||||
|
||||
@RequirePermission('storage:update')
|
||||
@Patch('stock-items/:id')
|
||||
updateStockItem(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateStockItemDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
) {
|
||||
return this.storageService.updateStockItem(id, dto, actor.id);
|
||||
}
|
||||
|
||||
@RequirePermission('storage:move')
|
||||
@Post('stock-items/:id/movements')
|
||||
addMovement(
|
||||
|
||||
@@ -4,7 +4,9 @@ import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||
import { StockItem, StorageContainer } from '../../generated/prisma';
|
||||
import { CreateStorageContainerDto } from './dto/create-storage-container.dto';
|
||||
import { UpdateStorageContainerDto } from './dto/update-storage-container.dto';
|
||||
import { CreateStockItemDto } from './dto/create-stock-item.dto';
|
||||
import { UpdateStockItemDto } from './dto/update-stock-item.dto';
|
||||
import { CreateStockMovementDto } from './dto/create-stock-movement.dto';
|
||||
|
||||
@Injectable()
|
||||
@@ -36,6 +38,32 @@ export class StorageService {
|
||||
return container;
|
||||
}
|
||||
|
||||
async updateContainer(
|
||||
id: string,
|
||||
dto: UpdateStorageContainerDto,
|
||||
actingUserId: string,
|
||||
): Promise<StorageContainer> {
|
||||
const before = await this.prisma.storageContainer.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
if (!before) {
|
||||
throw new NotFoundException('Storage container not found');
|
||||
}
|
||||
const container = await this.prisma.storageContainer.update({
|
||||
where: { id },
|
||||
data: { ...dto },
|
||||
});
|
||||
await this.auditLog.record({
|
||||
entityType: 'StorageContainer',
|
||||
entityId: id,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: container,
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
async findAllStockItems(): Promise<StockItem[]> {
|
||||
return this.prisma.stockItem.findMany({ orderBy: { name: 'asc' } });
|
||||
}
|
||||
@@ -66,6 +94,27 @@ export class StorageService {
|
||||
return stockItem;
|
||||
}
|
||||
|
||||
async updateStockItem(
|
||||
id: string,
|
||||
dto: UpdateStockItemDto,
|
||||
actingUserId: string,
|
||||
): Promise<StockItem> {
|
||||
const before = await this.findStockItemById(id);
|
||||
const stockItem = await this.prisma.stockItem.update({
|
||||
where: { id },
|
||||
data: { ...dto },
|
||||
});
|
||||
await this.auditLog.record({
|
||||
entityType: 'StockItem',
|
||||
entityId: id,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: stockItem,
|
||||
});
|
||||
return stockItem;
|
||||
}
|
||||
|
||||
async addMovement(
|
||||
id: string,
|
||||
dto: CreateStockMovementDto,
|
||||
|
||||
@@ -2,6 +2,19 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
// Browsers are supposed to bypass HTTP cache when checking for a new
|
||||
// service worker, but that's not reliably honored everywhere - an
|
||||
// explicit no-cache header here is the only way to guarantee a sw.js
|
||||
// fix actually reaches already-open tabs promptly after a deploy,
|
||||
// instead of possibly sitting cached for hours.
|
||||
source: "/sw.js",
|
||||
headers: [{ key: "Cache-Control", value: "no-cache, no-store, must-revalidate" }],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -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>
|
||||
|
||||
@@ -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,6 +110,8 @@ export default function TaskTemplatesPage() {
|
||||
{ label: "Nächste Fälligkeit", value: formatDate(template.nextDueDate) },
|
||||
]}
|
||||
actions={
|
||||
<>
|
||||
<EditTaskTemplateDialog template={template} />
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -118,6 +121,7 @@ export default function TaskTemplatesPage() {
|
||||
>
|
||||
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={
|
||||
<>
|
||||
<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",
|
||||
};
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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",
|
||||
};
|
||||
Reference in new issue
Block a user