feat: allow correcting honey measurements and queen evaluations
Both records could be created but never changed. A water-content reading decides whether a batch may be sold as honey at all, and queen ratings drive breeding decisions - a slipped digit in either is exactly the kind of wrong value that must not be left standing. - New PATCH /honey-batches/measurements/:measurementId behind honey-batches:measure, and PATCH /queens/evaluations/:evaluationId behind queens:evaluate. Both are addressed by the child record's own id, so the routes take two path segments and do not collide with PATCH /:id. - Both log before and after data, like every other correction. - The measurement list on the honey batch page and the rating list on the queen page gained an edit dialog; the rating dialog reuses the existing criteria list rather than restating it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
b0e05daa42
commit
42ceb33844
14 files changed
+503
-15
No files matched your search
@@ -0,0 +1,11 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateHoneyMeasurementDto } from './create-honey-measurement.dto';
|
||||
|
||||
/**
|
||||
* A water-content reading typed in wrong has to be correctable: it decides
|
||||
* whether a batch may be sold as honey at all, so a wrong figure left standing
|
||||
* is worse than one that was never recorded.
|
||||
*/
|
||||
export class UpdateHoneyMeasurementDto extends PartialType(
|
||||
CreateHoneyMeasurementDto,
|
||||
) {}
|
||||
@@ -17,6 +17,7 @@ import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||
import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto';
|
||||
import { UpdateHoneyBatchDto } from './dto/update-honey-batch.dto';
|
||||
import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
|
||||
import { UpdateHoneyMeasurementDto } from './dto/update-honey-measurement.dto';
|
||||
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto';
|
||||
import { SetHoneyBatchContainerAllocationDto } from './dto/set-honey-batch-container-allocation.dto';
|
||||
import { HoneyBatchesService } from './honey-batches.service';
|
||||
@@ -117,6 +118,20 @@ export class HoneyBatchesController {
|
||||
}
|
||||
|
||||
@RequirePermission('honey-batches:measure')
|
||||
@RequirePermission('honey-batches:measure')
|
||||
@Patch('measurements/:measurementId')
|
||||
updateMeasurement(
|
||||
@Param('measurementId') measurementId: string,
|
||||
@Body() dto: UpdateHoneyMeasurementDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
) {
|
||||
return this.honeyBatchesService.updateMeasurement(
|
||||
measurementId,
|
||||
dto,
|
||||
actor.id,
|
||||
);
|
||||
}
|
||||
|
||||
@Post(':id/measurements')
|
||||
addMeasurement(
|
||||
@Param('id') id: string,
|
||||
|
||||
@@ -14,7 +14,11 @@ describe('HoneyBatchesService', () => {
|
||||
create: jest.Mock;
|
||||
update: jest.Mock;
|
||||
};
|
||||
honeyMeasurement: { create: jest.Mock };
|
||||
honeyMeasurement: {
|
||||
create: jest.Mock;
|
||||
findUnique: jest.Mock;
|
||||
update: jest.Mock;
|
||||
};
|
||||
storageContainer: { findUnique: jest.Mock };
|
||||
honeyBatchRelocation: { create: jest.Mock; findMany: jest.Mock };
|
||||
honeyBatchContainerAllocation: {
|
||||
@@ -60,7 +64,11 @@ describe('HoneyBatchesService', () => {
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
honeyMeasurement: { create: jest.fn() },
|
||||
honeyMeasurement: {
|
||||
create: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
storageContainer: { findUnique: jest.fn() },
|
||||
honeyBatchRelocation: { create: jest.fn(), findMany: jest.fn() },
|
||||
honeyBatchContainerAllocation: {
|
||||
@@ -398,4 +406,41 @@ describe('HoneyBatchesService', () => {
|
||||
expect(prisma.honeyBatchRelocation.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('updateMeasurement corrects a reading and logs before/after', async () => {
|
||||
const before = {
|
||||
id: 'measurement-1',
|
||||
honeyBatchId: 'batch-1',
|
||||
waterContent: 22,
|
||||
};
|
||||
prisma.honeyMeasurement.findUnique.mockResolvedValue(before);
|
||||
prisma.honeyMeasurement.update.mockResolvedValue({
|
||||
...before,
|
||||
waterContent: 17.5,
|
||||
});
|
||||
prisma.honeyBatch.findUnique.mockResolvedValue(honeyBatch);
|
||||
|
||||
await service.updateMeasurement(
|
||||
'measurement-1',
|
||||
{ waterContent: 17.5 },
|
||||
'user-1',
|
||||
);
|
||||
|
||||
expect(auditLog.record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityType: 'HoneyMeasurement',
|
||||
entityId: 'measurement-1',
|
||||
action: 'UPDATE',
|
||||
beforeData: before,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('updateMeasurement throws NotFoundException for an unknown reading', async () => {
|
||||
prisma.honeyMeasurement.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.updateMeasurement('missing', { waterContent: 17.5 }, 'user-1'),
|
||||
).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { HoneyBatchStatus } from '../../generated/prisma';
|
||||
import { CreateHoneyBatchDto } from './dto/create-honey-batch.dto';
|
||||
import { UpdateHoneyBatchDto } from './dto/update-honey-batch.dto';
|
||||
import { CreateHoneyMeasurementDto } from './dto/create-honey-measurement.dto';
|
||||
import { UpdateHoneyMeasurementDto } from './dto/update-honey-measurement.dto';
|
||||
import { RelocateHoneyBatchDto } from './dto/relocate-honey-batch.dto';
|
||||
import { SetHoneyBatchContainerAllocationDto } from './dto/set-honey-batch-container-allocation.dto';
|
||||
|
||||
@@ -122,6 +123,35 @@ export class HoneyBatchesService {
|
||||
return updated;
|
||||
}
|
||||
|
||||
async updateMeasurement(
|
||||
measurementId: string,
|
||||
dto: UpdateHoneyMeasurementDto,
|
||||
actingUserId: string,
|
||||
) {
|
||||
const before = await this.prisma.honeyMeasurement.findUnique({
|
||||
where: { id: measurementId },
|
||||
});
|
||||
if (!before) {
|
||||
throw new NotFoundException('Honey measurement not found');
|
||||
}
|
||||
const measurement = await this.prisma.honeyMeasurement.update({
|
||||
where: { id: measurementId },
|
||||
data: {
|
||||
...dto,
|
||||
measuredAt: dto.measuredAt ? new Date(dto.measuredAt) : undefined,
|
||||
},
|
||||
});
|
||||
await this.auditLog.record({
|
||||
entityType: 'HoneyMeasurement',
|
||||
entityId: measurementId,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: measurement,
|
||||
});
|
||||
return this.findById(before.honeyBatchId);
|
||||
}
|
||||
|
||||
async addMeasurement(
|
||||
id: string,
|
||||
dto: CreateHoneyMeasurementDto,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateQueenEvaluationDto } from './create-queen-evaluation.dto';
|
||||
|
||||
/**
|
||||
* Breeding decisions are made from these scores, so a rating entered against
|
||||
* the wrong queen or with a slipped digit has to be correctable.
|
||||
*/
|
||||
export class UpdateQueenEvaluationDto extends PartialType(
|
||||
CreateQueenEvaluationDto,
|
||||
) {}
|
||||
@@ -16,6 +16,7 @@ import { CreateQueenDto } from './dto/create-queen.dto';
|
||||
import { UpdateQueenDto } from './dto/update-queen.dto';
|
||||
import { AssignColonyDto } from './dto/assign-colony.dto';
|
||||
import { CreateQueenEvaluationDto } from './dto/create-queen-evaluation.dto';
|
||||
import { UpdateQueenEvaluationDto } from './dto/update-queen-evaluation.dto';
|
||||
import { QueensService } from './queens.service';
|
||||
|
||||
@ApiTags('queens')
|
||||
@@ -81,6 +82,16 @@ export class QueensController {
|
||||
}
|
||||
|
||||
@RequirePermission('queens:evaluate')
|
||||
@RequirePermission('queens:evaluate')
|
||||
@Patch('evaluations/:evaluationId')
|
||||
updateEvaluation(
|
||||
@Param('evaluationId') evaluationId: string,
|
||||
@Body() dto: UpdateQueenEvaluationDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
) {
|
||||
return this.queensService.updateEvaluation(evaluationId, dto, actor.id);
|
||||
}
|
||||
|
||||
@Post(':id/evaluations')
|
||||
addEvaluation(
|
||||
@Param('id') id: string,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { CreateQueenDto } from './dto/create-queen.dto';
|
||||
import { UpdateQueenDto } from './dto/update-queen.dto';
|
||||
import { AssignColonyDto } from './dto/assign-colony.dto';
|
||||
import { CreateQueenEvaluationDto } from './dto/create-queen-evaluation.dto';
|
||||
import { UpdateQueenEvaluationDto } from './dto/update-queen-evaluation.dto';
|
||||
|
||||
export interface QueenFilter {
|
||||
status?: QueenStatus;
|
||||
@@ -227,6 +228,32 @@ export class QueensService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateEvaluation(
|
||||
evaluationId: string,
|
||||
dto: UpdateQueenEvaluationDto,
|
||||
actingUserId: string,
|
||||
) {
|
||||
const before = await this.prisma.queenEvaluation.findUnique({
|
||||
where: { id: evaluationId },
|
||||
});
|
||||
if (!before) {
|
||||
throw new NotFoundException('Queen evaluation not found');
|
||||
}
|
||||
const evaluation = await this.prisma.queenEvaluation.update({
|
||||
where: { id: evaluationId },
|
||||
data: { ...dto, date: dto.date ? new Date(dto.date) : undefined },
|
||||
});
|
||||
await this.auditLog.record({
|
||||
entityType: 'QueenEvaluation',
|
||||
entityId: evaluationId,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: evaluation,
|
||||
});
|
||||
return evaluation;
|
||||
}
|
||||
|
||||
async addEvaluation(
|
||||
id: string,
|
||||
dto: CreateQueenEvaluationDto,
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { updateHoneyMeasurement } from "@/lib/honey-batches/api";
|
||||
import { HoneyMeasurement } from "@/lib/honey-batches/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,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
/**
|
||||
* Corrects a water-content reading. The figure decides whether a batch may be
|
||||
* sold as honey at all, so a slipped digit left standing is worse than a
|
||||
* reading that was never recorded.
|
||||
*/
|
||||
function EditMeasurementForm({
|
||||
measurement,
|
||||
honeyBatchId,
|
||||
onSaved,
|
||||
}: {
|
||||
measurement: HoneyMeasurement;
|
||||
honeyBatchId: string;
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [waterContent, setWaterContent] = useState(
|
||||
measurement.waterContent.toString(),
|
||||
);
|
||||
const [measuredAt, setMeasuredAt] = useState(
|
||||
measurement.measuredAt.slice(0, 10),
|
||||
);
|
||||
const [device, setDevice] = useState(measurement.device ?? "");
|
||||
const [temperature, setTemperature] = useState(
|
||||
measurement.temperature?.toString() ?? "",
|
||||
);
|
||||
const [notes, setNotes] = useState(measurement.notes ?? "");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateHoneyMeasurement(measurement.id, {
|
||||
waterContent: Number(waterContent),
|
||||
measuredAt: new Date(measuredAt).toISOString(),
|
||||
device: device || undefined,
|
||||
temperature: temperature ? Number(temperature) : undefined,
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["honey-batches", honeyBatchId],
|
||||
});
|
||||
onSaved();
|
||||
},
|
||||
onError: (mutationError: Error) => {
|
||||
const message =
|
||||
mutationError.message || "Messung konnte nicht gespeichert werden";
|
||||
setError(message);
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="measurement-edit-water">Wassergehalt (%)</Label>
|
||||
<Input
|
||||
id="measurement-edit-water"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min={0}
|
||||
max={100}
|
||||
required
|
||||
value={waterContent}
|
||||
onChange={(event) => setWaterContent(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="measurement-edit-date">Gemessen am</Label>
|
||||
<Input
|
||||
id="measurement-edit-date"
|
||||
type="date"
|
||||
required
|
||||
value={measuredAt}
|
||||
onChange={(event) => setMeasuredAt(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="measurement-edit-device">Gerät</Label>
|
||||
<Input
|
||||
id="measurement-edit-device"
|
||||
value={device}
|
||||
onChange={(event) => setDevice(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Label htmlFor="measurement-edit-temp">Temperatur (°C)</Label>
|
||||
<Input
|
||||
id="measurement-edit-temp"
|
||||
type="number"
|
||||
value={temperature}
|
||||
onChange={(event) => setTemperature(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="measurement-edit-notes">Notizen</Label>
|
||||
<Textarea
|
||||
id="measurement-edit-notes"
|
||||
value={notes}
|
||||
onChange={(event) => setNotes(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? "Speichert…" : "Speichern"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function EditMeasurementDialog({
|
||||
measurement,
|
||||
honeyBatchId,
|
||||
}: {
|
||||
measurement: HoneyMeasurement;
|
||||
honeyBatchId: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger
|
||||
render={
|
||||
<Button variant="ghost" size="sm">
|
||||
<Pencil className="size-3.5" />
|
||||
Bearbeiten
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Messung bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
{open ? (
|
||||
<EditMeasurementForm
|
||||
measurement={measurement}
|
||||
honeyBatchId={honeyBatchId}
|
||||
onSaved={() => setOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AddMeasurementDialog } from "./add-measurement-dialog";
|
||||
import { EditMeasurementDialog } from "./edit-measurement-dialog";
|
||||
import { ExtractionEditDialog } from "./extraction-edit-dialog";
|
||||
import { HoneyBatchEditDialog } from "./honey-batch-edit-dialog";
|
||||
|
||||
@@ -106,13 +107,21 @@ export default function HoneyBatchDetailPage() {
|
||||
) : null}
|
||||
{honeyBatch.measurements.map((measurement) => (
|
||||
<div key={measurement.id} className="rounded-md border p-3">
|
||||
<p className="font-medium">
|
||||
{new Date(measurement.measuredAt).toLocaleDateString("de-DE")} ·{" "}
|
||||
{measurement.waterContent}% Wassergehalt
|
||||
</p>
|
||||
{measurement.device ? (
|
||||
<p className="text-muted-foreground">{measurement.device}</p>
|
||||
) : null}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{new Date(measurement.measuredAt).toLocaleDateString("de-DE")} ·{" "}
|
||||
{measurement.waterContent}% Wassergehalt
|
||||
</p>
|
||||
{measurement.device ? (
|
||||
<p className="text-muted-foreground">{measurement.device}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<EditMeasurementDialog
|
||||
measurement={measurement}
|
||||
honeyBatchId={honeyBatch.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Star } from "lucide-react";
|
||||
import { Pencil, Star } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { addQueenEvaluation, fetchQueenEvaluations } from "@/lib/queens/api";
|
||||
import { CreateQueenEvaluationInput } from "@/lib/queens/types";
|
||||
import {
|
||||
addQueenEvaluation,
|
||||
fetchQueenEvaluations,
|
||||
updateQueenEvaluation,
|
||||
} from "@/lib/queens/api";
|
||||
import { CreateQueenEvaluationInput, QueenEvaluation } from "@/lib/queens/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -32,6 +36,122 @@ function todayIsoDate(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects a rating after the fact. Breeding decisions are made from these
|
||||
* scores, so a slipped digit has to be fixable rather than left standing.
|
||||
*/
|
||||
function EditEvaluationDialog({
|
||||
evaluation,
|
||||
queenId,
|
||||
}: {
|
||||
evaluation: QueenEvaluation;
|
||||
queenId: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [date, setDate] = useState(evaluation.date.slice(0, 10));
|
||||
const [scores, setScores] = useState<Record<string, string>>(
|
||||
Object.fromEntries(
|
||||
CRITERIA.map((criterion) => [
|
||||
criterion.key,
|
||||
String(evaluation[criterion.key]),
|
||||
]),
|
||||
),
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateQueenEvaluation(evaluation.id, {
|
||||
date: new Date(date).toISOString(),
|
||||
colonyStrength: Number(scores.colonyStrength),
|
||||
gentleness: Number(scores.gentleness),
|
||||
combBuild: Number(scores.combBuild),
|
||||
swarmingTendency: Number(scores.swarmingTendency),
|
||||
broodPattern: Number(scores.broodPattern),
|
||||
honeyYield: Number(scores.honeyYield),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["queens", queenId, "evaluations"],
|
||||
});
|
||||
setOpen(false);
|
||||
setError(null);
|
||||
},
|
||||
onError: (mutationError: Error) =>
|
||||
setError(
|
||||
mutationError.message || "Bewertung 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>Bewertung bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={`evaluation-edit-date-${evaluation.id}`}>Datum</Label>
|
||||
<Input
|
||||
id={`evaluation-edit-date-${evaluation.id}`}
|
||||
type="date"
|
||||
required
|
||||
value={date}
|
||||
onChange={(event) => setDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{CRITERIA.map((criterion) => (
|
||||
<div key={criterion.key} className="flex flex-col gap-2">
|
||||
<Label htmlFor={`evaluation-edit-${criterion.key}-${evaluation.id}`}>
|
||||
{criterion.label} (1–5)
|
||||
</Label>
|
||||
<Input
|
||||
id={`evaluation-edit-${criterion.key}-${evaluation.id}`}
|
||||
type="number"
|
||||
min={1}
|
||||
max={5}
|
||||
required
|
||||
value={scores[criterion.key]}
|
||||
onChange={(event) =>
|
||||
setScores((prev) => ({
|
||||
...prev,
|
||||
[criterion.key]: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{error ? (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? "Wird gespeichert…" : "Speichern"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function QueenEvaluationsCard({ queenId }: { queenId: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -141,9 +261,12 @@ export function QueenEvaluationsCard({ queenId }: { queenId: string }) {
|
||||
) : null}
|
||||
{evaluations?.map((evaluation) => (
|
||||
<div key={evaluation.id} className="rounded-md border p-3">
|
||||
<p className="font-medium">
|
||||
{new Date(evaluation.date).toLocaleDateString("de-DE")}
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="font-medium">
|
||||
{new Date(evaluation.date).toLocaleDateString("de-DE")}
|
||||
</p>
|
||||
<EditEvaluationDialog evaluation={evaluation} queenId={queenId} />
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Stärke {evaluation.colonyStrength} · Sanftmut {evaluation.gentleness} · Wabensitz{" "}
|
||||
{evaluation.combBuild} · Schwarmtrieb {evaluation.swarmingTendency} · Brutbild{" "}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { apiFetch } from "@/lib/auth/api-client";
|
||||
import {
|
||||
CreateHoneyBatchInput,
|
||||
CreateHoneyMeasurementInput,
|
||||
UpdateHoneyMeasurementInput,
|
||||
HoneyBatch,
|
||||
HoneyBatchContainerAllocation,
|
||||
HoneyBatchRelocation,
|
||||
@@ -47,6 +48,16 @@ export function updateHoneyBatch(
|
||||
});
|
||||
}
|
||||
|
||||
export function updateHoneyMeasurement(
|
||||
measurementId: string,
|
||||
input: UpdateHoneyMeasurementInput,
|
||||
): Promise<HoneyBatch> {
|
||||
return apiFetch<HoneyBatch>(`/honey-batches/measurements/${measurementId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function addHoneyMeasurement(
|
||||
id: string,
|
||||
input: CreateHoneyMeasurementInput,
|
||||
|
||||
@@ -107,3 +107,11 @@ export interface CreateHoneyMeasurementInput {
|
||||
temperature?: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateHoneyMeasurementInput {
|
||||
waterContent?: number;
|
||||
measuredAt?: string;
|
||||
device?: string;
|
||||
temperature?: number;
|
||||
notes?: string;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { apiFetch } from "@/lib/auth/api-client";
|
||||
import {
|
||||
AssignColonyInput,
|
||||
CreateQueenEvaluationInput,
|
||||
UpdateQueenEvaluationInput,
|
||||
CreateQueenInput,
|
||||
Queen,
|
||||
QueenColonyHistoryEntry,
|
||||
@@ -59,6 +60,16 @@ export function fetchQueenEvaluations(id: string): Promise<QueenEvaluation[]> {
|
||||
return apiFetch<QueenEvaluation[]>(`/queens/${id}/evaluations`);
|
||||
}
|
||||
|
||||
export function updateQueenEvaluation(
|
||||
evaluationId: string,
|
||||
input: UpdateQueenEvaluationInput,
|
||||
): Promise<QueenEvaluation> {
|
||||
return apiFetch<QueenEvaluation>(`/queens/evaluations/${evaluationId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function addQueenEvaluation(
|
||||
id: string,
|
||||
input: CreateQueenEvaluationInput,
|
||||
|
||||
@@ -124,3 +124,5 @@ export interface CreateQueenEvaluationInput {
|
||||
honeyYield: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export type UpdateQueenEvaluationInput = Partial<CreateQueenEvaluationInput>;
|
||||
Reference in new issue
Block a user