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'],
|
['read', 'Ansehen'],
|
||||||
['create', 'Anlegen'],
|
['create', 'Anlegen'],
|
||||||
|
['update', 'Bearbeiten'],
|
||||||
['relocate', 'Umlagern'],
|
['relocate', 'Umlagern'],
|
||||||
['measure', 'Messwerte erfassen'],
|
['measure', 'Messwerte erfassen'],
|
||||||
],
|
],
|
||||||
@@ -168,6 +169,7 @@ export const PERMISSION_CATALOG = [
|
|||||||
[
|
[
|
||||||
['read', 'Ansehen'],
|
['read', 'Ansehen'],
|
||||||
['create', 'Anlegen'],
|
['create', 'Anlegen'],
|
||||||
|
['update', 'Bearbeiten'],
|
||||||
['move', 'Bewegung buchen'],
|
['move', 'Bewegung buchen'],
|
||||||
],
|
],
|
||||||
1400,
|
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 { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { HoneyBatchStatus } from '../../generated/prisma';
|
import { HoneyBatchStatus } from '../../generated/prisma';
|
||||||
import { RequirePermission } from '../auth/decorators/require-permission.decorator';
|
import { RequirePermission } from '../auth/decorators/require-permission.decorator';
|
||||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||||
import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto';
|
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 { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
|
||||||
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto';
|
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto';
|
||||||
import { HoneyBatchesService } from './honey-batches.service';
|
import { HoneyBatchesService } from './honey-batches.service';
|
||||||
@@ -58,6 +67,16 @@ export class HoneyBatchesController {
|
|||||||
return this.honeyBatchesService.create(dto, actor.id);
|
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')
|
@RequirePermission('honey-batches:measure')
|
||||||
@Post(':id/measurements')
|
@Post(':id/measurements')
|
||||||
addMeasurement(
|
addMeasurement(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AuditLogService } from '../audit/audit-log.service';
|
|||||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||||
import { HoneyBatchStatus } from '../../generated/prisma';
|
import { HoneyBatchStatus } from '../../generated/prisma';
|
||||||
import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto';
|
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 { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
|
||||||
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto';
|
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto';
|
||||||
|
|
||||||
@@ -68,6 +69,24 @@ export class HoneyBatchesService {
|
|||||||
return created;
|
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(
|
async addMeasurement(
|
||||||
id: string,
|
id: string,
|
||||||
dto: CreateHoneyMeasurementDto,
|
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 { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||||
import { RequirePermission } from '../auth/decorators/require-permission.decorator';
|
import { RequirePermission } from '../auth/decorators/require-permission.decorator';
|
||||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||||
import { CreateStorageContainerDto } from './dto/create-storage-container.dto';
|
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 { CreateStockItemDto } from './dto/create-stock-item.dto';
|
||||||
|
import { UpdateStockItemDto } from './dto/update-stock-item.dto';
|
||||||
import { CreateStockMovementDto } from './dto/create-stock-movement.dto';
|
import { CreateStockMovementDto } from './dto/create-stock-movement.dto';
|
||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
@@ -29,6 +31,16 @@ export class StorageController {
|
|||||||
return this.storageService.createContainer(dto, actor.id);
|
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')
|
@RequirePermission('storage:read')
|
||||||
@Get('stock-items')
|
@Get('stock-items')
|
||||||
findAllStockItems() {
|
findAllStockItems() {
|
||||||
@@ -50,6 +62,16 @@ export class StorageController {
|
|||||||
return this.storageService.createStockItem(dto, actor.id);
|
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')
|
@RequirePermission('storage:move')
|
||||||
@Post('stock-items/:id/movements')
|
@Post('stock-items/:id/movements')
|
||||||
addMovement(
|
addMovement(
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { AuditLogService } from '../audit/audit-log.service';
|
|||||||
import { WebhooksService } from '../webhooks/webhooks.service';
|
import { WebhooksService } from '../webhooks/webhooks.service';
|
||||||
import { StockItem, StorageContainer } from '../../generated/prisma';
|
import { StockItem, StorageContainer } from '../../generated/prisma';
|
||||||
import { CreateStorageContainerDto } from './dto/create-storage-container.dto';
|
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 { CreateStockItemDto } from './dto/create-stock-item.dto';
|
||||||
|
import { UpdateStockItemDto } from './dto/update-stock-item.dto';
|
||||||
import { CreateStockMovementDto } from './dto/create-stock-movement.dto';
|
import { CreateStockMovementDto } from './dto/create-stock-movement.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -36,6 +38,32 @@ export class StorageService {
|
|||||||
return container;
|
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[]> {
|
async findAllStockItems(): Promise<StockItem[]> {
|
||||||
return this.prisma.stockItem.findMany({ orderBy: { name: 'asc' } });
|
return this.prisma.stockItem.findMany({ orderBy: { name: 'asc' } });
|
||||||
}
|
}
|
||||||
@@ -66,6 +94,27 @@ export class StorageService {
|
|||||||
return stockItem;
|
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(
|
async addMovement(
|
||||||
id: string,
|
id: string,
|
||||||
dto: CreateStockMovementDto,
|
dto: CreateStockMovementDto,
|
||||||
|
|||||||
@@ -2,6 +2,19 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
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;
|
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 { COLONY_TYPES, COLONY_TYPE_LABELS } from "@/lib/colonies/labels";
|
||||||
import { fetchApiaries } from "@/lib/apiaries/api";
|
import { fetchApiaries } from "@/lib/apiaries/api";
|
||||||
import { fetchColonies } from "@/lib/colonies/api";
|
import { fetchColonies } from "@/lib/colonies/api";
|
||||||
|
import { assignQueenColony, fetchQueens } from "@/lib/queens/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
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 }) {
|
function MergeDialog({ colony }: { colony: Colony }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -298,6 +363,7 @@ export function ColonyActions({ colony }: { colony: Colony }) {
|
|||||||
{colony.status === "ACTIVE" ? (
|
{colony.status === "ACTIVE" ? (
|
||||||
<>
|
<>
|
||||||
<MoveDialog colony={colony} />
|
<MoveDialog colony={colony} />
|
||||||
|
<AssignQueenDialog colony={colony} />
|
||||||
<SplitDialog colony={colony} />
|
<SplitDialog colony={colony} />
|
||||||
<MergeDialog colony={colony} />
|
<MergeDialog colony={colony} />
|
||||||
<DissolveButton colony={colony} />
|
<DissolveButton colony={colony} />
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { statusTone } from "@/lib/status-tone";
|
import { statusTone } from "@/lib/status-tone";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import {
|
import {
|
||||||
@@ -74,6 +75,8 @@ function ConditionSelect({
|
|||||||
export function ColonyEditCard({ colony }: { colony: Colony }) {
|
export function ColonyEditCard({ colony }: { colony: Colony }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [colonyType, setColonyType] = useState<ColonyType>(colony.colonyType);
|
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 [strength, setStrength] = useState(colony.strength?.toString() ?? "");
|
||||||
const [broodStatus, setBroodStatus] = useState<ColonyCondition | undefined>(
|
const [broodStatus, setBroodStatus] = useState<ColonyCondition | undefined>(
|
||||||
colony.broodStatus ?? undefined,
|
colony.broodStatus ?? undefined,
|
||||||
@@ -90,6 +93,8 @@ export function ColonyEditCard({ colony }: { colony: Colony }) {
|
|||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
updateColony(colony.id, {
|
updateColony(colony.id, {
|
||||||
colonyType,
|
colonyType,
|
||||||
|
hiveType: hiveType || undefined,
|
||||||
|
originText: originText || undefined,
|
||||||
strength: strength ? Number(strength) : undefined,
|
strength: strength ? Number(strength) : undefined,
|
||||||
broodStatus,
|
broodStatus,
|
||||||
feedStatus,
|
feedStatus,
|
||||||
@@ -141,6 +146,22 @@ export function ColonyEditCard({ colony }: { colony: Colony }) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="colony-strength">Volksstärke</Label>
|
<Label htmlFor="colony-strength">Volksstärke</Label>
|
||||||
<Select value={strength} onValueChange={(value) => setStrength(value ?? "")}>
|
<Select value={strength} onValueChange={(value) => setStrength(value ?? "")}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import { Utensils } from "lucide-react";
|
import { Utensils } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchColonyFeedings, fetchColonyFeedingsSummary } from "@/lib/feedings/api";
|
import { fetchColonyFeedings, fetchColonyFeedingsSummary } from "@/lib/feedings/api";
|
||||||
|
import { FEED_TYPE_LABELS } from "@/lib/feedings/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
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 key={feeding.id} className="rounded-md border p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="font-medium">
|
<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>
|
</p>
|
||||||
<FeedingEditDialog feeding={feeding} />
|
<FeedingEditDialog feeding={feeding} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import { Bug } from "lucide-react";
|
import { Bug } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchColonyVarroaMeasurements } from "@/lib/varroa/api";
|
import { fetchColonyVarroaMeasurements } from "@/lib/varroa/api";
|
||||||
|
import { VARROA_METHOD_LABELS } from "@/lib/varroa/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
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 key={measurement.id} className="rounded-md border p-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="font-medium">
|
<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>
|
</p>
|
||||||
<VarroaEditDialog measurement={measurement} />
|
<VarroaEditDialog measurement={measurement} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Pencil } from "lucide-react";
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { updateFeeding } from "@/lib/feedings/api";
|
import { updateFeeding } from "@/lib/feedings/api";
|
||||||
import { Feeding, FeedType } from "@/lib/feedings/types";
|
import { Feeding, FeedType } from "@/lib/feedings/types";
|
||||||
|
import { FEED_TYPES, FEED_TYPE_LABELS } from "@/lib/feedings/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -24,14 +25,6 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
const FEED_TYPE_OPTIONS: FeedType[] = [
|
|
||||||
"SUGAR_WATER",
|
|
||||||
"FEED_SYRUP",
|
|
||||||
"FEED_PASTE",
|
|
||||||
"OWN_HONEY",
|
|
||||||
"OTHER",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
|
export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -96,12 +89,12 @@ export function FeedingEditDialog({ feeding }: { feeding: Feeding }) {
|
|||||||
onValueChange={(value) => setFeedType((value ?? feedType) as FeedType)}
|
onValueChange={(value) => setFeedType((value ?? feedType) as FeedType)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="feeding-edit-type" className="w-full">
|
<SelectTrigger id="feeding-edit-type" className="w-full">
|
||||||
<SelectValue />
|
<SelectValue>{FEED_TYPE_LABELS[feedType]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{FEED_TYPE_OPTIONS.map((option) => (
|
{FEED_TYPES.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{FEED_TYPE_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Pencil } from "lucide-react";
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { updateVarroaMeasurement } from "@/lib/varroa/api";
|
import { updateVarroaMeasurement } from "@/lib/varroa/api";
|
||||||
import { VarroaMeasurement, VarroaMethod } from "@/lib/varroa/types";
|
import { VarroaMeasurement, VarroaMethod } from "@/lib/varroa/types";
|
||||||
|
import { VARROA_METHODS, VARROA_METHOD_LABELS } from "@/lib/varroa/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -24,13 +25,6 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
const METHOD_OPTIONS: VarroaMethod[] = [
|
|
||||||
"NATURAL_MITE_FALL",
|
|
||||||
"POWDERED_SUGAR",
|
|
||||||
"WASHING_METHOD",
|
|
||||||
"DRONE_BROOD_CONTROL",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasurement }) {
|
export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasurement }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -97,12 +91,12 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme
|
|||||||
onValueChange={(value) => setMethod((value ?? method) as VarroaMethod)}
|
onValueChange={(value) => setMethod((value ?? method) as VarroaMethod)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="varroa-edit-method" className="w-full">
|
<SelectTrigger id="varroa-edit-method" className="w-full">
|
||||||
<SelectValue />
|
<SelectValue>{VARROA_METHOD_LABELS[method]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{METHOD_OPTIONS.map((option) => (
|
{VARROA_METHODS.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{VARROA_METHOD_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export default function NewColonyPage() {
|
|||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
const [apiaryId, setApiaryId] = useState("");
|
const [apiaryId, setApiaryId] = useState("");
|
||||||
const [colonyType, setColonyType] = useState<ColonyType>("PRODUCTION_COLONY");
|
const [colonyType, setColonyType] = useState<ColonyType>("PRODUCTION_COLONY");
|
||||||
|
const [hiveType, setHiveType] = useState("");
|
||||||
|
const [originText, setOriginText] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
@@ -49,7 +51,13 @@ export default function NewColonyPage() {
|
|||||||
setError("Bitte einen Bienenstand auswählen.");
|
setError("Bitte einen Bienenstand auswählen.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutation.mutate({ code, apiaryId, colonyType });
|
mutation.mutate({
|
||||||
|
code,
|
||||||
|
apiaryId,
|
||||||
|
colonyType,
|
||||||
|
hiveType: hiveType || undefined,
|
||||||
|
originText: originText || undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -104,6 +112,22 @@ export default function NewColonyPage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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 ? (
|
{error ? (
|
||||||
<p role="alert" className="text-sm text-destructive">
|
<p role="alert" className="text-sm text-destructive">
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Wrench } from "lucide-react";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { fetchEquipment, updateEquipment } from "@/lib/equipment/api";
|
import { fetchEquipment, updateEquipment } from "@/lib/equipment/api";
|
||||||
import { EquipmentCondition } from "@/lib/equipment/types";
|
import { EquipmentCondition } from "@/lib/equipment/types";
|
||||||
|
import { EQUIPMENT_CONDITIONS, EQUIPMENT_CONDITION_LABELS } from "@/lib/equipment/labels";
|
||||||
import { statusTone } from "@/lib/status-tone";
|
import { statusTone } from "@/lib/status-tone";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -16,8 +17,6 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
const CONDITIONS: EquipmentCondition[] = ["NEW", "GOOD", "WORN", "DEFECTIVE"];
|
|
||||||
|
|
||||||
export default function EquipmentDetailPage() {
|
export default function EquipmentDetailPage() {
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -59,7 +58,9 @@ export default function EquipmentDetailPage() {
|
|||||||
</span>
|
</span>
|
||||||
<CardTitle>{equipment.name}</CardTitle>
|
<CardTitle>{equipment.name}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={statusTone(equipment.condition)}>{equipment.condition}</Badge>
|
<Badge variant={statusTone(equipment.condition)}>
|
||||||
|
{EQUIPMENT_CONDITION_LABELS[equipment.condition]}
|
||||||
|
</Badge>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-3 text-sm">
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
<p>
|
<p>
|
||||||
@@ -85,12 +86,12 @@ export default function EquipmentDetailPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-40">
|
<SelectTrigger className="w-40">
|
||||||
<SelectValue />
|
<SelectValue>{EQUIPMENT_CONDITION_LABELS[equipment.condition]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{CONDITIONS.map((condition) => (
|
{EQUIPMENT_CONDITIONS.map((condition) => (
|
||||||
<SelectItem key={condition} value={condition}>
|
<SelectItem key={condition} value={condition}>
|
||||||
{condition}
|
{EQUIPMENT_CONDITION_LABELS[condition]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import { Wrench } from "lucide-react";
|
import { Wrench } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchEquipmentList } from "@/lib/equipment/api";
|
import { fetchEquipmentList } from "@/lib/equipment/api";
|
||||||
|
import { EQUIPMENT_CONDITION_LABELS } from "@/lib/equipment/labels";
|
||||||
import { statusTone } from "@/lib/status-tone";
|
import { statusTone } from "@/lib/status-tone";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -56,7 +57,11 @@ export default function EquipmentPage() {
|
|||||||
gradient="var(--gradient-sage)"
|
gradient="var(--gradient-sage)"
|
||||||
title={item.name}
|
title={item.name}
|
||||||
subtitle={item.inventoryNumber}
|
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 ?? "—" }]}
|
fields={[{ label: "Standort", value: item.location ?? "—" }]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Utensils } from "lucide-react";
|
|||||||
import { fetchColonies } from "@/lib/colonies/api";
|
import { fetchColonies } from "@/lib/colonies/api";
|
||||||
import { createFeeding } from "@/lib/feedings/api";
|
import { createFeeding } from "@/lib/feedings/api";
|
||||||
import { FeedType } from "@/lib/feedings/types";
|
import { FeedType } from "@/lib/feedings/types";
|
||||||
|
import { FEED_TYPES, FEED_TYPE_LABELS } from "@/lib/feedings/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -22,14 +23,6 @@ import {
|
|||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
const FEED_TYPE_OPTIONS: FeedType[] = [
|
|
||||||
"SUGAR_WATER",
|
|
||||||
"FEED_SYRUP",
|
|
||||||
"FEED_PASTE",
|
|
||||||
"OWN_HONEY",
|
|
||||||
"OTHER",
|
|
||||||
];
|
|
||||||
|
|
||||||
function todayIsoDate(): string {
|
function todayIsoDate(): string {
|
||||||
return new Date().toISOString().slice(0, 10);
|
return new Date().toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
@@ -136,12 +129,12 @@ export default function NewFeedingPage() {
|
|||||||
onValueChange={(value) => setFeedType((value ?? "SUGAR_WATER") as FeedType)}
|
onValueChange={(value) => setFeedType((value ?? "SUGAR_WATER") as FeedType)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="feeding-type" className="h-11 w-full">
|
<SelectTrigger id="feeding-type" className="h-11 w-full">
|
||||||
<SelectValue />
|
<SelectValue>{FEED_TYPE_LABELS[feedType]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{FEED_TYPE_OPTIONS.map((option) => (
|
{FEED_TYPES.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{FEED_TYPE_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</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 { Droplet, FlaskConical } from "lucide-react";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { fetchHoneyBatch, fetchHoneyBatchTraceability } from "@/lib/honey-batches/api";
|
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 { exportHoneyBatchTraceabilityPdf } from "@/lib/export/api";
|
||||||
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
import { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||||
import { DocumentUploadCard } from "@/components/document-upload-card";
|
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { AddMeasurementDialog } from "./add-measurement-dialog";
|
import { AddMeasurementDialog } from "./add-measurement-dialog";
|
||||||
import { ExtractionEditDialog } from "./extraction-edit-dialog";
|
import { ExtractionEditDialog } from "./extraction-edit-dialog";
|
||||||
|
import { HoneyBatchEditDialog } from "./honey-batch-edit-dialog";
|
||||||
|
|
||||||
export default function HoneyBatchDetailPage() {
|
export default function HoneyBatchDetailPage() {
|
||||||
const params = useParams<{ id: string }>();
|
const params = useParams<{ id: string }>();
|
||||||
@@ -56,13 +58,17 @@ export default function HoneyBatchDetailPage() {
|
|||||||
<CardTitle className="font-mono">{honeyBatch.batchNumber}</CardTitle>
|
<CardTitle className="font-mono">{honeyBatch.batchNumber}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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} />
|
<QrCodeDialog entityType="honey-batches" id={honeyBatch.id} />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-3 text-sm">
|
<CardContent className="flex flex-col gap-3 text-sm">
|
||||||
{honeyBatch.honeyType ? <p>Sorte: {honeyBatch.honeyType}</p> : null}
|
{honeyBatch.honeyType ? <p>Sorte: {honeyBatch.honeyType}</p> : null}
|
||||||
{honeyBatch.amount !== null ? <p>Menge: {honeyBatch.amount} kg</p> : null}
|
{honeyBatch.amount !== null ? <p>Menge: {honeyBatch.amount} kg</p> : null}
|
||||||
|
{honeyBatch.waterContent !== null ? (
|
||||||
|
<p>Wassergehalt: {honeyBatch.waterContent}%</p>
|
||||||
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Droplet } from "lucide-react";
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
|
import { fetchHoneyBatches } from "@/lib/honey-batches/api";
|
||||||
import { HoneyBatchStatus } from "@/lib/honey-batches/types";
|
import { HoneyBatchStatus } from "@/lib/honey-batches/types";
|
||||||
|
import { HONEY_BATCH_STATUS_LABELS } from "@/lib/honey-batches/labels";
|
||||||
import { exportHoneyBatchesCsv } from "@/lib/export/api";
|
import { exportHoneyBatchesCsv } from "@/lib/export/api";
|
||||||
import { statusTone } from "@/lib/status-tone";
|
import { statusTone } from "@/lib/status-tone";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -90,7 +91,11 @@ export default function HoneyBatchesPage() {
|
|||||||
icon={Droplet}
|
icon={Droplet}
|
||||||
title={<span className="font-mono">{honeyBatch.batchNumber}</span>}
|
title={<span className="font-mono">{honeyBatch.batchNumber}</span>}
|
||||||
subtitle={honeyBatch.honeyType ?? "—"}
|
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={[
|
fields={[
|
||||||
{
|
{
|
||||||
label: "Menge",
|
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 { ShoppingBag } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchProducts } from "@/lib/products/api";
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
@@ -72,13 +74,14 @@ export default function ProductsPage() {
|
|||||||
title={product.name}
|
title={product.name}
|
||||||
badge={
|
badge={
|
||||||
<Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}>
|
<Badge variant={product.status === "ACTIVE" ? "default" : "secondary"}>
|
||||||
{product.status}
|
{PRODUCT_STATUS_LABELS[product.status]}
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
fields={[
|
fields={[
|
||||||
{ label: "SKU", value: product.sku ?? "—" },
|
{ label: "SKU", value: product.sku ?? "—" },
|
||||||
{ label: "Einheit", value: product.salesUnit ?? "—" },
|
{ label: "Einheit", value: product.salesUnit ?? "—" },
|
||||||
]}
|
]}
|
||||||
|
actions={<EditProductDialog product={product} />}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Crown } from "lucide-react";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { fetchQueens, updateQueen } from "@/lib/queens/api";
|
import { fetchQueens, updateQueen } from "@/lib/queens/api";
|
||||||
import { Queen, QueenStatus } from "@/lib/queens/types";
|
import { Queen, QueenStatus } from "@/lib/queens/types";
|
||||||
|
import { QUEEN_STATUSES, QUEEN_STATUS_LABELS } from "@/lib/queens/labels";
|
||||||
import { statusTone } from "@/lib/status-tone";
|
import { statusTone } from "@/lib/status-tone";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -25,17 +26,6 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
const QUEEN_STATUSES: QueenStatus[] = [
|
|
||||||
"ACTIVE",
|
|
||||||
"RESERVE",
|
|
||||||
"UNMATED",
|
|
||||||
"MATED",
|
|
||||||
"LOST",
|
|
||||||
"REPLACED",
|
|
||||||
"SOLD",
|
|
||||||
"DECEASED",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function QueenEditCard({ queen }: { queen: Queen }) {
|
export function QueenEditCard({ queen }: { queen: Queen }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [breed, setBreed] = useState(queen.breed ?? "");
|
const [breed, setBreed] = useState(queen.breed ?? "");
|
||||||
@@ -82,7 +72,7 @@ export function QueenEditCard({ queen }: { queen: Queen }) {
|
|||||||
</span>
|
</span>
|
||||||
<CardTitle className="font-mono">{queen.code}</CardTitle>
|
<CardTitle className="font-mono">{queen.code}</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={statusTone(queen.status)}>{queen.status}</Badge>
|
<Badge variant={statusTone(queen.status)}>{QUEEN_STATUS_LABELS[queen.status]}</Badge>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
<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>
|
<Label htmlFor="queen-status">Status</Label>
|
||||||
<Select value={status} onValueChange={(value) => setStatus(value as QueenStatus)}>
|
<Select value={status} onValueChange={(value) => setStatus(value as QueenStatus)}>
|
||||||
<SelectTrigger id="queen-status">
|
<SelectTrigger id="queen-status">
|
||||||
<SelectValue />
|
<SelectValue>{QUEEN_STATUS_LABELS[status]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{QUEEN_STATUSES.map((option) => (
|
{QUEEN_STATUSES.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{QUEEN_STATUS_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Crown } from "lucide-react";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchQueens } from "@/lib/queens/api";
|
import { fetchQueens } from "@/lib/queens/api";
|
||||||
import { QueenStatus } from "@/lib/queens/types";
|
import { QueenStatus } from "@/lib/queens/types";
|
||||||
|
import { QUEEN_STATUSES, QUEEN_STATUS_LABELS } from "@/lib/queens/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
@@ -20,17 +21,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
const STATUS_OPTIONS: QueenStatus[] = [
|
|
||||||
"ACTIVE",
|
|
||||||
"RESERVE",
|
|
||||||
"UNMATED",
|
|
||||||
"MATED",
|
|
||||||
"LOST",
|
|
||||||
"REPLACED",
|
|
||||||
"SOLD",
|
|
||||||
"DECEASED",
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function QueensPage() {
|
export default function QueensPage() {
|
||||||
const [tab, setTab] = useState<EntityLifecycleTab>("active");
|
const [tab, setTab] = useState<EntityLifecycleTab>("active");
|
||||||
const [status, setStatus] = useState<QueenStatus | "">("");
|
const [status, setStatus] = useState<QueenStatus | "">("");
|
||||||
@@ -74,13 +64,15 @@ export default function QueensPage() {
|
|||||||
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as QueenStatus))}
|
onValueChange={(value) => setStatus(value === "ALL" ? "" : ((value ?? "") as QueenStatus))}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-40">
|
<SelectTrigger className="w-40">
|
||||||
<SelectValue placeholder="Status">{status || "Alle Status"}</SelectValue>
|
<SelectValue placeholder="Status">
|
||||||
|
{status ? QUEEN_STATUS_LABELS[status] : "Alle Status"}
|
||||||
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="ALL">Alle Status</SelectItem>
|
<SelectItem value="ALL">Alle Status</SelectItem>
|
||||||
{STATUS_OPTIONS.map((option) => (
|
{QUEEN_STATUSES.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{QUEEN_STATUS_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -118,7 +110,7 @@ export default function QueensPage() {
|
|||||||
subtitle={queen.breed ?? "—"}
|
subtitle={queen.breed ?? "—"}
|
||||||
badge={
|
badge={
|
||||||
<Badge variant={queen.status === "ACTIVE" ? "default" : "secondary"}>
|
<Badge variant={queen.status === "ACTIVE" ? "default" : "secondary"}>
|
||||||
{queen.status}
|
{QUEEN_STATUS_LABELS[queen.status]}
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
fields={[{ label: "Schlupfjahr", value: queen.hatchYear ?? "—" }]}
|
fields={[{ label: "Schlupfjahr", value: queen.hatchYear ?? "—" }]}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ describe("AuditLogPage", () => {
|
|||||||
|
|
||||||
renderWithClient(<AuditLogPage />);
|
renderWithClient(<AuditLogPage />);
|
||||||
|
|
||||||
expect(await screen.findByText("Apiary")).toBeInTheDocument();
|
expect(await screen.findByText("Bienenstand")).toBeInTheDocument();
|
||||||
expect(screen.getByText("apiary-1")).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 () => {
|
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 { History } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchAuditLog } from "@/lib/audit-log/api";
|
import { fetchAuditLog } from "@/lib/audit-log/api";
|
||||||
|
import { auditActionLabel, auditEntityTypeLabel } from "@/lib/audit-log/labels";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
@@ -92,9 +93,9 @@ export default function AuditLogPage() {
|
|||||||
{entries.map((entry) => (
|
{entries.map((entry) => (
|
||||||
<TableRow key={entry.id}>
|
<TableRow key={entry.id}>
|
||||||
<TableCell>{new Date(entry.timestamp).toLocaleString("de-DE")}</TableCell>
|
<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 className="font-mono text-xs">{entry.entityId}</TableCell>
|
||||||
<TableCell>{entry.action}</TableCell>
|
<TableCell>{auditActionLabel(entry.action)}</TableCell>
|
||||||
<TableCell>{entry.userId ?? "—"}</TableCell>
|
<TableCell>{entry.userId ?? "—"}</TableCell>
|
||||||
</TableRow>
|
</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 { EntityCard } from "@/components/entity-card";
|
||||||
import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs";
|
import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-history-tabs";
|
||||||
import { CreateTaskTemplateDialog } from "./create-task-template-dialog";
|
import { CreateTaskTemplateDialog } from "./create-task-template-dialog";
|
||||||
|
import { EditTaskTemplateDialog } from "./edit-task-template-dialog";
|
||||||
|
|
||||||
const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = {
|
const RECURRENCE_LABELS: Record<TaskTemplateRecurrenceType, string> = {
|
||||||
YEARLY: "Jährlich",
|
YEARLY: "Jährlich",
|
||||||
@@ -109,6 +110,8 @@ export default function TaskTemplatesPage() {
|
|||||||
{ label: "Nächste Fälligkeit", value: formatDate(template.nextDueDate) },
|
{ label: "Nächste Fälligkeit", value: formatDate(template.nextDueDate) },
|
||||||
]}
|
]}
|
||||||
actions={
|
actions={
|
||||||
|
<>
|
||||||
|
<EditTaskTemplateDialog template={template} />
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -118,6 +121,7 @@ export default function TaskTemplatesPage() {
|
|||||||
>
|
>
|
||||||
Deaktivieren
|
Deaktivieren
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { FormEvent, useState } from "react";
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { createStockItem } from "@/lib/storage/api";
|
import { createStockItem } from "@/lib/storage/api";
|
||||||
import { StockCategory } from "@/lib/storage/types";
|
import { StockCategory } from "@/lib/storage/types";
|
||||||
|
import { STOCK_CATEGORIES, STOCK_CATEGORY_LABELS } from "@/lib/storage/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -23,8 +24,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
const CATEGORY_OPTIONS: StockCategory[] = ["HONEY", "CONSUMABLE", "EQUIPMENT_MATERIAL"];
|
|
||||||
|
|
||||||
export function CreateStockItemDialog() {
|
export function CreateStockItemDialog() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -84,12 +83,12 @@ export function CreateStockItemDialog() {
|
|||||||
onValueChange={(value) => setCategory((value ?? "CONSUMABLE") as StockCategory)}
|
onValueChange={(value) => setCategory((value ?? "CONSUMABLE") as StockCategory)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="item-category">
|
<SelectTrigger id="item-category">
|
||||||
<SelectValue />
|
<SelectValue>{STOCK_CATEGORY_LABELS[category]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{CATEGORY_OPTIONS.map((option) => (
|
{STOCK_CATEGORIES.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{STOCK_CATEGORY_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</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 { Package, Warehouse } from "lucide-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { fetchStockItems, fetchStorageContainers } from "@/lib/storage/api";
|
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 { QrCodeDialog } from "@/components/qr-code-dialog";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
@@ -12,6 +13,8 @@ import { ActiveHistoryTabs, type EntityLifecycleTab } from "@/components/active-
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { CreateStorageContainerDialog } from "./create-storage-container-dialog";
|
import { CreateStorageContainerDialog } from "./create-storage-container-dialog";
|
||||||
import { CreateStockItemDialog } from "./create-stock-item-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";
|
import { RecordMovementDialog } from "./record-movement-dialog";
|
||||||
|
|
||||||
export default function StoragePage() {
|
export default function StoragePage() {
|
||||||
@@ -76,12 +79,15 @@ export default function StoragePage() {
|
|||||||
subtitle={container.containerType ?? "—"}
|
subtitle={container.containerType ?? "—"}
|
||||||
badge={
|
badge={
|
||||||
<Badge variant={container.status === "FILLED" ? "default" : "secondary"}>
|
<Badge variant={container.status === "FILLED" ? "default" : "secondary"}>
|
||||||
{container.status}
|
{STORAGE_CONTAINER_STATUS_LABELS[container.status]}
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
fields={[{ label: "Standort", value: container.location ?? "—" }]}
|
fields={[{ label: "Standort", value: container.location ?? "—" }]}
|
||||||
actions={
|
actions={
|
||||||
|
<>
|
||||||
|
<EditStorageContainerDialog container={container} />
|
||||||
<QrCodeDialog entityType="storage-containers" id={container.id} label="QR" />
|
<QrCodeDialog entityType="storage-containers" id={container.id} label="QR" />
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -111,14 +117,19 @@ export default function StoragePage() {
|
|||||||
icon={Package}
|
icon={Package}
|
||||||
gradient="var(--gradient-sky)"
|
gradient="var(--gradient-sky)"
|
||||||
title={item.name}
|
title={item.name}
|
||||||
subtitle={item.category}
|
subtitle={STOCK_CATEGORY_LABELS[item.category]}
|
||||||
badge={
|
badge={
|
||||||
item.minThreshold !== null && item.quantity < item.minThreshold ? (
|
item.minThreshold !== null && item.quantity < item.minThreshold ? (
|
||||||
<Badge variant="destructive">Niedrig</Badge>
|
<Badge variant="destructive">Niedrig</Badge>
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
fields={[{ label: "Bestand", value: `${item.quantity} ${item.unit}` }]}
|
fields={[{ label: "Bestand", value: `${item.quantity} ${item.unit}` }]}
|
||||||
actions={<RecordMovementDialog stockItemId={item.id} />}
|
actions={
|
||||||
|
<>
|
||||||
|
<EditStockItemDialog item={item} />
|
||||||
|
<RecordMovementDialog stockItemId={item.id} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { FormEvent, useState } from "react";
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { addStockMovement } from "@/lib/storage/api";
|
import { addStockMovement } from "@/lib/storage/api";
|
||||||
import { StockMovementType } from "@/lib/storage/types";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -23,8 +24,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
const TYPE_OPTIONS: StockMovementType[] = ["IN", "OUT", "TRANSFER", "ADJUSTMENT"];
|
|
||||||
|
|
||||||
export function RecordMovementDialog({ stockItemId }: { stockItemId: string }) {
|
export function RecordMovementDialog({ stockItemId }: { stockItemId: string }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -71,12 +70,12 @@ export function RecordMovementDialog({ stockItemId }: { stockItemId: string }) {
|
|||||||
onValueChange={(value) => setType((value ?? "IN") as StockMovementType)}
|
onValueChange={(value) => setType((value ?? "IN") as StockMovementType)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="movement-type">
|
<SelectTrigger id="movement-type">
|
||||||
<SelectValue />
|
<SelectValue>{STOCK_MOVEMENT_TYPE_LABELS[type]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{TYPE_OPTIONS.map((option) => (
|
{STOCK_MOVEMENT_TYPES.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{STOCK_MOVEMENT_TYPE_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { toast } from "sonner";
|
|||||||
import { ListTodo } from "lucide-react";
|
import { ListTodo } from "lucide-react";
|
||||||
import { createTask } from "@/lib/tasks/api";
|
import { createTask } from "@/lib/tasks/api";
|
||||||
import { TaskPriority } from "@/lib/tasks/types";
|
import { TaskPriority } from "@/lib/tasks/types";
|
||||||
|
import { TASK_PRIORITIES, TASK_PRIORITY_LABELS } from "@/lib/tasks/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -21,8 +22,6 @@ import {
|
|||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
const PRIORITY_OPTIONS: TaskPriority[] = ["LOW", "NORMAL", "HIGH", "URGENT"];
|
|
||||||
|
|
||||||
export default function NewTaskPage() {
|
export default function NewTaskPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -82,12 +81,12 @@ export default function NewTaskPage() {
|
|||||||
onValueChange={(value) => setPriority((value ?? "NORMAL") as TaskPriority)}
|
onValueChange={(value) => setPriority((value ?? "NORMAL") as TaskPriority)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="task-priority">
|
<SelectTrigger id="task-priority">
|
||||||
<SelectValue />
|
<SelectValue>{TASK_PRIORITY_LABELS[priority]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{PRIORITY_OPTIONS.map((option) => (
|
{TASK_PRIORITIES.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{TASK_PRIORITY_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ListTodo } from "lucide-react";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { completeTask, fetchTasks } from "@/lib/tasks/api";
|
import { completeTask, fetchTasks } from "@/lib/tasks/api";
|
||||||
import { TaskPriority, TaskStatus } from "@/lib/tasks/types";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
@@ -21,7 +22,6 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { TaskEditDialog } from "./task-edit-dialog";
|
import { TaskEditDialog } from "./task-edit-dialog";
|
||||||
|
|
||||||
const PRIORITY_OPTIONS: TaskPriority[] = ["LOW", "NORMAL", "HIGH", "URGENT"];
|
|
||||||
const HISTORY_STATUSES: TaskStatus[] = ["DONE", "CANCELLED"];
|
const HISTORY_STATUSES: TaskStatus[] = ["DONE", "CANCELLED"];
|
||||||
|
|
||||||
export default function TasksPage() {
|
export default function TasksPage() {
|
||||||
@@ -79,13 +79,15 @@ export default function TasksPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-40">
|
<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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="ALL">Alle Prioritäten</SelectItem>
|
<SelectItem value="ALL">Alle Prioritäten</SelectItem>
|
||||||
{PRIORITY_OPTIONS.map((option) => (
|
{TASK_PRIORITIES.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{TASK_PRIORITY_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -133,11 +135,11 @@ export default function TasksPage() {
|
|||||||
title={task.title}
|
title={task.title}
|
||||||
badge={
|
badge={
|
||||||
<Badge variant={task.status === "DONE" ? "secondary" : "default"}>
|
<Badge variant={task.status === "DONE" ? "secondary" : "default"}>
|
||||||
{task.status}
|
{TASK_STATUS_LABELS[task.status]}
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
fields={[
|
fields={[
|
||||||
{ label: "Priorität", value: task.priority },
|
{ label: "Priorität", value: TASK_PRIORITY_LABELS[task.priority] },
|
||||||
{
|
{
|
||||||
label: "Fällig",
|
label: "Fällig",
|
||||||
value: task.dueDate ? new Date(task.dueDate).toLocaleDateString("de-DE") : "—",
|
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 { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { updateTask } from "@/lib/tasks/api";
|
import { updateTask } from "@/lib/tasks/api";
|
||||||
import { Task, TaskPriority } from "@/lib/tasks/types";
|
import { Task, TaskPriority } from "@/lib/tasks/types";
|
||||||
|
import { TASK_PRIORITIES, TASK_PRIORITY_LABELS } from "@/lib/tasks/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -24,8 +25,6 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
const PRIORITY_OPTIONS: TaskPriority[] = ["LOW", "NORMAL", "HIGH", "URGENT"];
|
|
||||||
|
|
||||||
export function TaskEditDialog({ task }: { task: Task }) {
|
export function TaskEditDialog({ task }: { task: Task }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -84,12 +83,12 @@ export function TaskEditDialog({ task }: { task: Task }) {
|
|||||||
onValueChange={(value) => setPriority((value ?? priority) as TaskPriority)}
|
onValueChange={(value) => setPriority((value ?? priority) as TaskPriority)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="task-edit-priority" className="w-full">
|
<SelectTrigger id="task-edit-priority" className="w-full">
|
||||||
<SelectValue />
|
<SelectValue>{TASK_PRIORITY_LABELS[priority]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{PRIORITY_OPTIONS.map((option) => (
|
{TASK_PRIORITIES.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{TASK_PRIORITY_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Bug } from "lucide-react";
|
|||||||
import { fetchColonies } from "@/lib/colonies/api";
|
import { fetchColonies } from "@/lib/colonies/api";
|
||||||
import { createVarroaMeasurement } from "@/lib/varroa/api";
|
import { createVarroaMeasurement } from "@/lib/varroa/api";
|
||||||
import { VarroaMethod } from "@/lib/varroa/types";
|
import { VarroaMethod } from "@/lib/varroa/types";
|
||||||
|
import { VARROA_METHODS, VARROA_METHOD_LABELS } from "@/lib/varroa/labels";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -22,13 +23,6 @@ import {
|
|||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
const METHOD_OPTIONS: VarroaMethod[] = [
|
|
||||||
"NATURAL_MITE_FALL",
|
|
||||||
"POWDERED_SUGAR",
|
|
||||||
"WASHING_METHOD",
|
|
||||||
"DRONE_BROOD_CONTROL",
|
|
||||||
];
|
|
||||||
|
|
||||||
function todayIsoDate(): string {
|
function todayIsoDate(): string {
|
||||||
return new Date().toISOString().slice(0, 10);
|
return new Date().toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
@@ -129,12 +123,12 @@ export default function NewVarroaMeasurementPage() {
|
|||||||
onValueChange={(value) => setMethod((value ?? "NATURAL_MITE_FALL") as VarroaMethod)}
|
onValueChange={(value) => setMethod((value ?? "NATURAL_MITE_FALL") as VarroaMethod)}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="varroa-method" className="h-11 w-full">
|
<SelectTrigger id="varroa-method" className="h-11 w-full">
|
||||||
<SelectValue />
|
<SelectValue>{VARROA_METHOD_LABELS[method]}</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{METHOD_OPTIONS.map((option) => (
|
{VARROA_METHODS.map((option) => (
|
||||||
<SelectItem key={option} value={option}>
|
<SelectItem key={option} value={option}>
|
||||||
{option}
|
{VARROA_METHOD_LABELS[option]}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</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,
|
HoneyBatchStatus,
|
||||||
RelocateHoneyBatchInput,
|
RelocateHoneyBatchInput,
|
||||||
Traceability,
|
Traceability,
|
||||||
|
UpdateHoneyBatchInput,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export interface HoneyBatchFilter {
|
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(
|
export function addHoneyMeasurement(
|
||||||
id: string,
|
id: string,
|
||||||
input: CreateHoneyMeasurementInput,
|
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;
|
waterContent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateHoneyBatchInput {
|
||||||
|
honeyType?: string;
|
||||||
|
amount?: number;
|
||||||
|
waterContent?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateHoneyMeasurementInput {
|
export interface CreateHoneyMeasurementInput {
|
||||||
waterContent: number;
|
waterContent: number;
|
||||||
measuredAt: string;
|
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,
|
CreateStorageContainerInput,
|
||||||
StockItem,
|
StockItem,
|
||||||
StorageContainer,
|
StorageContainer,
|
||||||
|
UpdateStockItemInput,
|
||||||
|
UpdateStorageContainerInput,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export function fetchStorageContainers(): Promise<StorageContainer[]> {
|
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[]> {
|
export function fetchStockItems(): Promise<StockItem[]> {
|
||||||
return apiFetch<StockItem[]>("/stock-items");
|
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(
|
export function addStockMovement(
|
||||||
id: string,
|
id: string,
|
||||||
input: CreateStockMovementInput,
|
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;
|
location?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateStorageContainerInput {
|
||||||
|
containerType?: string;
|
||||||
|
capacity?: number;
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateStockItemInput {
|
export interface CreateStockItemInput {
|
||||||
name: string;
|
name: string;
|
||||||
category: StockCategory;
|
category: StockCategory;
|
||||||
@@ -50,6 +56,14 @@ export interface CreateStockItemInput {
|
|||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateStockItemInput {
|
||||||
|
name?: string;
|
||||||
|
category?: StockCategory;
|
||||||
|
unit?: string;
|
||||||
|
minThreshold?: number;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateStockMovementInput {
|
export interface CreateStockMovementInput {
|
||||||
type: StockMovementType;
|
type: StockMovementType;
|
||||||
quantity: number;
|
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