feat(backend): Durchsichten-Modul (Modul 5)

InspectionsModule mit CRUD, Colony-Historie-Endpunkt und
Folgeaufgaben-Erstellung aus einer Durchsicht heraus.
This commit is contained in:
Nick Szaunig committed 2026-08-21 18:43:08 +02:00
1 parent 49ce23147c
commit 9b59e4bb35
10 files changed
+489 -1

No files matched your search

+2
View File
@@ -9,6 +9,7 @@ import { AuthModule } from './auth/auth.module';
import { ApiariesModule } from './apiaries/apiaries.module';
import { ColoniesModule } from './colonies/colonies.module';
import { QueensModule } from './queens/queens.module';
import { InspectionsModule } from './inspections/inspections.module';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';
import { RolesGuard } from './auth/guards/roles.guard';
@@ -23,6 +24,7 @@ import { RolesGuard } from './auth/guards/roles.guard';
ApiariesModule,
ColoniesModule,
QueensModule,
InspectionsModule,
],
controllers: [],
providers: [
@@ -0,0 +1,15 @@
import { Controller, Get, Param } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { InspectionsService } from './inspections.service';
@ApiTags('inspections')
@ApiBearerAuth()
@Controller('colonies')
export class ColonyInspectionsController {
constructor(private readonly inspectionsService: InspectionsService) {}
@Get(':id/inspections')
findByColony(@Param('id') id: string) {
return this.inspectionsService.findByColony(id);
}
}
@@ -0,0 +1,114 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsBoolean,
IsDateString,
IsInt,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
export class CreateInspectionDto {
@ApiProperty()
@IsString()
colonyId!: string;
@ApiProperty()
@IsDateString()
date!: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
weather?: string;
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
temperature?: number;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
colonyStrength?: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
broodAmount?: string;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
broodPattern?: number;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
openBroodPresent?: boolean;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
cappedBroodPresent?: boolean;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
eggsPresent?: boolean;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
queenSeen?: boolean;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
queenCellsPresent?: boolean;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
swarmMood?: number;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
gentleness?: number;
@ApiPropertyOptional()
@IsOptional()
@IsInt()
combBehavior?: number;
@ApiPropertyOptional()
@IsOptional()
@IsString()
feedStatus?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
honeySuperStatus?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
healthIssues?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
varroaIssues?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
measures?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
notes?: string;
}
@@ -0,0 +1,29 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsDateString, IsEnum, IsOptional, IsString } from 'class-validator';
import { TaskPriority } from '../../../generated/prisma';
export class CreateTaskFromInspectionDto {
@ApiProperty()
@IsString()
title!: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
@ApiPropertyOptional({ enum: TaskPriority })
@IsOptional()
@IsEnum(TaskPriority)
priority?: TaskPriority;
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
dueDate?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
assignedUserId?: string;
}
@@ -0,0 +1,45 @@
import { Body, Controller, Get, Param, 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 { CreateInspectionDto } from './dto/create-inspection.dto';
import { CreateTaskFromInspectionDto } from './dto/create-task-from-inspection.dto';
import { InspectionsService } from './inspections.service';
@ApiTags('inspections')
@ApiBearerAuth()
@Controller('inspections')
export class InspectionsController {
constructor(private readonly inspectionsService: InspectionsService) {}
@Get()
findAll() {
return this.inspectionsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.inspectionsService.findById(id);
}
@Roles(Role.ADMIN, Role.BEEKEEPER, Role.STAFF)
@Post()
create(
@Body() dto: CreateInspectionDto,
@CurrentUser() actor: AuthenticatedUser,
) {
return this.inspectionsService.create(dto, actor.id);
}
@Roles(Role.ADMIN, Role.BEEKEEPER, Role.STAFF)
@Post(':id/create-task')
createTask(
@Param('id') id: string,
@Body() dto: CreateTaskFromInspectionDto,
@CurrentUser() actor: AuthenticatedUser,
) {
return this.inspectionsService.createTaskFromInspection(id, dto, actor.id);
}
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { InspectionsController } from './inspections.controller';
import { ColonyInspectionsController } from './colony-inspections.controller';
import { InspectionsService } from './inspections.service';
@Module({
controllers: [InspectionsController, ColonyInspectionsController],
providers: [InspectionsService],
exports: [InspectionsService],
})
export class InspectionsModule {}
@@ -0,0 +1,81 @@
import { NotFoundException } from '@nestjs/common';
import { InspectionsService } from './inspections.service';
import { PrismaService } from '../prisma/prisma.service';
describe('InspectionsService', () => {
let service: InspectionsService;
let prisma: {
inspection: {
findMany: jest.Mock;
findUnique: jest.Mock;
create: jest.Mock;
};
task: { create: jest.Mock };
};
const inspection = {
id: 'inspection-1',
colonyId: 'colony-1',
date: new Date(),
};
beforeEach(() => {
prisma = {
inspection: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
},
task: { create: jest.fn() },
};
service = new InspectionsService(prisma as unknown as PrismaService);
});
it('findById throws NotFoundException when missing', async () => {
prisma.inspection.findUnique.mockResolvedValue(null);
await expect(service.findById('missing')).rejects.toBeInstanceOf(
NotFoundException,
);
});
it('create stores the acting user as the inspector', async () => {
prisma.inspection.create.mockResolvedValue(inspection);
await service.create(
{ colonyId: 'colony-1', date: '2026-01-01' },
'user-1',
);
const [[createArg]] = prisma.inspection.create.mock.calls as [
[{ data: { colonyId: string; userId: string } }],
];
expect(createArg.data.colonyId).toBe('colony-1');
expect(createArg.data.userId).toBe('user-1');
});
it('createTaskFromInspection links the task to the inspection and its colony', async () => {
prisma.inspection.findUnique.mockResolvedValue(inspection);
prisma.task.create.mockResolvedValue({ id: 'task-1' });
await service.createTaskFromInspection(
'inspection-1',
{ title: 'Nachschauen' },
'user-1',
);
const [[taskArg]] = prisma.task.create.mock.calls as [
[
{
data: {
sourceInspectionId: string;
relatedType: string;
relatedId: string;
};
},
],
];
expect(taskArg.data.sourceInspectionId).toBe('inspection-1');
expect(taskArg.data.relatedType).toBe('COLONY');
expect(taskArg.data.relatedId).toBe('colony-1');
});
});
@@ -0,0 +1,61 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Inspection, Task } from '../../generated/prisma';
import { CreateInspectionDto } from './dto/create-inspection.dto';
import { CreateTaskFromInspectionDto } from './dto/create-task-from-inspection.dto';
@Injectable()
export class InspectionsService {
constructor(private readonly prisma: PrismaService) {}
async findAll(): Promise<Inspection[]> {
return this.prisma.inspection.findMany({ orderBy: { date: 'desc' } });
}
async findById(id: string): Promise<Inspection> {
const inspection = await this.prisma.inspection.findUnique({
where: { id },
});
if (!inspection) {
throw new NotFoundException('Inspection not found');
}
return inspection;
}
async findByColony(colonyId: string): Promise<Inspection[]> {
return this.prisma.inspection.findMany({
where: { colonyId },
orderBy: { date: 'desc' },
});
}
async create(
dto: CreateInspectionDto,
actingUserId: string,
): Promise<Inspection> {
return this.prisma.inspection.create({
data: { ...dto, date: new Date(dto.date), userId: actingUserId },
});
}
async createTaskFromInspection(
id: string,
dto: CreateTaskFromInspectionDto,
actingUserId: string,
): Promise<Task> {
const inspection = await this.findById(id);
return this.prisma.task.create({
data: {
title: dto.title,
description: dto.description,
priority: dto.priority,
dueDate: dto.dueDate ? new Date(dto.dueDate) : undefined,
assignedUserId: dto.assignedUserId,
createdByUserId: actingUserId,
relatedType: 'COLONY',
relatedId: inspection.colonyId,
sourceInspectionId: inspection.id,
},
});
}
}