feat: warn against harvesting honey during a treatment's Wartezeit
Treatments (Ameisensäure, Oxalsäure, etc.) can require a legally mandated withdrawal period before honey from that colony may be sold. Nothing in the app tracked this before, so a beekeeper had no safeguard against harvesting into an active withdrawal window. Treatments now optionally record withdrawalPeriodDays (from the product's Packungsbeilage). The new harvest form fetches each candidate colony's active withdrawal window and shows an inline warning naming the product and the date it clears, computed as max(endDate ?? date) + withdrawalPeriodDays across a colony's treatments, restricted to windows still in the future. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
0881a38800
commit
d42f8331f9
13 files changed
+270
-56
No files matched your search
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Treatment" ADD COLUMN "withdrawalPeriodDays" INTEGER;
|
||||||
@@ -569,22 +569,23 @@ model SwarmControlCheck {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
model Treatment {
|
model Treatment {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
colonyId String
|
colonyId String
|
||||||
colony Colony @relation(fields: [colonyId], references: [id])
|
colony Colony @relation(fields: [colonyId], references: [id])
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
date DateTime
|
date DateTime
|
||||||
startDate DateTime?
|
startDate DateTime?
|
||||||
endDate DateTime?
|
endDate DateTime?
|
||||||
product String
|
product String
|
||||||
activeIngredient String?
|
activeIngredient String?
|
||||||
amount String?
|
amount String?
|
||||||
method String?
|
method String?
|
||||||
reason String?
|
reason String?
|
||||||
batchNumber String?
|
batchNumber String?
|
||||||
notes String?
|
notes String?
|
||||||
createdAt DateTime @default(now())
|
withdrawalPeriodDays Int?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
@@index([colonyId, date])
|
@@index([colonyId, date])
|
||||||
}
|
}
|
||||||
@@ -708,9 +709,9 @@ model HoneyBatch {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
measurements HoneyMeasurement[]
|
measurements HoneyMeasurement[]
|
||||||
bottlingBatches BottlingBatch[]
|
bottlingBatches BottlingBatch[]
|
||||||
relocations HoneyBatchRelocation[]
|
relocations HoneyBatchRelocation[]
|
||||||
containerAllocations HoneyBatchContainerAllocation[]
|
containerAllocations HoneyBatchContainerAllocation[]
|
||||||
|
|
||||||
@@index([status])
|
@@index([status])
|
||||||
@@ -757,10 +758,10 @@ model StorageContainer {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
honeyBatches HoneyBatch[]
|
honeyBatches HoneyBatch[]
|
||||||
relocationsFrom HoneyBatchRelocation[] @relation("RelocationFrom")
|
relocationsFrom HoneyBatchRelocation[] @relation("RelocationFrom")
|
||||||
relocationsTo HoneyBatchRelocation[] @relation("RelocationTo")
|
relocationsTo HoneyBatchRelocation[] @relation("RelocationTo")
|
||||||
batchAllocations HoneyBatchContainerAllocation[]
|
batchAllocations HoneyBatchContainerAllocation[]
|
||||||
|
|
||||||
@@index([status])
|
@@index([status])
|
||||||
}
|
}
|
||||||
@@ -879,14 +880,14 @@ model Product {
|
|||||||
// Etikettenvorlage mit {{Platzhalter}}-Text, wird beim Drucken mit den
|
// Etikettenvorlage mit {{Platzhalter}}-Text, wird beim Drucken mit den
|
||||||
// Daten einer Abfuellung befuellt (siehe LabelTemplatesService.renderLabelsPdf).
|
// Daten einer Abfuellung befuellt (siehe LabelTemplatesService.renderLabelsPdf).
|
||||||
model LabelTemplate {
|
model LabelTemplate {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
// Array of LabelElement objects (text/placeholder/qrCode/image/line), see
|
// Array of LabelElement objects (text/placeholder/qrCode/image/line), see
|
||||||
// label-templates.service.ts. Drives both the in-browser canvas designer
|
// label-templates.service.ts. Drives both the in-browser canvas designer
|
||||||
// and the pdfkit print renderer from the same source of truth.
|
// and the pdfkit print renderer from the same source of truth.
|
||||||
elements Json @default("[]")
|
elements Json @default("[]")
|
||||||
widthMm Float @default(90)
|
widthMm Float @default(90)
|
||||||
heightMm Float @default(35)
|
heightMm Float @default(35)
|
||||||
// A4 sheet layout. columns/rows are nullable: null means "auto-fit as many
|
// A4 sheet layout. columns/rows are nullable: null means "auto-fit as many
|
||||||
// as fit" (the original behaviour); a real label sheet product (e.g. a
|
// as fit" (the original behaviour); a real label sheet product (e.g. a
|
||||||
// specific Avery SKU) publishes an exact columns/rows/margin/gap grid, so
|
// specific Avery SKU) publishes an exact columns/rows/margin/gap grid, so
|
||||||
@@ -894,14 +895,14 @@ model LabelTemplate {
|
|||||||
// pre-die-cut sheet instead of just whatever the naive fit computes.
|
// pre-die-cut sheet instead of just whatever the naive fit computes.
|
||||||
columns Int?
|
columns Int?
|
||||||
rows Int?
|
rows Int?
|
||||||
marginTopMm Float @default(5)
|
marginTopMm Float @default(5)
|
||||||
marginLeftMm Float @default(5)
|
marginLeftMm Float @default(5)
|
||||||
gapXMm Float @default(2)
|
gapXMm Float @default(2)
|
||||||
gapYMm Float @default(2)
|
gapYMm Float @default(2)
|
||||||
isDefault Boolean @default(false)
|
isDefault Boolean @default(false)
|
||||||
active Boolean @default(true)
|
active Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@index([active])
|
@@index([active])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsDateString, IsOptional, IsString } from 'class-validator';
|
import {
|
||||||
|
IsDateString,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
Min,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
export class CreateTreatmentDto {
|
export class CreateTreatmentDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -53,4 +59,13 @@ export class CreateTreatmentDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
'Wartezeit in Tagen ab Behandlungsende, bevor Honig aus diesem Volk geerntet werden darf.',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
withdrawalPeriodDays?: number;
|
||||||
}
|
}
|
||||||
@@ -13,5 +13,6 @@ export class UpdateTreatmentDto extends PartialType(
|
|||||||
'reason',
|
'reason',
|
||||||
'batchNumber',
|
'batchNumber',
|
||||||
'notes',
|
'notes',
|
||||||
|
'withdrawalPeriodDays',
|
||||||
] as const),
|
] as const),
|
||||||
) {}
|
) {}
|
||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Body, Controller, Get, Param, Patch, Post } 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 { 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';
|
||||||
@@ -19,6 +27,13 @@ export class TreatmentsController {
|
|||||||
return this.treatmentsService.findAll();
|
return this.treatmentsService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequirePermission('treatments:read')
|
||||||
|
@Get('withdrawal-status')
|
||||||
|
getWithdrawalStatus(@Query('colonyIds') colonyIds?: string) {
|
||||||
|
const ids = colonyIds ? colonyIds.split(',').filter(Boolean) : [];
|
||||||
|
return this.treatmentsService.getWithdrawalStatus(ids);
|
||||||
|
}
|
||||||
|
|
||||||
@RequirePermission('treatments:read')
|
@RequirePermission('treatments:read')
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
|
|||||||
@@ -97,4 +97,58 @@ describe('TreatmentsService', () => {
|
|||||||
expect(queryArg.where.colonyId).toBe('colony-1');
|
expect(queryArg.where.colonyId).toBe('colony-1');
|
||||||
expect(queryArg.orderBy.date).toBe('asc');
|
expect(queryArg.orderBy.date).toBe('asc');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getWithdrawalStatus', () => {
|
||||||
|
it('returns an empty array without querying when no colony ids are given', async () => {
|
||||||
|
const result = await service.getWithdrawalStatus([]);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(prisma.treatment.findMany).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports the treatment whose withdrawal window reaches furthest into the future', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const inTenDays = new Date(now + 10 * 24 * 60 * 60 * 1000);
|
||||||
|
const inFiveDays = new Date(now + 5 * 24 * 60 * 60 * 1000);
|
||||||
|
prisma.treatment.findMany.mockResolvedValue([
|
||||||
|
{
|
||||||
|
colonyId: 'colony-1',
|
||||||
|
date: inTenDays,
|
||||||
|
endDate: null,
|
||||||
|
withdrawalPeriodDays: 0,
|
||||||
|
product: 'Ameisensäure',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colonyId: 'colony-1',
|
||||||
|
date: inFiveDays,
|
||||||
|
endDate: null,
|
||||||
|
withdrawalPeriodDays: 21,
|
||||||
|
product: 'Api Bioxal',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.getWithdrawalStatus(['colony-1']);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].colonyId).toBe('colony-1');
|
||||||
|
expect(result[0].product).toBe('Api Bioxal');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits a colony whose withdrawal window has already passed', async () => {
|
||||||
|
const longAgo = new Date('2020-01-01');
|
||||||
|
prisma.treatment.findMany.mockResolvedValue([
|
||||||
|
{
|
||||||
|
colonyId: 'colony-1',
|
||||||
|
date: longAgo,
|
||||||
|
endDate: null,
|
||||||
|
withdrawalPeriodDays: 14,
|
||||||
|
product: 'Ameisensäure',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.getWithdrawalStatus(['colony-1']);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -6,6 +6,12 @@ import { Treatment } from '../../generated/prisma';
|
|||||||
import { CreateTreatmentDto } from './dto/create-treatment.dto';
|
import { CreateTreatmentDto } from './dto/create-treatment.dto';
|
||||||
import { UpdateTreatmentDto } from './dto/update-treatment.dto';
|
import { UpdateTreatmentDto } from './dto/update-treatment.dto';
|
||||||
|
|
||||||
|
export interface WithdrawalStatus {
|
||||||
|
colonyId: string;
|
||||||
|
activeUntil: string;
|
||||||
|
product: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TreatmentsService {
|
export class TreatmentsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -33,6 +39,44 @@ export class TreatmentsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For each colony, returns the treatment whose withdrawal window (its
|
||||||
|
// end date, or start date if the treatment is still ongoing, plus its
|
||||||
|
// withdrawalPeriodDays) reaches furthest into the future - only when
|
||||||
|
// that window is still active. Colonies with no active window are
|
||||||
|
// omitted rather than returned as null, since most colonies have none.
|
||||||
|
async getWithdrawalStatus(colonyIds: string[]): Promise<WithdrawalStatus[]> {
|
||||||
|
if (colonyIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const treatments = await this.prisma.treatment.findMany({
|
||||||
|
where: {
|
||||||
|
colonyId: { in: colonyIds },
|
||||||
|
withdrawalPeriodDays: { not: null },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const now = new Date();
|
||||||
|
const activeByColony = new Map<string, WithdrawalStatus>();
|
||||||
|
for (const treatment of treatments) {
|
||||||
|
const referenceDate = treatment.endDate ?? treatment.date;
|
||||||
|
const activeUntil = new Date(referenceDate);
|
||||||
|
activeUntil.setUTCDate(
|
||||||
|
activeUntil.getUTCDate() + (treatment.withdrawalPeriodDays ?? 0),
|
||||||
|
);
|
||||||
|
if (activeUntil <= now) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const current = activeByColony.get(treatment.colonyId);
|
||||||
|
if (!current || activeUntil > new Date(current.activeUntil)) {
|
||||||
|
activeByColony.set(treatment.colonyId, {
|
||||||
|
colonyId: treatment.colonyId,
|
||||||
|
activeUntil: activeUntil.toISOString(),
|
||||||
|
product: treatment.product,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...activeByColony.values()];
|
||||||
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
dto: CreateTreatmentDto,
|
dto: CreateTreatmentDto,
|
||||||
actingUserId: string,
|
actingUserId: string,
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export function ColonyTreatmentsCard({ colonyId }: { colonyId: string }) {
|
|||||||
{treatment.method ? ` · ${treatment.method}` : ""}
|
{treatment.method ? ` · ${treatment.method}` : ""}
|
||||||
</p>
|
</p>
|
||||||
{treatment.reason ? <p>Grund: {treatment.reason}</p> : null}
|
{treatment.reason ? <p>Grund: {treatment.reason}</p> : null}
|
||||||
|
{treatment.withdrawalPeriodDays !== null ? (
|
||||||
|
<p>Wartezeit: {treatment.withdrawalPeriodDays} Tag(e)</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
|||||||
const [method, setMethod] = useState(treatment.method ?? "");
|
const [method, setMethod] = useState(treatment.method ?? "");
|
||||||
const [reason, setReason] = useState(treatment.reason ?? "");
|
const [reason, setReason] = useState(treatment.reason ?? "");
|
||||||
const [notes, setNotes] = useState(treatment.notes ?? "");
|
const [notes, setNotes] = useState(treatment.notes ?? "");
|
||||||
|
const [withdrawalPeriodDays, setWithdrawalPeriodDays] = useState(
|
||||||
|
treatment.withdrawalPeriodDays?.toString() ?? "",
|
||||||
|
);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
@@ -36,6 +39,7 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
|||||||
method: method || undefined,
|
method: method || undefined,
|
||||||
reason: reason || undefined,
|
reason: reason || undefined,
|
||||||
notes: notes || undefined,
|
notes: notes || undefined,
|
||||||
|
withdrawalPeriodDays: withdrawalPeriodDays ? Number(withdrawalPeriodDays) : undefined,
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
@@ -107,6 +111,17 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) {
|
|||||||
onChange={(event) => setReason(event.target.value)}
|
onChange={(event) => setReason(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="treatment-edit-withdrawal">Wartezeit (Tage)</Label>
|
||||||
|
<Input
|
||||||
|
id="treatment-edit-withdrawal"
|
||||||
|
type="number"
|
||||||
|
inputMode="numeric"
|
||||||
|
min={0}
|
||||||
|
value={withdrawalPeriodDays}
|
||||||
|
onChange={(event) => setWithdrawalPeriodDays(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="treatment-edit-notes">Notizen</Label>
|
<Label htmlFor="treatment-edit-notes">Notizen</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { FormEvent, useState } from "react";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Boxes } from "lucide-react";
|
import { Boxes, AlertTriangle } from "lucide-react";
|
||||||
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 { createHarvest } from "@/lib/harvests/api";
|
import { createHarvest } from "@/lib/harvests/api";
|
||||||
|
import { fetchWithdrawalStatus } from "@/lib/treatments/api";
|
||||||
import { ApiError } from "@/lib/auth/api-client";
|
import { ApiError } from "@/lib/auth/api-client";
|
||||||
import { queueHarvest } from "@/lib/offline/db";
|
import { queueHarvest } from "@/lib/offline/db";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -47,6 +48,15 @@ export default function NewHarvestPage() {
|
|||||||
const apiaryColonies = (colonies ?? []).filter(
|
const apiaryColonies = (colonies ?? []).filter(
|
||||||
(colony) => colony.apiaryId === apiaryId && colony.status === "ACTIVE",
|
(colony) => colony.apiaryId === apiaryId && colony.status === "ACTIVE",
|
||||||
);
|
);
|
||||||
|
const apiaryColonyIds = apiaryColonies.map((colony) => colony.id);
|
||||||
|
const { data: withdrawalStatuses } = useQuery({
|
||||||
|
queryKey: ["treatments", "withdrawal-status", apiaryColonyIds],
|
||||||
|
queryFn: () => fetchWithdrawalStatus(apiaryColonyIds),
|
||||||
|
enabled: apiaryColonyIds.length > 0,
|
||||||
|
});
|
||||||
|
const withdrawalByColony = new Map(
|
||||||
|
(withdrawalStatuses ?? []).map((status) => [status.colonyId, status]),
|
||||||
|
);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: createHarvest,
|
mutationFn: createHarvest,
|
||||||
@@ -144,22 +154,33 @@ export default function NewHarvestPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{apiaryColonies.map((colony) => (
|
{apiaryColonies.map((colony) => {
|
||||||
<label
|
const withdrawal = withdrawalByColony.get(colony.id);
|
||||||
key={colony.id}
|
return (
|
||||||
className="flex h-11 items-center gap-3 text-sm"
|
<div key={colony.id} className="flex flex-col gap-1">
|
||||||
htmlFor={`harvest-colony-${colony.id}`}
|
<label
|
||||||
>
|
className="flex h-11 items-center gap-3 text-sm"
|
||||||
<input
|
htmlFor={`harvest-colony-${colony.id}`}
|
||||||
id={`harvest-colony-${colony.id}`}
|
>
|
||||||
type="checkbox"
|
<input
|
||||||
className="size-5"
|
id={`harvest-colony-${colony.id}`}
|
||||||
checked={colonyIds.includes(colony.id)}
|
type="checkbox"
|
||||||
onChange={(event) => toggleColony(colony.id, event.target.checked)}
|
className="size-5"
|
||||||
/>
|
checked={colonyIds.includes(colony.id)}
|
||||||
{colony.code}
|
onChange={(event) => toggleColony(colony.id, event.target.checked)}
|
||||||
</label>
|
/>
|
||||||
))}
|
{colony.code}
|
||||||
|
</label>
|
||||||
|
{withdrawal ? (
|
||||||
|
<p className="flex items-center gap-1.5 pl-8 text-xs text-destructive">
|
||||||
|
<AlertTriangle className="size-3.5 shrink-0" />
|
||||||
|
Wartezeit ({withdrawal.product}) bis{" "}
|
||||||
|
{new Date(withdrawal.activeUntil).toLocaleDateString("de-DE")}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default function NewTreatmentPage() {
|
|||||||
const [method, setMethod] = useState("");
|
const [method, setMethod] = useState("");
|
||||||
const [reason, setReason] = useState("");
|
const [reason, setReason] = useState("");
|
||||||
const [notes, setNotes] = useState("");
|
const [notes, setNotes] = useState("");
|
||||||
|
const [withdrawalPeriodDays, setWithdrawalPeriodDays] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
@@ -83,6 +84,7 @@ export default function NewTreatmentPage() {
|
|||||||
method: method || undefined,
|
method: method || undefined,
|
||||||
reason: reason || undefined,
|
reason: reason || undefined,
|
||||||
notes: notes || undefined,
|
notes: notes || undefined,
|
||||||
|
withdrawalPeriodDays: withdrawalPeriodDays ? Number(withdrawalPeriodDays) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +162,22 @@ export default function NewTreatmentPage() {
|
|||||||
onChange={(event) => setReason(event.target.value)}
|
onChange={(event) => setReason(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="treatment-withdrawal">Wartezeit (Tage)</Label>
|
||||||
|
<Input
|
||||||
|
id="treatment-withdrawal"
|
||||||
|
type="number"
|
||||||
|
inputMode="numeric"
|
||||||
|
min={0}
|
||||||
|
className="h-11"
|
||||||
|
value={withdrawalPeriodDays}
|
||||||
|
onChange={(event) => setWithdrawalPeriodDays(event.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Laut Packungsbeilage des Mittels – solange darf aus diesem Volk kein Honig
|
||||||
|
geerntet werden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="treatment-notes">Notizen</Label>
|
<Label htmlFor="treatment-notes">Notizen</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
import { apiFetch } from "@/lib/auth/api-client";
|
import { apiFetch } from "@/lib/auth/api-client";
|
||||||
import { CreateTreatmentInput, Treatment, UpdateTreatmentInput } from "./types";
|
import {
|
||||||
|
CreateTreatmentInput,
|
||||||
|
Treatment,
|
||||||
|
UpdateTreatmentInput,
|
||||||
|
WithdrawalStatus,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export function fetchColonyTreatments(colonyId: string): Promise<Treatment[]> {
|
export function fetchColonyTreatments(colonyId: string): Promise<Treatment[]> {
|
||||||
return apiFetch<Treatment[]>(`/colonies/${colonyId}/treatments`);
|
return apiFetch<Treatment[]>(`/colonies/${colonyId}/treatments`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchWithdrawalStatus(
|
||||||
|
colonyIds: string[],
|
||||||
|
): Promise<WithdrawalStatus[]> {
|
||||||
|
if (colonyIds.length === 0) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
return apiFetch<WithdrawalStatus[]>(
|
||||||
|
`/treatments/withdrawal-status?colonyIds=${colonyIds.join(",")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function createTreatment(input: CreateTreatmentInput): Promise<Treatment> {
|
export function createTreatment(input: CreateTreatmentInput): Promise<Treatment> {
|
||||||
return apiFetch<Treatment>("/treatments", {
|
return apiFetch<Treatment>("/treatments", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface Treatment {
|
|||||||
reason: string | null;
|
reason: string | null;
|
||||||
batchNumber: string | null;
|
batchNumber: string | null;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
withdrawalPeriodDays: number | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ export interface CreateTreatmentInput {
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
batchNumber?: string;
|
batchNumber?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
withdrawalPeriodDays?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateTreatmentInput {
|
export interface UpdateTreatmentInput {
|
||||||
@@ -40,4 +42,11 @@ export interface UpdateTreatmentInput {
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
batchNumber?: string;
|
batchNumber?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
withdrawalPeriodDays?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawalStatus {
|
||||||
|
colonyId: string;
|
||||||
|
activeUntil: string;
|
||||||
|
product: string;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user