feat: Schwarmkontrolle-Protokoll
Neues SwarmControlCheck-Modul: strukturierte Weiselzellenkontrollen pro Volk mit Zahl/Verdeckelungsstatus der Zellen, Schwarmstimmung, ergriffener Maßnahme und optionalem naechsten Kontrolltermin. Frontend: neue Karte auf der Volk-Detailseite, Erfassungsformular unter /swarm-control/new sowie Quick-Add-Eintrag.
This commit is contained in:
1 parent
4609a0f9ec
commit
1a7331b6b2
19 files changed
+1076
No files matched your search
+31
@@ -0,0 +1,31 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SwarmMood" AS ENUM ('NONE', 'LOW', 'HIGH');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SwarmControlAction" AS ENUM ('NONE', 'QUEEN_CELLS_REMOVED', 'ARTIFICIAL_SWARM', 'COLONY_DIVIDED', 'OTHER');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SwarmControlCheck" (
|
||||
"id" TEXT NOT NULL,
|
||||
"colonyId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"date" TIMESTAMP(3) NOT NULL,
|
||||
"queenCellsFound" INTEGER,
|
||||
"queenCellsCapped" BOOLEAN NOT NULL DEFAULT false,
|
||||
"swarmMood" "SwarmMood" NOT NULL DEFAULT 'NONE',
|
||||
"actionTaken" "SwarmControlAction" NOT NULL DEFAULT 'NONE',
|
||||
"notes" TEXT,
|
||||
"nextCheckDate" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "SwarmControlCheck_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SwarmControlCheck_colonyId_date_idx" ON "SwarmControlCheck"("colonyId", "date");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SwarmControlCheck" ADD CONSTRAINT "SwarmControlCheck_colonyId_fkey" FOREIGN KEY ("colonyId") REFERENCES "Colony"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SwarmControlCheck" ADD CONSTRAINT "SwarmControlCheck_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -39,6 +39,7 @@ model User {
|
||||
|
||||
inspections Inspection[]
|
||||
varroaMeasurements VarroaMeasurement[]
|
||||
swarmControlChecks SwarmControlCheck[]
|
||||
treatments Treatment[]
|
||||
feedings Feeding[]
|
||||
harvests Harvest[]
|
||||
@@ -175,6 +176,7 @@ model Colony {
|
||||
lineageAsResult ColonyLineage[] @relation("LineageTo")
|
||||
inspections Inspection[]
|
||||
varroaMeasurements VarroaMeasurement[]
|
||||
swarmControlChecks SwarmControlCheck[]
|
||||
treatments Treatment[]
|
||||
feedings Feeding[]
|
||||
harvests HarvestColony[]
|
||||
@@ -457,6 +459,43 @@ model VarroaMeasurement {
|
||||
@@index([colonyId, date])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schwarmkontrolle (jenseits Lastenheft: strukturiertes Protokoll für
|
||||
// Weiselzellenkontrollen während der Schwarmzeit)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum SwarmMood {
|
||||
NONE
|
||||
LOW
|
||||
HIGH
|
||||
}
|
||||
|
||||
enum SwarmControlAction {
|
||||
NONE
|
||||
QUEEN_CELLS_REMOVED
|
||||
ARTIFICIAL_SWARM
|
||||
COLONY_DIVIDED
|
||||
OTHER
|
||||
}
|
||||
|
||||
model SwarmControlCheck {
|
||||
id String @id @default(cuid())
|
||||
colonyId String
|
||||
colony Colony @relation(fields: [colonyId], references: [id])
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
date DateTime
|
||||
queenCellsFound Int?
|
||||
queenCellsCapped Boolean @default(false)
|
||||
swarmMood SwarmMood @default(NONE)
|
||||
actionTaken SwarmControlAction @default(NONE)
|
||||
notes String?
|
||||
nextCheckDate DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([colonyId, date])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Behandlung / Tierarzneimittel (Abschnitt 4.11)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -34,6 +34,7 @@ import { AutomationModule } from './automation/automation.module';
|
||||
import { ImportModule } from './import/import.module';
|
||||
import { PublicModule } from './public/public.module';
|
||||
import { TaskTemplatesModule } from './task-templates/task-templates.module';
|
||||
import { SwarmControlModule } from './swarm-control/swarm-control.module';
|
||||
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from './auth/guards/roles.guard';
|
||||
|
||||
@@ -73,6 +74,7 @@ import { RolesGuard } from './auth/guards/roles.guard';
|
||||
ImportModule,
|
||||
PublicModule,
|
||||
TaskTemplatesModule,
|
||||
SwarmControlModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { SwarmControlService } from './swarm-control.service';
|
||||
|
||||
@ApiTags('swarm-control-checks')
|
||||
@ApiBearerAuth()
|
||||
@Controller('colonies')
|
||||
export class ColonySwarmControlChecksController {
|
||||
constructor(private readonly swarmControlService: SwarmControlService) {}
|
||||
|
||||
@Get(':id/swarm-control-checks')
|
||||
findByColony(@Param('id') id: string) {
|
||||
return this.swarmControlService.findByColony(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
import { SwarmControlAction, SwarmMood } from '../../../generated/prisma';
|
||||
|
||||
export class CreateSwarmControlCheckDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
colonyId!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsDateString()
|
||||
date!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
queenCellsFound?: number;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
queenCellsCapped?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ enum: SwarmMood })
|
||||
@IsOptional()
|
||||
@IsEnum(SwarmMood)
|
||||
swarmMood?: SwarmMood;
|
||||
|
||||
@ApiPropertyOptional({ enum: SwarmControlAction })
|
||||
@IsOptional()
|
||||
@IsEnum(SwarmControlAction)
|
||||
actionTaken?: SwarmControlAction;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
nextCheckDate?: string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { PartialType, PickType } from '@nestjs/swagger';
|
||||
import { CreateSwarmControlCheckDto } from './create-swarm-control-check.dto';
|
||||
|
||||
export class UpdateSwarmControlCheckDto extends PartialType(
|
||||
PickType(CreateSwarmControlCheckDto, [
|
||||
'date',
|
||||
'queenCellsFound',
|
||||
'queenCellsCapped',
|
||||
'swarmMood',
|
||||
'actionTaken',
|
||||
'notes',
|
||||
'nextCheckDate',
|
||||
] as const),
|
||||
) {}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { Role } from '../../generated/prisma';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
||||
import { CreateSwarmControlCheckDto } from './dto/create-swarm-control-check.dto';
|
||||
import { UpdateSwarmControlCheckDto } from './dto/update-swarm-control-check.dto';
|
||||
import { SwarmControlService } from './swarm-control.service';
|
||||
|
||||
@ApiTags('swarm-control-checks')
|
||||
@ApiBearerAuth()
|
||||
@Controller('swarm-control-checks')
|
||||
export class SwarmControlController {
|
||||
constructor(private readonly swarmControlService: SwarmControlService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.swarmControlService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.swarmControlService.findById(id);
|
||||
}
|
||||
|
||||
@Roles(Role.ADMIN, Role.BEEKEEPER)
|
||||
@Post()
|
||||
create(
|
||||
@Body() dto: CreateSwarmControlCheckDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
) {
|
||||
return this.swarmControlService.create(dto, actor.id);
|
||||
}
|
||||
|
||||
@Roles(Role.ADMIN, Role.BEEKEEPER)
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateSwarmControlCheckDto,
|
||||
@CurrentUser() actor: AuthenticatedUser,
|
||||
) {
|
||||
return this.swarmControlService.update(id, dto, actor.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SwarmControlController } from './swarm-control.controller';
|
||||
import { ColonySwarmControlChecksController } from './colony-swarm-control-checks.controller';
|
||||
import { SwarmControlService } from './swarm-control.service';
|
||||
|
||||
@Module({
|
||||
controllers: [SwarmControlController, ColonySwarmControlChecksController],
|
||||
providers: [SwarmControlService],
|
||||
exports: [SwarmControlService],
|
||||
})
|
||||
export class SwarmControlModule {}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { SwarmControlService } from './swarm-control.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
|
||||
describe('SwarmControlService', () => {
|
||||
let service: SwarmControlService;
|
||||
let prisma: {
|
||||
swarmControlCheck: {
|
||||
findMany: jest.Mock;
|
||||
findUnique: jest.Mock;
|
||||
create: jest.Mock;
|
||||
update: jest.Mock;
|
||||
};
|
||||
};
|
||||
let auditLog: { record: jest.Mock };
|
||||
|
||||
const check = {
|
||||
id: 'check-1',
|
||||
colonyId: 'colony-1',
|
||||
date: new Date('2026-05-01T00:00:00.000Z'),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
prisma = {
|
||||
swarmControlCheck: {
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
};
|
||||
auditLog = { record: jest.fn() };
|
||||
service = new SwarmControlService(
|
||||
prisma as unknown as PrismaService,
|
||||
auditLog as unknown as AuditLogService,
|
||||
);
|
||||
});
|
||||
|
||||
it('findById throws NotFoundException when missing', async () => {
|
||||
prisma.swarmControlCheck.findUnique.mockResolvedValue(null);
|
||||
await expect(service.findById('missing')).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
|
||||
it('create stores the acting user and converts date fields', async () => {
|
||||
prisma.swarmControlCheck.create.mockResolvedValue(check);
|
||||
|
||||
await service.create(
|
||||
{
|
||||
colonyId: 'colony-1',
|
||||
date: '2026-05-01',
|
||||
nextCheckDate: '2026-05-08',
|
||||
},
|
||||
'user-1',
|
||||
);
|
||||
|
||||
const [[createArg]] = prisma.swarmControlCheck.create.mock.calls as [
|
||||
[
|
||||
{
|
||||
data: {
|
||||
colonyId: string;
|
||||
userId: string;
|
||||
date: Date;
|
||||
nextCheckDate: Date;
|
||||
};
|
||||
},
|
||||
],
|
||||
];
|
||||
expect(createArg.data.colonyId).toBe('colony-1');
|
||||
expect(createArg.data.userId).toBe('user-1');
|
||||
expect(createArg.data.date).toBeInstanceOf(Date);
|
||||
expect(createArg.data.nextCheckDate).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('update writes an audit log entry', async () => {
|
||||
prisma.swarmControlCheck.findUnique.mockResolvedValue(check);
|
||||
const updated = { ...check, queenCellsFound: 3, queenCellsCapped: true };
|
||||
prisma.swarmControlCheck.update.mockResolvedValue(updated);
|
||||
|
||||
const result = await service.update(
|
||||
'check-1',
|
||||
{ queenCellsFound: 3, queenCellsCapped: true },
|
||||
'user-1',
|
||||
);
|
||||
|
||||
expect(result).toBe(updated);
|
||||
expect(auditLog.record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
entityType: 'SwarmControlCheck',
|
||||
entityId: 'check-1',
|
||||
action: 'UPDATE',
|
||||
beforeData: check,
|
||||
afterData: updated,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('findByColony orders results chronologically', async () => {
|
||||
prisma.swarmControlCheck.findMany.mockResolvedValue([check]);
|
||||
|
||||
await service.findByColony('colony-1');
|
||||
|
||||
const [[queryArg]] = prisma.swarmControlCheck.findMany.mock.calls as [
|
||||
[{ where: { colonyId: string }; orderBy: { date: string } }],
|
||||
];
|
||||
expect(queryArg.where.colonyId).toBe('colony-1');
|
||||
expect(queryArg.orderBy.date).toBe('asc');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { SwarmControlCheck } from '../../generated/prisma';
|
||||
import { CreateSwarmControlCheckDto } from './dto/create-swarm-control-check.dto';
|
||||
import { UpdateSwarmControlCheckDto } from './dto/update-swarm-control-check.dto';
|
||||
|
||||
@Injectable()
|
||||
export class SwarmControlService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly auditLog: AuditLogService,
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<SwarmControlCheck[]> {
|
||||
return this.prisma.swarmControlCheck.findMany({
|
||||
orderBy: { date: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<SwarmControlCheck> {
|
||||
const check = await this.prisma.swarmControlCheck.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
if (!check) {
|
||||
throw new NotFoundException('Swarm control check not found');
|
||||
}
|
||||
return check;
|
||||
}
|
||||
|
||||
async findByColony(colonyId: string): Promise<SwarmControlCheck[]> {
|
||||
return this.prisma.swarmControlCheck.findMany({
|
||||
where: { colonyId },
|
||||
orderBy: { date: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async create(
|
||||
dto: CreateSwarmControlCheckDto,
|
||||
actingUserId: string,
|
||||
): Promise<SwarmControlCheck> {
|
||||
const check = await this.prisma.swarmControlCheck.create({
|
||||
data: {
|
||||
...dto,
|
||||
date: new Date(dto.date),
|
||||
nextCheckDate: dto.nextCheckDate
|
||||
? new Date(dto.nextCheckDate)
|
||||
: undefined,
|
||||
userId: actingUserId,
|
||||
},
|
||||
});
|
||||
await this.auditLog.record({
|
||||
entityType: 'SwarmControlCheck',
|
||||
entityId: check.id,
|
||||
action: 'CREATE',
|
||||
userId: actingUserId,
|
||||
afterData: check,
|
||||
});
|
||||
return check;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
dto: UpdateSwarmControlCheckDto,
|
||||
actingUserId: string,
|
||||
): Promise<SwarmControlCheck> {
|
||||
const before = await this.findById(id);
|
||||
const check = await this.prisma.swarmControlCheck.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...dto,
|
||||
date: dto.date ? new Date(dto.date) : undefined,
|
||||
nextCheckDate: dto.nextCheckDate
|
||||
? new Date(dto.nextCheckDate)
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
await this.auditLog.record({
|
||||
entityType: 'SwarmControlCheck',
|
||||
entityId: id,
|
||||
action: 'UPDATE',
|
||||
userId: actingUserId,
|
||||
beforeData: before,
|
||||
afterData: check,
|
||||
});
|
||||
return check;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { PrismaService } from '../src/prisma/prisma.service';
|
||||
import { createTestApp } from './utils/create-test-app';
|
||||
import { login } from './utils/api';
|
||||
|
||||
interface SwarmControlCheckBody {
|
||||
id: string;
|
||||
colonyId: string;
|
||||
queenCellsCapped: boolean;
|
||||
}
|
||||
|
||||
describe('Swarm control checks (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
let prisma: PrismaService;
|
||||
const beekeeperEmail = `swarm-control-e2e-beekeeper-${Date.now()}@example.com`;
|
||||
const staffEmail = `swarm-control-e2e-staff-${Date.now()}@example.com`;
|
||||
const password = 'SuperSecret123!';
|
||||
let beekeeperId: string;
|
||||
let staffId: string;
|
||||
let beekeeperToken: string;
|
||||
let staffToken: string;
|
||||
let apiaryId: string;
|
||||
let colonyId: string;
|
||||
let checkId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp();
|
||||
prisma = app.get(PrismaService);
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
const beekeeper = await prisma.user.create({
|
||||
data: {
|
||||
email: beekeeperEmail,
|
||||
passwordHash,
|
||||
name: 'Beekeeper',
|
||||
role: 'BEEKEEPER',
|
||||
},
|
||||
});
|
||||
beekeeperId = beekeeper.id;
|
||||
const staff = await prisma.user.create({
|
||||
data: { email: staffEmail, passwordHash, name: 'Staff', role: 'STAFF' },
|
||||
});
|
||||
staffId = staff.id;
|
||||
beekeeperToken = (await login(app, beekeeperEmail, password)).accessToken;
|
||||
staffToken = (await login(app, staffEmail, password)).accessToken;
|
||||
|
||||
const apiary = await prisma.apiary.create({
|
||||
data: { name: `Stand ${Date.now()}` },
|
||||
});
|
||||
apiaryId = apiary.id;
|
||||
const colony = await prisma.colony.create({
|
||||
data: {
|
||||
code: `V-${Date.now()}`,
|
||||
apiaryId,
|
||||
colonyType: 'PRODUCTION_COLONY',
|
||||
},
|
||||
});
|
||||
colonyId = colony.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.swarmControlCheck.deleteMany({ where: { colonyId } });
|
||||
await prisma.colony.deleteMany({ where: { id: colonyId } });
|
||||
await prisma.apiary.deleteMany({ where: { id: apiaryId } });
|
||||
await prisma.refreshToken.deleteMany({
|
||||
where: { userId: { in: [beekeeperId, staffId] } },
|
||||
});
|
||||
await prisma.auditLog.deleteMany({
|
||||
where: { userId: { in: [beekeeperId, staffId] } },
|
||||
});
|
||||
await prisma.user.deleteMany({
|
||||
where: { id: { in: [beekeeperId, staffId] } },
|
||||
});
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('rejects create for a STAFF user', () => {
|
||||
return request(app.getHttpServer())
|
||||
.post('/api/v1/swarm-control-checks')
|
||||
.set('Authorization', `Bearer ${staffToken}`)
|
||||
.send({ colonyId, date: new Date().toISOString() })
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('lets a BEEKEEPER user record a check', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/api/v1/swarm-control-checks')
|
||||
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||
.send({
|
||||
colonyId,
|
||||
date: new Date().toISOString(),
|
||||
queenCellsFound: 2,
|
||||
queenCellsCapped: false,
|
||||
swarmMood: 'LOW',
|
||||
})
|
||||
.expect(201);
|
||||
checkId = (response.body as SwarmControlCheckBody).id;
|
||||
expect(checkId).toEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it('lists checks for the colony', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v1/colonies/${colonyId}/swarm-control-checks`)
|
||||
.set('Authorization', `Bearer ${staffToken}`)
|
||||
.expect(200);
|
||||
const checks = response.body as SwarmControlCheckBody[];
|
||||
expect(checks.some((entry) => entry.id === checkId)).toBe(true);
|
||||
});
|
||||
|
||||
it('updates a check to record capped queen cells', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/api/v1/swarm-control-checks/${checkId}`)
|
||||
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||
.send({
|
||||
queenCellsCapped: true,
|
||||
actionTaken: 'ARTIFICIAL_SWARM',
|
||||
nextCheckDate: new Date().toISOString(),
|
||||
})
|
||||
.expect(200);
|
||||
expect((response.body as SwarmControlCheckBody).queenCellsCapped).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Sprout } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchColonySwarmControlChecks } from "@/lib/swarm-control/api";
|
||||
import { SWARM_ACTION_LABELS, SWARM_MOOD_LABELS } from "@/lib/swarm-control/labels";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { SwarmControlEditDialog } from "./swarm-control-edit-dialog";
|
||||
|
||||
export function ColonySwarmControlCard({ colonyId }: { colonyId: string }) {
|
||||
const { data: checks, isLoading } = useQuery({
|
||||
queryKey: ["colonies", colonyId, "swarm-control-checks"],
|
||||
queryFn: () => fetchColonySwarmControlChecks(colonyId),
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="max-w-lg">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Schwarmkontrolle</CardTitle>
|
||||
<Button
|
||||
size="sm"
|
||||
nativeButton={false}
|
||||
render={<Link href={`/swarm-control/new?colonyId=${colonyId}`} />}
|
||||
>
|
||||
Neue Kontrolle
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3 text-sm">
|
||||
{isLoading ? <p className="text-muted-foreground">Lädt…</p> : null}
|
||||
{!isLoading && (!checks || checks.length === 0) ? (
|
||||
<EmptyState
|
||||
icon={Sprout}
|
||||
title="Noch keine Schwarmkontrolle"
|
||||
description="Erfasse die erste Weiselzellenkontrolle für dieses Volk."
|
||||
/>
|
||||
) : null}
|
||||
{checks?.map((check) => (
|
||||
<div key={check.id} className="rounded-md border p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-medium">{new Date(check.date).toLocaleDateString("de-DE")}</p>
|
||||
<SwarmControlEditDialog check={check} />
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
{check.queenCellsFound !== null
|
||||
? `${check.queenCellsFound} Weiselzelle(n) gefunden`
|
||||
: "Keine Weiselzellen erfasst"}
|
||||
{check.queenCellsCapped ? (
|
||||
<Badge variant="destructive" className="ml-2">
|
||||
Verdeckelt
|
||||
</Badge>
|
||||
) : null}
|
||||
</p>
|
||||
<p>Schwarmstimmung: {SWARM_MOOD_LABELS[check.swarmMood]}</p>
|
||||
{check.actionTaken !== "NONE" ? (
|
||||
<p>Maßnahme: {SWARM_ACTION_LABELS[check.actionTaken]}</p>
|
||||
) : null}
|
||||
{check.nextCheckDate ? (
|
||||
<p>Nächste Kontrolle: {new Date(check.nextCheckDate).toLocaleDateString("de-DE")}</p>
|
||||
) : null}
|
||||
{check.notes ? <p>{check.notes}</p> : null}
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ColonyActions } from "./colony-actions";
|
||||
import { ColonyHistoryCard } from "./colony-history";
|
||||
import { ColonyInspectionsCard } from "./colony-inspections";
|
||||
import { ColonyVarroaCard } from "./colony-varroa";
|
||||
import { ColonySwarmControlCard } from "./colony-swarm-control";
|
||||
import { ColonyTreatmentsCard } from "./colony-treatments";
|
||||
import { ColonyFeedingsCard } from "./colony-feedings";
|
||||
|
||||
@@ -40,6 +41,7 @@ export default function ColonyDetailPage() {
|
||||
<ColonyActions colony={colony} />
|
||||
<ColonyInspectionsCard colonyId={colony.id} />
|
||||
<ColonyVarroaCard colonyId={colony.id} />
|
||||
<ColonySwarmControlCard colonyId={colony.id} />
|
||||
<ColonyTreatmentsCard colonyId={colony.id} />
|
||||
<ColonyFeedingsCard colonyId={colony.id} />
|
||||
<ColonyHistoryCard colonyId={colony.id} />
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { updateSwarmControlCheck } from "@/lib/swarm-control/api";
|
||||
import { SWARM_ACTION_LABELS, SWARM_MOOD_LABELS } from "@/lib/swarm-control/labels";
|
||||
import { SwarmControlAction, SwarmControlCheck, SwarmMood } from "@/lib/swarm-control/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,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export function SwarmControlEditDialog({ check }: { check: SwarmControlCheck }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [date, setDate] = useState(check.date.slice(0, 10));
|
||||
const [queenCellsFound, setQueenCellsFound] = useState(check.queenCellsFound?.toString() ?? "");
|
||||
const [queenCellsCapped, setQueenCellsCapped] = useState(check.queenCellsCapped);
|
||||
const [swarmMood, setSwarmMood] = useState<SwarmMood>(check.swarmMood);
|
||||
const [actionTaken, setActionTaken] = useState<SwarmControlAction>(check.actionTaken);
|
||||
const [nextCheckDate, setNextCheckDate] = useState(check.nextCheckDate?.slice(0, 10) ?? "");
|
||||
const [notes, setNotes] = useState(check.notes ?? "");
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateSwarmControlCheck(check.id, {
|
||||
date: new Date(date).toISOString(),
|
||||
queenCellsFound: queenCellsFound ? Number(queenCellsFound) : undefined,
|
||||
queenCellsCapped,
|
||||
swarmMood,
|
||||
actionTaken,
|
||||
nextCheckDate: nextCheckDate ? new Date(nextCheckDate).toISOString() : undefined,
|
||||
notes: notes || undefined,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", check.colonyId, "swarm-control-checks"],
|
||||
});
|
||||
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>Schwarmkontrolle bearbeiten</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-edit-date">Datum</Label>
|
||||
<Input
|
||||
id="swarm-edit-date"
|
||||
type="date"
|
||||
value={date}
|
||||
onChange={(event) => setDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-edit-cells-found">Weiselzellen gefunden</Label>
|
||||
<Input
|
||||
id="swarm-edit-cells-found"
|
||||
type="number"
|
||||
min={0}
|
||||
value={queenCellsFound}
|
||||
onChange={(event) => setQueenCellsFound(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<label
|
||||
className="flex h-9 items-center gap-3 text-sm"
|
||||
htmlFor="swarm-edit-cells-capped"
|
||||
>
|
||||
<input
|
||||
id="swarm-edit-cells-capped"
|
||||
type="checkbox"
|
||||
className="size-4.5"
|
||||
checked={queenCellsCapped}
|
||||
onChange={(event) => setQueenCellsCapped(event.target.checked)}
|
||||
/>
|
||||
Weiselzellen bereits verdeckelt (akut)
|
||||
</label>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-edit-mood">Schwarmstimmung</Label>
|
||||
<Select
|
||||
value={swarmMood}
|
||||
onValueChange={(value) => setSwarmMood((value ?? swarmMood) as SwarmMood)}
|
||||
>
|
||||
<SelectTrigger id="swarm-edit-mood" className="w-full">
|
||||
<SelectValue>{SWARM_MOOD_LABELS[swarmMood]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(SWARM_MOOD_LABELS) as SwarmMood[]).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{SWARM_MOOD_LABELS[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-edit-action">Maßnahme</Label>
|
||||
<Select
|
||||
value={actionTaken}
|
||||
onValueChange={(value) =>
|
||||
setActionTaken((value ?? actionTaken) as SwarmControlAction)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="swarm-edit-action" className="w-full">
|
||||
<SelectValue>{SWARM_ACTION_LABELS[actionTaken]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(SWARM_ACTION_LABELS) as SwarmControlAction[]).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{SWARM_ACTION_LABELS[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-edit-next-check">Nächste Kontrolle</Label>
|
||||
<Input
|
||||
id="swarm-edit-next-check"
|
||||
type="date"
|
||||
value={nextCheckDate}
|
||||
onChange={(event) => setNextCheckDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-edit-notes">Notizen</Label>
|
||||
<Textarea
|
||||
id="swarm-edit-notes"
|
||||
value={notes}
|
||||
onChange={(event) => setNotes(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? "Speichert…" : "Speichern"}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
const QUICK_ACTIONS = [
|
||||
{ label: "Durchsicht", href: "/inspections/new" },
|
||||
{ label: "Varroa-Messung", href: "/varroa/new" },
|
||||
{ label: "Schwarmkontrolle", href: "/swarm-control/new" },
|
||||
{ label: "Behandlung", href: "/treatments/new" },
|
||||
{ label: "Fütterung", href: "/feedings/new" },
|
||||
{ label: "Aufgabe", href: "/tasks/new" },
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Sprout } from "lucide-react";
|
||||
import { fetchColonies } from "@/lib/colonies/api";
|
||||
import { createSwarmControlCheck } from "@/lib/swarm-control/api";
|
||||
import { SWARM_ACTION_LABELS, SWARM_MOOD_LABELS } from "@/lib/swarm-control/labels";
|
||||
import { SwarmControlAction, SwarmMood } from "@/lib/swarm-control/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 { PageHeader } from "@/components/page-header";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
function todayIsoDate(): string {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export default function NewSwarmControlCheckPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: colonies } = useQuery({
|
||||
queryKey: ["colonies"],
|
||||
queryFn: () => fetchColonies(),
|
||||
});
|
||||
const activeColonies = colonies?.filter((colony) => colony.status === "ACTIVE") ?? [];
|
||||
|
||||
const [colonyId, setColonyId] = useState(searchParams.get("colonyId") ?? "");
|
||||
const [date, setDate] = useState(todayIsoDate());
|
||||
const [queenCellsFound, setQueenCellsFound] = useState("");
|
||||
const [queenCellsCapped, setQueenCellsCapped] = useState(false);
|
||||
const [swarmMood, setSwarmMood] = useState<SwarmMood>("NONE");
|
||||
const [actionTaken, setActionTaken] = useState<SwarmControlAction>("NONE");
|
||||
const [nextCheckDate, setNextCheckDate] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: createSwarmControlCheck,
|
||||
onSuccess: (check) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", check.colonyId, "swarm-control-checks"],
|
||||
});
|
||||
toast.success("Schwarmkontrolle gespeichert");
|
||||
router.push(`/colonies/${check.colonyId}`);
|
||||
},
|
||||
onError: () => {
|
||||
setError("Schwarmkontrolle konnte nicht gespeichert werden.");
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
if (!colonyId) {
|
||||
setError("Bitte ein Volk auswählen.");
|
||||
return;
|
||||
}
|
||||
mutation.mutate({
|
||||
colonyId,
|
||||
date: new Date(date).toISOString(),
|
||||
queenCellsFound: queenCellsFound ? Number(queenCellsFound) : undefined,
|
||||
queenCellsCapped,
|
||||
swarmMood,
|
||||
actionTaken,
|
||||
nextCheckDate: nextCheckDate ? new Date(nextCheckDate).toISOString() : undefined,
|
||||
notes: notes || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-4 p-4 sm:p-6">
|
||||
<PageHeader
|
||||
icon={Sprout}
|
||||
title="Neue Schwarmkontrolle"
|
||||
description="Erfasse eine Weiselzellenkontrolle für ein Volk."
|
||||
gradient="var(--gradient-sky)"
|
||||
/>
|
||||
<Card className="max-w-lg">
|
||||
<CardContent>
|
||||
<form className="flex flex-col gap-5" onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-colony">Volk</Label>
|
||||
<Select value={colonyId} onValueChange={(value) => setColonyId(value ?? "")}>
|
||||
<SelectTrigger id="swarm-colony" className="h-11 w-full">
|
||||
<SelectValue placeholder="Volk wählen" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{activeColonies.map((colony) => (
|
||||
<SelectItem key={colony.id} value={colony.id}>
|
||||
{colony.code}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-date">Datum</Label>
|
||||
<Input
|
||||
id="swarm-date"
|
||||
type="date"
|
||||
required
|
||||
className="h-11"
|
||||
value={date}
|
||||
onChange={(event) => setDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-cells-found">Weiselzellen gefunden</Label>
|
||||
<Input
|
||||
id="swarm-cells-found"
|
||||
type="number"
|
||||
min={0}
|
||||
inputMode="numeric"
|
||||
className="h-11"
|
||||
value={queenCellsFound}
|
||||
onChange={(event) => setQueenCellsFound(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex h-9 items-center gap-3 text-sm" htmlFor="swarm-cells-capped">
|
||||
<input
|
||||
id="swarm-cells-capped"
|
||||
type="checkbox"
|
||||
className="size-4.5"
|
||||
checked={queenCellsCapped}
|
||||
onChange={(event) => setQueenCellsCapped(event.target.checked)}
|
||||
/>
|
||||
Weiselzellen bereits verdeckelt (akut)
|
||||
</label>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-mood">Schwarmstimmung</Label>
|
||||
<Select
|
||||
value={swarmMood}
|
||||
onValueChange={(value) => setSwarmMood((value ?? swarmMood) as SwarmMood)}
|
||||
>
|
||||
<SelectTrigger id="swarm-mood" className="h-11 w-full">
|
||||
<SelectValue>{SWARM_MOOD_LABELS[swarmMood]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(SWARM_MOOD_LABELS) as SwarmMood[]).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{SWARM_MOOD_LABELS[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-action">Maßnahme</Label>
|
||||
<Select
|
||||
value={actionTaken}
|
||||
onValueChange={(value) =>
|
||||
setActionTaken((value ?? actionTaken) as SwarmControlAction)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="swarm-action" className="h-11 w-full">
|
||||
<SelectValue>{SWARM_ACTION_LABELS[actionTaken]}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(SWARM_ACTION_LABELS) as SwarmControlAction[]).map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{SWARM_ACTION_LABELS[option]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-next-check">Nächste Kontrolle</Label>
|
||||
<Input
|
||||
id="swarm-next-check"
|
||||
type="date"
|
||||
className="h-11"
|
||||
value={nextCheckDate}
|
||||
onChange={(event) => setNextCheckDate(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="swarm-notes">Notizen</Label>
|
||||
<Textarea
|
||||
id="swarm-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" className="h-12 text-base" disabled={mutation.isPending}>
|
||||
{mutation.isPending ? "Wird gespeichert…" : "Kontrolle speichern"}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { apiFetch } from "@/lib/auth/api-client";
|
||||
import {
|
||||
CreateSwarmControlCheckInput,
|
||||
SwarmControlCheck,
|
||||
UpdateSwarmControlCheckInput,
|
||||
} from "./types";
|
||||
|
||||
export function fetchColonySwarmControlChecks(colonyId: string): Promise<SwarmControlCheck[]> {
|
||||
return apiFetch<SwarmControlCheck[]>(`/colonies/${colonyId}/swarm-control-checks`);
|
||||
}
|
||||
|
||||
export function createSwarmControlCheck(
|
||||
input: CreateSwarmControlCheckInput,
|
||||
): Promise<SwarmControlCheck> {
|
||||
return apiFetch<SwarmControlCheck>("/swarm-control-checks", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function updateSwarmControlCheck(
|
||||
id: string,
|
||||
input: UpdateSwarmControlCheckInput,
|
||||
): Promise<SwarmControlCheck> {
|
||||
return apiFetch<SwarmControlCheck>(`/swarm-control-checks/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { SwarmControlAction, SwarmMood } from "./types";
|
||||
|
||||
export const SWARM_MOOD_LABELS: Record<SwarmMood, string> = {
|
||||
NONE: "Keine",
|
||||
LOW: "Gering",
|
||||
HIGH: "Hoch",
|
||||
};
|
||||
|
||||
export const SWARM_ACTION_LABELS: Record<SwarmControlAction, string> = {
|
||||
NONE: "Keine",
|
||||
QUEEN_CELLS_REMOVED: "Weiselzellen gebrochen",
|
||||
ARTIFICIAL_SWARM: "Kunstschwarm gebildet",
|
||||
COLONY_DIVIDED: "Volk geteilt",
|
||||
OTHER: "Sonstiges",
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
export type SwarmMood = "NONE" | "LOW" | "HIGH";
|
||||
export type SwarmControlAction =
|
||||
| "NONE"
|
||||
| "QUEEN_CELLS_REMOVED"
|
||||
| "ARTIFICIAL_SWARM"
|
||||
| "COLONY_DIVIDED"
|
||||
| "OTHER";
|
||||
|
||||
export interface SwarmControlCheck {
|
||||
id: string;
|
||||
colonyId: string;
|
||||
userId: string;
|
||||
date: string;
|
||||
queenCellsFound: number | null;
|
||||
queenCellsCapped: boolean;
|
||||
swarmMood: SwarmMood;
|
||||
actionTaken: SwarmControlAction;
|
||||
notes: string | null;
|
||||
nextCheckDate: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateSwarmControlCheckInput {
|
||||
colonyId: string;
|
||||
date: string;
|
||||
queenCellsFound?: number;
|
||||
queenCellsCapped?: boolean;
|
||||
swarmMood?: SwarmMood;
|
||||
actionTaken?: SwarmControlAction;
|
||||
notes?: string;
|
||||
nextCheckDate?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSwarmControlCheckInput {
|
||||
date?: string;
|
||||
queenCellsFound?: number;
|
||||
queenCellsCapped?: boolean;
|
||||
swarmMood?: SwarmMood;
|
||||
actionTaken?: SwarmControlAction;
|
||||
notes?: string;
|
||||
nextCheckDate?: string;
|
||||
}
|
||||
Reference in new issue
Block a user