diff --git a/ApiCore-Backend/src/colonies/dto/update-colony.dto.ts b/ApiCore-Backend/src/colonies/dto/update-colony.dto.ts index a4ccbcf..d3e4258 100644 --- a/ApiCore-Backend/src/colonies/dto/update-colony.dto.ts +++ b/ApiCore-Backend/src/colonies/dto/update-colony.dto.ts @@ -3,8 +3,15 @@ import { IsEnum, IsOptional } from 'class-validator'; import { ColonyStatus } from '../../../generated/prisma'; import { CreateColonyDto } from './create-colony.dto'; +/** + * `apiaryId` is deliberately absent: moving a colony to another apiary runs + * through POST /colonies/:id/move, which also writes the ColonyApiaryHistory + * entry the traceability chain depends on. A plain PATCH would move the + * colony without that record. + */ class UpdatableColonyFieldsDto extends PartialType( PickType(CreateColonyDto, [ + 'code', 'hiveType', 'originText', 'colonyType', diff --git a/ApiCore-Backend/src/common/filters/all-exceptions.filter.spec.ts b/ApiCore-Backend/src/common/filters/all-exceptions.filter.spec.ts index 4eccd64..6b1493e 100644 --- a/ApiCore-Backend/src/common/filters/all-exceptions.filter.spec.ts +++ b/ApiCore-Backend/src/common/filters/all-exceptions.filter.spec.ts @@ -123,4 +123,57 @@ describe('AllExceptionsFilter', () => { expect(warnSpy).toHaveBeenCalled(); expect(errorSpy).not.toHaveBeenCalled(); }); + + it('translates a P2003 foreign-key error to a 400 naming the broken relation', () => { + jest.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined); + const exception = new Prisma.PrismaClientKnownRequestError( + 'Foreign key constraint failed on the field: `colonyId`', + { + code: 'P2003', + clientVersion: '6.0.0', + meta: { field_name: 'colonyId' }, + }, + ); + + filter.catch(exception, host); + + expect(statusMock).toHaveBeenCalledWith(400); + expect(jsonMock).toHaveBeenCalledWith({ + statusCode: 400, + message: + 'Der verknüpfte Datensatz existiert nicht (mehr) - bitte die Auswahl prüfen', + }); + }); + + it('translates a P2025 missing-record error to a 404', () => { + jest.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined); + const exception = new Prisma.PrismaClientKnownRequestError( + 'An operation failed because it depends on one or more records that were required but not found.', + { code: 'P2025', clientVersion: '6.0.0' }, + ); + + filter.catch(exception, host); + + expect(statusMock).toHaveBeenCalledWith(404); + expect(jsonMock).toHaveBeenCalledWith({ + statusCode: 404, + message: 'Datensatz nicht gefunden', + }); + }); + + it('leaves an unmapped Prisma error as a generic 500, leaking nothing', () => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); + const exception = new Prisma.PrismaClientKnownRequestError( + 'Timed out fetching a new connection from the connection pool', + { code: 'P2024', clientVersion: '6.0.0' }, + ); + + filter.catch(exception, host); + + expect(statusMock).toHaveBeenCalledWith(500); + expect(jsonMock).toHaveBeenCalledWith({ + statusCode: 500, + message: 'Internal server error', + }); + }); }); diff --git a/ApiCore-Backend/src/common/filters/all-exceptions.filter.ts b/ApiCore-Backend/src/common/filters/all-exceptions.filter.ts index 93f1677..db9dc4f 100644 --- a/ApiCore-Backend/src/common/filters/all-exceptions.filter.ts +++ b/ApiCore-Backend/src/common/filters/all-exceptions.filter.ts @@ -34,6 +34,45 @@ function duplicateFieldMessage(target: unknown): string { : 'Dieser Wert wird bereits verwendet - bitte einen anderen Wert wählen'; } +/** + * A relation pointed at a record that does not exist (Prisma P2003). Every + * module lets a record be re-pointed at a different parent - an inspection + * filed against the wrong colony, a harvest against the wrong apiary - and + * relies on the foreign key rather than an application-level existence check, + * so a stale or mistyped id has to come back as a clean 400. + */ +const FOREIGN_KEY_MESSAGE = + 'Der verknüpfte Datensatz existiert nicht (mehr) - bitte die Auswahl prüfen'; + +/** + * Translates the Prisma errors that are really user input problems into the + * status and message they deserve. Anything not listed here stays a 500 with + * a generic body, so no internal detail leaks. + */ +function prismaErrorResponse( + exception: unknown, +): { status: number; message: string } | null { + if (!(exception instanceof Prisma.PrismaClientKnownRequestError)) { + return null; + } + switch (exception.code) { + case 'P2002': + return { + status: HttpStatus.CONFLICT, + message: duplicateFieldMessage(exception.meta?.target), + }; + case 'P2003': + return { status: HttpStatus.BAD_REQUEST, message: FOREIGN_KEY_MESSAGE }; + case 'P2025': + return { + status: HttpStatus.NOT_FOUND, + message: 'Datensatz nicht gefunden', + }; + default: + return null; + } +} + /** * Replaces Nest's default exception handling so EVERY error - not just * unhandled 5xx ones - shows up in stdout/kubectl logs. Nest's built-in @@ -46,12 +85,13 @@ function duplicateFieldMessage(target: unknown): string { * Client-facing behaviour is unchanged: HttpExceptions still return * exactly the body/status Nest would have produced. Non-HttpException * errors still collapse to a generic 500 body - the real message and - * stack go to the log, never to the client. The one exception is Prisma's - * P2002 (unique constraint violation): services create/update records - * straight off a DB `@unique` column (colony/apiary/queen codes, SKUs, - * batch numbers) with no application-level duplicate check, relying on - * the constraint itself - so without this branch, a beekeeper reusing a - * code by mistake saw a bare 500 "Internal server error" instead of a + * stack go to the log, never to the client. The exceptions are the Prisma + * errors listed in prismaErrorResponse: services write records straight + * against DB constraints (`@unique` columns like colony/apiary/queen codes, + * SKUs and batch numbers; foreign keys for every parent relation) with no + * application-level check, relying on the constraint itself - so without + * those branches, a beekeeper reusing a code or picking a record that was + * meanwhile removed saw a bare 500 "Internal server error" instead of a * clean, specific message, and the log filled with fake server errors for * what is really a user input problem. */ @@ -65,22 +105,16 @@ export class AllExceptionsFilter implements ExceptionFilter { const request = ctx.getRequest(); const isHttpException = exception instanceof HttpException; - const isUniqueConstraintError = - exception instanceof Prisma.PrismaClientKnownRequestError && - exception.code === 'P2002'; + const prismaResponse = prismaErrorResponse(exception); const status = isHttpException ? exception.getStatus() - : isUniqueConstraintError - ? HttpStatus.CONFLICT - : HttpStatus.INTERNAL_SERVER_ERROR; + : (prismaResponse?.status ?? HttpStatus.INTERNAL_SERVER_ERROR); const body = isHttpException ? exception.getResponse() - : isUniqueConstraintError - ? { - statusCode: status, - message: duplicateFieldMessage(exception.meta?.target), - } - : { statusCode: status, message: 'Internal server error' }; + : { + statusCode: status, + message: prismaResponse?.message ?? 'Internal server error', + }; const message = exception instanceof Error ? exception.message : 'Unknown error'; const context = `${request.method} ${request.originalUrl} -> ${status}`; diff --git a/ApiCore-Backend/src/extractions/dto/update-extraction.dto.ts b/ApiCore-Backend/src/extractions/dto/update-extraction.dto.ts index 0c94f35..1f8de7f 100644 --- a/ApiCore-Backend/src/extractions/dto/update-extraction.dto.ts +++ b/ApiCore-Backend/src/extractions/dto/update-extraction.dto.ts @@ -1,13 +1,9 @@ -import { PartialType, PickType } from '@nestjs/swagger'; +import { PartialType } from '@nestjs/swagger'; import { CreateExtractionDto } from './create-extraction.dto'; -export class UpdateExtractionDto extends PartialType( - PickType(CreateExtractionDto, [ - 'date', - 'amount', - 'room', - 'waterContent', - 'temperature', - 'notes', - ] as const), -) {} +/** + * Every field stays correctable, the set of harvests included: a Schleuderung + * linked to the wrong Ernte is a data-entry error, and re-pointing it is the + * only way to repair the traceability chain without deleting the record. + */ +export class UpdateExtractionDto extends PartialType(CreateExtractionDto) {} diff --git a/ApiCore-Backend/src/extractions/extractions.service.spec.ts b/ApiCore-Backend/src/extractions/extractions.service.spec.ts index a1c296f..3d0312a 100644 --- a/ApiCore-Backend/src/extractions/extractions.service.spec.ts +++ b/ApiCore-Backend/src/extractions/extractions.service.spec.ts @@ -12,16 +12,19 @@ describe('ExtractionsService', () => { }; let auditLog: { record: jest.Mock }; let tx: { - extraction: { create: jest.Mock }; - extractionHarvest: { createMany: jest.Mock }; + extraction: { create: jest.Mock; update: jest.Mock }; + extractionHarvest: { createMany: jest.Mock; deleteMany: jest.Mock }; }; const extraction = { id: 'extraction-1', date: new Date() }; beforeEach(() => { tx = { - extraction: { create: jest.fn().mockResolvedValue(extraction) }, - extractionHarvest: { createMany: jest.fn() }, + extraction: { + create: jest.fn().mockResolvedValue(extraction), + update: jest.fn(), + }, + extractionHarvest: { createMany: jest.fn(), deleteMany: jest.fn() }, }; prisma = { extraction: { findUnique: jest.fn(), update: jest.fn() }, @@ -82,7 +85,6 @@ describe('ExtractionsService', () => { prisma.extraction.findUnique .mockResolvedValueOnce(extraction) .mockResolvedValueOnce(updated); - prisma.extraction.update.mockResolvedValue(updated); const result = await service.update( 'extraction-1', @@ -100,5 +102,41 @@ describe('ExtractionsService', () => { afterData: updated, }), ); + expect(tx.extractionHarvest.deleteMany).not.toHaveBeenCalled(); + }); + + it('update replaces the whole harvest set when harvestIds is given', async () => { + prisma.extraction.findUnique.mockResolvedValue(extraction); + + await service.update( + 'extraction-1', + { harvestIds: ['harvest-1', 'harvest-2'] }, + 'user-1', + ); + + expect(tx.extractionHarvest.deleteMany).toHaveBeenCalledWith({ + where: { extractionId: 'extraction-1' }, + }); + const [[createArg]] = tx.extractionHarvest.createMany.mock.calls as [ + [{ data: { extractionId: string; harvestId: string }[] }], + ]; + expect(createArg.data).toEqual([ + { extractionId: 'extraction-1', harvestId: 'harvest-1' }, + { extractionId: 'extraction-1', harvestId: 'harvest-2' }, + ]); + }); + + it('update rejects an unknown harvest id before touching the database', async () => { + prisma.extraction.findUnique.mockResolvedValue(extraction); + prisma.harvest.findMany.mockResolvedValue([{ id: 'harvest-1' }]); + + await expect( + service.update( + 'extraction-1', + { harvestIds: ['harvest-1', 'missing-harvest'] }, + 'user-1', + ), + ).rejects.toBeInstanceOf(BadRequestException); + expect(prisma.$transaction).not.toHaveBeenCalled(); }); }); diff --git a/ApiCore-Backend/src/extractions/extractions.service.ts b/ApiCore-Backend/src/extractions/extractions.service.ts index 462855c..5886bb4 100644 --- a/ApiCore-Backend/src/extractions/extractions.service.ts +++ b/ApiCore-Backend/src/extractions/extractions.service.ts @@ -89,10 +89,32 @@ export class ExtractionsService { async update(id: string, dto: UpdateExtractionDto, actingUserId: string) { const before = await this.findById(id); - await this.prisma.extraction.update({ - where: { id }, - data: { ...dto, date: dto.date ? new Date(dto.date) : undefined }, + const { harvestIds, ...scalarFields } = dto; + if (harvestIds) { + await this.assertHarvestsExist(harvestIds); + } + + await this.prisma.$transaction(async (tx) => { + await tx.extraction.update({ + where: { id }, + data: { + ...scalarFields, + date: dto.date ? new Date(dto.date) : undefined, + }, + }); + // The join rows carry no data of their own, so replacing the whole set + // is both simpler and safer than diffing it. + if (harvestIds) { + await tx.extractionHarvest.deleteMany({ where: { extractionId: id } }); + await tx.extractionHarvest.createMany({ + data: harvestIds.map((harvestId) => ({ + extractionId: id, + harvestId, + })), + }); + } }); + const extraction = await this.findById(id); await this.auditLog.record({ entityType: 'Extraction', diff --git a/ApiCore-Backend/src/feedings/dto/update-feeding.dto.ts b/ApiCore-Backend/src/feedings/dto/update-feeding.dto.ts index aef079f..fee5e17 100644 --- a/ApiCore-Backend/src/feedings/dto/update-feeding.dto.ts +++ b/ApiCore-Backend/src/feedings/dto/update-feeding.dto.ts @@ -1,14 +1,9 @@ -import { PartialType, PickType } from '@nestjs/swagger'; +import { PartialType } from '@nestjs/swagger'; import { CreateFeedingDto } from './create-feeding.dto'; -export class UpdateFeedingDto extends PartialType( - PickType(CreateFeedingDto, [ - 'date', - 'feedType', - 'product', - 'amount', - 'unit', - 'batchNumber', - 'notes', - ] as const), -) {} +/** + * Every field stays correctable, `colonyId` included: a record filed against + * the wrong colony is a data-entry error, and re-filing it is the only way to + * repair the colony's history without deleting the record. + */ +export class UpdateFeedingDto extends PartialType(CreateFeedingDto) {} diff --git a/ApiCore-Backend/src/harvests/dto/update-harvest.dto.ts b/ApiCore-Backend/src/harvests/dto/update-harvest.dto.ts index b292218..39bdb05 100644 --- a/ApiCore-Backend/src/harvests/dto/update-harvest.dto.ts +++ b/ApiCore-Backend/src/harvests/dto/update-harvest.dto.ts @@ -1,13 +1,10 @@ -import { PartialType, PickType } from '@nestjs/swagger'; +import { PartialType } from '@nestjs/swagger'; import { CreateHarvestDto } from './create-harvest.dto'; -export class UpdateHarvestDto extends PartialType( - PickType(CreateHarvestDto, [ - 'date', - 'honeyType', - 'honeySuperCount', - 'grossWeight', - 'netWeight', - 'notes', - ] as const), -) {} +/** + * Every field stays correctable, the apiary and the set of colonies included: + * an Ernte booked against the wrong stand or with a colony forgotten is a + * data-entry error, and re-pointing it is the only way to repair the + * traceability chain without deleting the record. + */ +export class UpdateHarvestDto extends PartialType(CreateHarvestDto) {} diff --git a/ApiCore-Backend/src/harvests/harvests.service.spec.ts b/ApiCore-Backend/src/harvests/harvests.service.spec.ts index 72b0422..cd72d92 100644 --- a/ApiCore-Backend/src/harvests/harvests.service.spec.ts +++ b/ApiCore-Backend/src/harvests/harvests.service.spec.ts @@ -14,16 +14,19 @@ describe('HarvestsService', () => { }; let auditLog: { record: jest.Mock }; let tx: { - harvest: { create: jest.Mock }; - harvestColony: { createMany: jest.Mock }; + harvest: { create: jest.Mock; update: jest.Mock }; + harvestColony: { createMany: jest.Mock; deleteMany: jest.Mock }; }; const harvest = { id: 'harvest-1', apiaryId: 'apiary-1', date: new Date() }; beforeEach(() => { tx = { - harvest: { create: jest.fn().mockResolvedValue(harvest) }, - harvestColony: { createMany: jest.fn() }, + harvest: { + create: jest.fn().mockResolvedValue(harvest), + update: jest.fn(), + }, + harvestColony: { createMany: jest.fn(), deleteMany: jest.fn() }, }; prisma = { harvest: { findUnique: jest.fn(), update: jest.fn() }, @@ -96,7 +99,6 @@ describe('HarvestsService', () => { prisma.harvest.findUnique .mockResolvedValueOnce(harvest) .mockResolvedValueOnce(updated); - prisma.harvest.update.mockResolvedValue(updated); const result = await service.update( 'harvest-1', @@ -105,6 +107,7 @@ describe('HarvestsService', () => { ); expect(result).toEqual(updated); + expect(tx.harvestColony.deleteMany).not.toHaveBeenCalled(); expect(auditLog.record).toHaveBeenCalledWith( expect.objectContaining({ entityType: 'Harvest', @@ -115,4 +118,39 @@ describe('HarvestsService', () => { }), ); }); + + it('update replaces the whole colony set when colonyIds is given', async () => { + prisma.harvest.findUnique.mockResolvedValue(harvest); + + await service.update( + 'harvest-1', + { colonyIds: ['colony-1', 'colony-2'] }, + 'user-1', + ); + + expect(tx.harvestColony.deleteMany).toHaveBeenCalledWith({ + where: { harvestId: 'harvest-1' }, + }); + const [[createArg]] = tx.harvestColony.createMany.mock.calls as [ + [{ data: { harvestId: string; colonyId: string }[] }], + ]; + expect(createArg.data).toEqual([ + { harvestId: 'harvest-1', colonyId: 'colony-1' }, + { harvestId: 'harvest-1', colonyId: 'colony-2' }, + ]); + }); + + it('update rejects an unknown colony id before touching the database', async () => { + prisma.harvest.findUnique.mockResolvedValue(harvest); + prisma.colony.findMany.mockResolvedValue([{ id: 'colony-1' }]); + + await expect( + service.update( + 'harvest-1', + { colonyIds: ['colony-1', 'missing-colony'] }, + 'user-1', + ), + ).rejects.toBeInstanceOf(BadRequestException); + expect(prisma.$transaction).not.toHaveBeenCalled(); + }); }); diff --git a/ApiCore-Backend/src/harvests/harvests.service.ts b/ApiCore-Backend/src/harvests/harvests.service.ts index 58299a3..974802e 100644 --- a/ApiCore-Backend/src/harvests/harvests.service.ts +++ b/ApiCore-Backend/src/harvests/harvests.service.ts @@ -93,10 +93,29 @@ export class HarvestsService { async update(id: string, dto: UpdateHarvestDto, actingUserId: string) { const before = await this.findById(id); - await this.prisma.harvest.update({ - where: { id }, - data: { ...dto, date: dto.date ? new Date(dto.date) : undefined }, + const { colonyIds, ...scalarFields } = dto; + if (colonyIds) { + await this.assertColoniesExist(colonyIds); + } + + await this.prisma.$transaction(async (tx) => { + await tx.harvest.update({ + where: { id }, + data: { + ...scalarFields, + date: dto.date ? new Date(dto.date) : undefined, + }, + }); + // The join rows carry no data of their own, so replacing the whole set + // is both simpler and safer than diffing it. + if (colonyIds) { + await tx.harvestColony.deleteMany({ where: { harvestId: id } }); + await tx.harvestColony.createMany({ + data: colonyIds.map((colonyId) => ({ harvestId: id, colonyId })), + }); + } }); + const harvest = await this.findById(id); await this.auditLog.record({ entityType: 'Harvest', diff --git a/ApiCore-Backend/src/honey-batches/dto/update-honey-batch.dto.ts b/ApiCore-Backend/src/honey-batches/dto/update-honey-batch.dto.ts index ac08495..39a4aa7 100644 --- a/ApiCore-Backend/src/honey-batches/dto/update-honey-batch.dto.ts +++ b/ApiCore-Backend/src/honey-batches/dto/update-honey-batch.dto.ts @@ -1,10 +1,32 @@ -import { PartialType, PickType } from '@nestjs/swagger'; +import { ApiPropertyOptional, PartialType, PickType } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { HoneyBatchStatus } from '../../../generated/prisma'; import { CreateHoneyBatchDto } from './create-honey-batch.dto'; -export class UpdateHoneyBatchDto extends PartialType( +/** + * `storageContainerId` is deliberately absent: moving a batch to another + * container runs through POST /honey-batches/:id/relocate, which also writes + * the HoneyBatchRelocation entry. A plain PATCH would move the honey without + * that record. + */ +class UpdatableHoneyBatchFieldsDto extends PartialType( PickType(CreateHoneyBatchDto, [ + 'batchNumber', + 'extractionId', 'honeyType', 'amount', 'waterContent', ] as const), ) {} + +export class UpdateHoneyBatchDto extends UpdatableHoneyBatchFieldsDto { + /** + * Set automatically to BOTTLED when the batch is bottled, but a beekeeper + * still has to be able to record that a batch was sold or consumed, or to + * undo a status that was set by mistake. + */ + @ApiPropertyOptional({ enum: HoneyBatchStatus }) + @IsOptional() + @IsEnum(HoneyBatchStatus) + status?: HoneyBatchStatus; +} diff --git a/ApiCore-Backend/src/inspections/dto/update-inspection.dto.ts b/ApiCore-Backend/src/inspections/dto/update-inspection.dto.ts index c71e5e5..cd59941 100644 --- a/ApiCore-Backend/src/inspections/dto/update-inspection.dto.ts +++ b/ApiCore-Backend/src/inspections/dto/update-inspection.dto.ts @@ -1,27 +1,9 @@ -import { PartialType, PickType } from '@nestjs/swagger'; +import { PartialType } from '@nestjs/swagger'; import { CreateInspectionDto } from './create-inspection.dto'; -export class UpdateInspectionDto extends PartialType( - PickType(CreateInspectionDto, [ - 'date', - 'weather', - 'temperature', - 'colonyStrength', - 'broodAmount', - 'broodPattern', - 'openBroodPresent', - 'cappedBroodPresent', - 'eggsPresent', - 'queenSeen', - 'queenCellsPresent', - 'swarmMood', - 'gentleness', - 'combBehavior', - 'feedStatus', - 'honeySuperStatus', - 'healthIssues', - 'varroaIssues', - 'measures', - 'notes', - ] as const), -) {} +/** + * Every field stays correctable, `colonyId` included: a record filed against + * the wrong colony is a data-entry error, and re-filing it is the only way to + * repair the colony's history without deleting the record. + */ +export class UpdateInspectionDto extends PartialType(CreateInspectionDto) {} diff --git a/ApiCore-Backend/src/storage/dto/update-storage-container.dto.ts b/ApiCore-Backend/src/storage/dto/update-storage-container.dto.ts index 831fc0d..2530d5b 100644 --- a/ApiCore-Backend/src/storage/dto/update-storage-container.dto.ts +++ b/ApiCore-Backend/src/storage/dto/update-storage-container.dto.ts @@ -3,6 +3,7 @@ import { CreateStorageContainerDto } from './create-storage-container.dto'; export class UpdateStorageContainerDto extends PartialType( PickType(CreateStorageContainerDto, [ + 'code', 'containerType', 'capacity', 'location', diff --git a/ApiCore-Backend/src/swarm-control/dto/update-swarm-control-check.dto.ts b/ApiCore-Backend/src/swarm-control/dto/update-swarm-control-check.dto.ts index ffc3400..7252645 100644 --- a/ApiCore-Backend/src/swarm-control/dto/update-swarm-control-check.dto.ts +++ b/ApiCore-Backend/src/swarm-control/dto/update-swarm-control-check.dto.ts @@ -1,14 +1,11 @@ -import { PartialType, PickType } from '@nestjs/swagger'; +import { PartialType } from '@nestjs/swagger'; import { CreateSwarmControlCheckDto } from './create-swarm-control-check.dto'; +/** + * Every field stays correctable, `colonyId` included: a record filed against + * the wrong colony is a data-entry error, and re-filing it is the only way to + * repair the colony's history without deleting the record. + */ export class UpdateSwarmControlCheckDto extends PartialType( - PickType(CreateSwarmControlCheckDto, [ - 'date', - 'queenCellsFound', - 'queenCellsCapped', - 'swarmMood', - 'actionTaken', - 'notes', - 'nextCheckDate', - ] as const), + CreateSwarmControlCheckDto, ) {} diff --git a/ApiCore-Backend/src/tasks/dto/update-task.dto.ts b/ApiCore-Backend/src/tasks/dto/update-task.dto.ts index 082869e..0b6fe95 100644 --- a/ApiCore-Backend/src/tasks/dto/update-task.dto.ts +++ b/ApiCore-Backend/src/tasks/dto/update-task.dto.ts @@ -1,6 +1,10 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsDateString, IsEnum, IsOptional, IsString } from 'class-validator'; -import { TaskPriority, TaskStatus } from '../../../generated/prisma'; +import { + TaskPriority, + TaskRelatedType, + TaskStatus, +} from '../../../generated/prisma'; export class UpdateTaskDto { @ApiPropertyOptional() @@ -32,4 +36,14 @@ export class UpdateTaskDto { @IsOptional() @IsEnum(TaskStatus) status?: TaskStatus; + + @ApiPropertyOptional({ enum: TaskRelatedType }) + @IsOptional() + @IsEnum(TaskRelatedType) + relatedType?: TaskRelatedType; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + relatedId?: string; } diff --git a/ApiCore-Backend/src/treatments/dto/update-treatment.dto.ts b/ApiCore-Backend/src/treatments/dto/update-treatment.dto.ts index 36817e5..e121c60 100644 --- a/ApiCore-Backend/src/treatments/dto/update-treatment.dto.ts +++ b/ApiCore-Backend/src/treatments/dto/update-treatment.dto.ts @@ -1,18 +1,9 @@ -import { PartialType, PickType } from '@nestjs/swagger'; +import { PartialType } from '@nestjs/swagger'; import { CreateTreatmentDto } from './create-treatment.dto'; -export class UpdateTreatmentDto extends PartialType( - PickType(CreateTreatmentDto, [ - 'date', - 'startDate', - 'endDate', - 'product', - 'activeIngredient', - 'amount', - 'method', - 'reason', - 'batchNumber', - 'notes', - 'withdrawalPeriodDays', - ] as const), -) {} +/** + * Every field stays correctable, `colonyId` included: a record filed against + * the wrong colony is a data-entry error, and re-filing it is the only way to + * repair the colony's history without deleting the record. + */ +export class UpdateTreatmentDto extends PartialType(CreateTreatmentDto) {} diff --git a/ApiCore-Backend/src/varroa/dto/update-varroa-measurement.dto.ts b/ApiCore-Backend/src/varroa/dto/update-varroa-measurement.dto.ts index 697d628..0c38283 100644 --- a/ApiCore-Backend/src/varroa/dto/update-varroa-measurement.dto.ts +++ b/ApiCore-Backend/src/varroa/dto/update-varroa-measurement.dto.ts @@ -1,14 +1,11 @@ -import { PartialType, PickType } from '@nestjs/swagger'; +import { PartialType } from '@nestjs/swagger'; import { CreateVarroaMeasurementDto } from './create-varroa-measurement.dto'; +/** + * Every field stays correctable, `colonyId` included: a record filed against + * the wrong colony is a data-entry error, and re-filing it is the only way to + * repair the colony's history without deleting the record. + */ export class UpdateVarroaMeasurementDto extends PartialType( - PickType(CreateVarroaMeasurementDto, [ - 'date', - 'method', - 'durationMinutes', - 'miteCount', - 'calculatedValue', - 'assessment', - 'notes', - ] as const), + CreateVarroaMeasurementDto, ) {} diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-edit-card.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-edit-card.tsx index b65cd8a..14a6dad 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-edit-card.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-edit-card.tsx @@ -76,6 +76,7 @@ function ConditionSelect({ export function ColonyEditCard({ colony }: { colony: Colony }) { const queryClient = useQueryClient(); + const [code, setCode] = useState(colony.code); const [status, setStatus] = useState(colony.status); const [colonyType, setColonyType] = useState(colony.colonyType); const [hiveType, setHiveType] = useState(colony.hiveType ?? ""); @@ -95,6 +96,7 @@ export function ColonyEditCard({ colony }: { colony: Colony }) { const updateMutation = useMutation({ mutationFn: () => updateColony(colony.id, { + code, status, colonyType, hiveType: hiveType || undefined, @@ -133,6 +135,15 @@ export function ColonyEditCard({ colony }: { colony: Colony }) {
+
+ + setCode(event.target.value)} + /> +
updateInspection(inspection.id, { + colonyId, date: new Date(date).toISOString(), weather: weather || undefined, temperature: temperature ? Number(temperature) : undefined, @@ -86,9 +89,13 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection }) notes: notes || undefined, }), onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: ["colonies", inspection.colonyId, "inspections"], - }); + // After re-filing, both the colony it left and the one it joined + // show a stale list until each is invalidated. + for (const affectedColonyId of new Set([inspection.colonyId, colonyId])) { + void queryClient.invalidateQueries({ + queryKey: ["colonies", affectedColonyId, "inspections"], + }); + } setOpen(false); }, onError: () => toast.error("Durchsicht konnte nicht gespeichert werden"), @@ -114,6 +121,11 @@ export function InspectionEditDialog({ inspection }: { inspection: Inspection }) Durchsicht bearbeiten +
updateSwarmControlCheck(check.id, { + colonyId, date: new Date(date).toISOString(), queenCellsFound: queenCellsFound ? Number(queenCellsFound) : undefined, queenCellsCapped, @@ -49,9 +52,13 @@ export function SwarmControlEditDialog({ check }: { check: SwarmControlCheck }) notes: notes || undefined, }), onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: ["colonies", check.colonyId, "swarm-control-checks"], - }); + // After re-filing, both the colony it left and the one it joined + // show a stale list until each is invalidated. + for (const affectedColonyId of new Set([check.colonyId, colonyId])) { + void queryClient.invalidateQueries({ + queryKey: ["colonies", affectedColonyId, "swarm-control-checks"], + }); + } setOpen(false); }, onError: () => toast.error("Schwarmkontrolle konnte nicht gespeichert werden"), @@ -77,6 +84,11 @@ export function SwarmControlEditDialog({ check }: { check: SwarmControlCheck }) Schwarmkontrolle bearbeiten +
updateTreatment(treatment.id, { + colonyId, date: new Date(date).toISOString(), startDate: startDate ? new Date(startDate).toISOString() : undefined, endDate: endDate ? new Date(endDate).toISOString() : undefined, @@ -51,9 +54,13 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) { withdrawalPeriodDays: withdrawalPeriodDays ? Number(withdrawalPeriodDays) : undefined, }), onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: ["colonies", treatment.colonyId, "treatments"], - }); + // After re-filing, both the colony it left and the one it joined + // show a stale list until each is invalidated. + for (const affectedColonyId of new Set([treatment.colonyId, colonyId])) { + void queryClient.invalidateQueries({ + queryKey: ["colonies", affectedColonyId, "treatments"], + }); + } setOpen(false); }, onError: () => toast.error("Behandlung konnte nicht gespeichert werden"), @@ -79,6 +86,11 @@ export function TreatmentEditDialog({ treatment }: { treatment: Treatment }) { Behandlung bearbeiten +
(measurement.method); const [miteCount, setMiteCount] = useState(measurement.miteCount?.toString() ?? ""); @@ -44,6 +46,7 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme const mutation = useMutation({ mutationFn: () => updateVarroaMeasurement(measurement.id, { + colonyId, date: new Date(date).toISOString(), method, miteCount: miteCount ? Number(miteCount) : undefined, @@ -53,9 +56,13 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme notes: notes || undefined, }), onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: ["colonies", measurement.colonyId, "varroa-measurements"], - }); + // After re-filing, both the colony it left and the one it joined + // show a stale list until each is invalidated. + for (const affectedColonyId of new Set([measurement.colonyId, colonyId])) { + void queryClient.invalidateQueries({ + queryKey: ["colonies", affectedColonyId, "varroa-measurements"], + }); + } setOpen(false); }, onError: () => toast.error("Varroamessung konnte nicht gespeichert werden"), @@ -81,6 +88,11 @@ export function VarroaEditDialog({ measurement }: { measurement: VarroaMeasureme Varroamessung bearbeiten +
( + extraction.harvests.map((entry) => entry.harvestId), + ); const [date, setDate] = useState(extraction.date.slice(0, 10)); const [amount, setAmount] = useState(extraction.amount?.toString() ?? ""); const [room, setRoom] = useState(extraction.room ?? ""); @@ -31,6 +35,7 @@ export function ExtractionEditDialog({ extraction }: { extraction: Extraction }) const mutation = useMutation({ mutationFn: () => updateExtraction(extraction.id, { + harvestIds, date: new Date(date).toISOString(), amount: amount ? Number(amount) : undefined, room: room || undefined, @@ -42,11 +47,16 @@ export function ExtractionEditDialog({ extraction }: { extraction: Extraction }) void queryClient.invalidateQueries({ queryKey: ["extractions", extraction.id] }); setOpen(false); }, - onError: () => toast.error("Schleuderung konnte nicht gespeichert werden"), + onError: (error: Error) => + toast.error(error.message || "Schleuderung konnte nicht gespeichert werden"), }); function handleSubmit(event: FormEvent) { event.preventDefault(); + if (harvestIds.length === 0) { + toast.error("Bitte mindestens eine Ernte auswählen."); + return; + } mutation.mutate(); } @@ -65,6 +75,11 @@ export function ExtractionEditDialog({ extraction }: { extraction: Extraction }) Schleuderung bearbeiten +
( + harvest.colonies.map((entry) => entry.colonyId), + ); const [date, setDate] = useState(harvest.date.slice(0, 10)); const [honeyType, setHoneyType] = useState(harvest.honeyType ?? ""); const [grossWeight, setGrossWeight] = useState(harvest.grossWeight?.toString() ?? ""); @@ -30,9 +43,35 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) { ); const [notes, setNotes] = useState(harvest.notes ?? ""); + const { data: apiaries } = useQuery({ + queryKey: ["apiaries"], + queryFn: () => fetchApiaries(), + }); + const { data: colonies } = useQuery({ + queryKey: ["colonies"], + queryFn: () => fetchColonies(), + }); + + // Colonies of the chosen apiary, plus any colony already on this harvest - + // one that was meanwhile moved or dissolved must stay visible, otherwise + // saving would silently drop it from the harvest. + const selectableColonies = (colonies ?? []).filter( + (colony) => colony.apiaryId === apiaryId || colonyIds.includes(colony.id), + ); + + function toggleColony(colonyId: string, checked: boolean) { + setColonyIds((current) => + checked + ? [...current, colonyId] + : current.filter((id) => id !== colonyId), + ); + } + const mutation = useMutation({ mutationFn: () => updateHarvest(harvest.id, { + apiaryId, + colonyIds, date: new Date(date).toISOString(), honeyType: honeyType || undefined, grossWeight: grossWeight ? Number(grossWeight) : undefined, @@ -41,14 +80,19 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) { notes: notes || undefined, }), onSuccess: () => { - void queryClient.invalidateQueries({ queryKey: ["harvests", harvest.id] }); + void queryClient.invalidateQueries({ queryKey: ["harvests"] }); setOpen(false); }, - onError: () => toast.error("Ernte konnte nicht gespeichert werden"), + onError: (error: Error) => + toast.error(error.message || "Ernte konnte nicht gespeichert werden"), }); function handleSubmit(event: FormEvent) { event.preventDefault(); + if (colonyIds.length === 0) { + toast.error("Bitte mindestens ein Volk auswählen."); + return; + } mutation.mutate(); } @@ -67,6 +111,48 @@ export function HarvestEditDialog({ harvest }: { harvest: Harvest }) { Ernte bearbeiten +
+ + +
+
+ + {selectableColonies.length === 0 ? ( +

+ Keine Völker an diesem Bienenstand. +

+ ) : null} + {selectableColonies.map((colony) => ( + + ))} +
void; }) { const queryClient = useQueryClient(); + const [harvestIds, setHarvestIds] = useState( + extraction.harvests.map((entry) => entry.harvestId), + ); const [date, setDate] = useState(extraction.date.slice(0, 10)); const [amount, setAmount] = useState(extraction.amount?.toString() ?? ""); const [room, setRoom] = useState(extraction.room ?? ""); @@ -42,6 +46,7 @@ function ExtractionEditForm({ const mutation = useMutation({ mutationFn: () => updateExtraction(extraction.id, { + harvestIds, date: new Date(date).toISOString(), amount: amount ? Number(amount) : undefined, room: room || undefined, @@ -55,16 +60,26 @@ function ExtractionEditForm({ }); onSaved(); }, - onError: () => toast.error("Schleuderung konnte nicht gespeichert werden"), + onError: (error: Error) => + toast.error(error.message || "Schleuderung konnte nicht gespeichert werden"), }); function handleSubmit(event: FormEvent) { event.preventDefault(); + if (harvestIds.length === 0) { + toast.error("Bitte mindestens eine Ernte auswählen."); + return; + } mutation.mutate(); } return ( +
(honeyBatch.status); const [honeyType, setHoneyType] = useState(honeyBatch.honeyType ?? ""); const [amount, setAmount] = useState(honeyBatch.amount?.toString() ?? ""); const [waterContent, setWaterContent] = useState(honeyBatch.waterContent?.toString() ?? ""); + const { data: extractions } = useQuery({ + queryKey: ["extractions"], + queryFn: () => fetchExtractions(), + }); + const mutation = useMutation({ mutationFn: () => updateHoneyBatch(honeyBatch.id, { + batchNumber, + extractionId, + status, honeyType: honeyType || undefined, amount: amount ? Number(amount) : undefined, waterContent: waterContent ? Number(waterContent) : undefined, @@ -60,6 +80,54 @@ export function HoneyBatchEditDialog({ honeyBatch }: { honeyBatch: HoneyBatch }) Honigcharge bearbeiten +
+ + setBatchNumber(event.target.value)} + /> +
+
+ + +
+
+ + +
updateStorageContainer(container.id, { + code, containerType: containerType || undefined, capacity: capacity ? Number(capacity) : undefined, location: location || undefined, @@ -59,6 +61,15 @@ export function EditStorageContainerDialog({ container }: { container: StorageCo Lagerbehälter bearbeiten +
+ + setCode(event.target.value)} + /> +
(task.priority); const [dueDate, setDueDate] = useState(task.dueDate?.slice(0, 10) ?? ""); const [assignedUserId, setAssignedUserId] = useState(task.assignedUserId ?? ""); + const [relatedType, setRelatedType] = useState(task.relatedType); + const [relatedId, setRelatedId] = useState(task.relatedId ?? ""); // Assigning a task requires users:read, which not every role has - if the // request fails, the assignee field below is simply omitted rather than @@ -53,6 +56,8 @@ export function TaskEditDialog({ task }: { task: Task }) { priority, dueDate: dueDate ? new Date(dueDate).toISOString() : undefined, assignedUserId: assignedUserId || undefined, + relatedType, + relatedId: relatedType === "GENERAL" ? undefined : relatedId || undefined, }), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["tasks"] }); @@ -133,6 +138,13 @@ export function TaskEditDialog({ task }: { task: Task }) {
) : null} +
void; + label?: string; +} + +/** + * Picks the colony a record belongs to. Every colony-scoped record + * (inspection, treatment, feeding, varroa measurement, swarm control check) + * can be re-filed under a different colony after the fact, because filing one + * against the wrong colony is a data-entry error rather than a fact of + * beekeeping - and the only alternative repair would be deleting the record. + * + * Dissolved colonies stay in the list: a record already filed against one has + * to remain visible and selectable, otherwise opening the form would silently + * re-file it somewhere else. + */ +export function ColonySelectField({ + id, + value, + onChange, + label = "Volk", +}: ColonySelectFieldProps) { + const { data: colonies } = useQuery({ + queryKey: ["colonies"], + queryFn: () => fetchColonies(), + }); + + return ( +
+ + +
+ ); +} diff --git a/ApiCore-Frontend/src/components/harvest-select-field.tsx b/ApiCore-Frontend/src/components/harvest-select-field.tsx new file mode 100644 index 0000000..b61cdde --- /dev/null +++ b/ApiCore-Frontend/src/components/harvest-select-field.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { fetchHarvests } from "@/lib/harvests/api"; +import { Label } from "@/components/ui/label"; + +interface HarvestSelectFieldProps { + idPrefix: string; + value: string[]; + onChange: (harvestIds: string[]) => void; +} + +/** + * Picks the harvests an extraction was made from. The set stays editable after + * the fact: an extraction linked to the wrong harvest breaks the traceability + * chain from colony to jar, and re-pointing it is the only repair that does + * not require deleting the extraction and everything downstream of it. + */ +export function HarvestSelectField({ + idPrefix, + value, + onChange, +}: HarvestSelectFieldProps) { + const { data: harvests } = useQuery({ + queryKey: ["harvests"], + queryFn: () => fetchHarvests(), + }); + + function toggle(harvestId: string, checked: boolean) { + onChange( + checked ? [...value, harvestId] : value.filter((id) => id !== harvestId), + ); + } + + return ( +
+ + {(harvests ?? []).length === 0 ? ( +

Keine Ernten erfasst.

+ ) : null} +
+ {(harvests ?? []).map((harvest) => ( + + ))} +
+
+ ); +} diff --git a/ApiCore-Frontend/src/components/task-related-field.tsx b/ApiCore-Frontend/src/components/task-related-field.tsx new file mode 100644 index 0000000..7dfa1b9 --- /dev/null +++ b/ApiCore-Frontend/src/components/task-related-field.tsx @@ -0,0 +1,153 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { TaskRelatedType } from "@/lib/tasks/types"; +import { + TASK_RELATED_TYPES, + TASK_RELATED_TYPE_LABELS, +} from "@/lib/tasks/labels"; +import { fetchApiaries } from "@/lib/apiaries/api"; +import { fetchColonies } from "@/lib/colonies/api"; +import { fetchHoneyBatches } from "@/lib/honey-batches/api"; +import { fetchQueens } from "@/lib/queens/api"; +import { fetchStorageContainers } from "@/lib/storage/api"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface RelatedOption { + id: string; + label: string; +} + +interface TaskRelatedFieldProps { + idPrefix: string; + relatedType: TaskRelatedType; + relatedId: string; + onRelatedTypeChange: (relatedType: TaskRelatedType) => void; + onRelatedIdChange: (relatedId: string) => void; +} + +/** + * Picks what a task refers to. The reference is polymorphic, so the second + * select only loads the list matching the chosen type, and GENERAL has no + * record to point at at all. + * + * A task created from an inspection carries the reference automatically, but + * it stays editable: a task filed against the wrong colony sends a beekeeper + * to the wrong hive. + */ +export function TaskRelatedField({ + idPrefix, + relatedType, + relatedId, + onRelatedTypeChange, + onRelatedIdChange, +}: TaskRelatedFieldProps) { + const colonies = useQuery({ + queryKey: ["colonies"], + queryFn: () => fetchColonies(), + enabled: relatedType === "COLONY", + }); + const apiaries = useQuery({ + queryKey: ["apiaries"], + queryFn: () => fetchApiaries(), + enabled: relatedType === "APIARY", + }); + const queens = useQuery({ + queryKey: ["queens"], + queryFn: () => fetchQueens(), + enabled: relatedType === "QUEEN", + }); + const honeyBatches = useQuery({ + queryKey: ["honey-batches"], + queryFn: () => fetchHoneyBatches(), + enabled: relatedType === "HONEY_BATCH", + }); + const containers = useQuery({ + queryKey: ["storage-containers"], + queryFn: () => fetchStorageContainers(), + enabled: relatedType === "STORAGE", + }); + + const optionsByType: Record = { + COLONY: (colonies.data ?? []).map((colony) => ({ + id: colony.id, + label: colony.code, + })), + APIARY: (apiaries.data ?? []).map((apiary) => ({ + id: apiary.id, + label: apiary.name, + })), + QUEEN: (queens.data ?? []).map((queen) => ({ + id: queen.id, + label: queen.code, + })), + HONEY_BATCH: (honeyBatches.data ?? []).map((honeyBatch) => ({ + id: honeyBatch.id, + label: honeyBatch.batchNumber, + })), + STORAGE: (containers.data ?? []).map((container) => ({ + id: container.id, + label: container.code, + })), + GENERAL: [], + }; + const options = optionsByType[relatedType]; + + return ( + <> +
+ + +
+ {relatedType === "GENERAL" ? null : ( +
+ + +
+ )} + + ); +} diff --git a/ApiCore-Frontend/src/lib/colonies/types.ts b/ApiCore-Frontend/src/lib/colonies/types.ts index 38be574..db0d553 100644 --- a/ApiCore-Frontend/src/lib/colonies/types.ts +++ b/ApiCore-Frontend/src/lib/colonies/types.ts @@ -47,6 +47,7 @@ export interface CreateColonyInput { } export interface UpdateColonyInput { + code?: string; hiveType?: string; originText?: string; colonyType?: ColonyType; diff --git a/ApiCore-Frontend/src/lib/extractions/types.ts b/ApiCore-Frontend/src/lib/extractions/types.ts index 673f479..6096dfe 100644 --- a/ApiCore-Frontend/src/lib/extractions/types.ts +++ b/ApiCore-Frontend/src/lib/extractions/types.ts @@ -35,6 +35,8 @@ export interface CreateExtractionInput { } export interface UpdateExtractionInput { + /** Replaces the whole set of source harvests. */ + harvestIds?: string[]; date?: string; amount?: number; room?: string; diff --git a/ApiCore-Frontend/src/lib/feedings/types.ts b/ApiCore-Frontend/src/lib/feedings/types.ts index 3dc4d43..fe45d7d 100644 --- a/ApiCore-Frontend/src/lib/feedings/types.ts +++ b/ApiCore-Frontend/src/lib/feedings/types.ts @@ -31,6 +31,8 @@ export interface CreateFeedingInput { } export interface UpdateFeedingInput { + /** Re-files the feeding under a different colony. */ + colonyId?: string; date?: string; feedType?: FeedType; product?: string; diff --git a/ApiCore-Frontend/src/lib/harvests/types.ts b/ApiCore-Frontend/src/lib/harvests/types.ts index 0e3d395..24ea5c8 100644 --- a/ApiCore-Frontend/src/lib/harvests/types.ts +++ b/ApiCore-Frontend/src/lib/harvests/types.ts @@ -33,6 +33,9 @@ export interface CreateHarvestInput { } export interface UpdateHarvestInput { + apiaryId?: string; + /** Replaces the whole set of participating colonies. */ + colonyIds?: string[]; date?: string; honeyType?: string; honeySuperCount?: number; diff --git a/ApiCore-Frontend/src/lib/honey-batches/types.ts b/ApiCore-Frontend/src/lib/honey-batches/types.ts index 6a4705f..21fa744 100644 --- a/ApiCore-Frontend/src/lib/honey-batches/types.ts +++ b/ApiCore-Frontend/src/lib/honey-batches/types.ts @@ -90,9 +90,12 @@ export interface CreateHoneyBatchInput { } export interface UpdateHoneyBatchInput { + batchNumber?: string; + extractionId?: string; honeyType?: string; amount?: number; waterContent?: number; + status?: HoneyBatchStatus; } export interface CreateHoneyMeasurementInput { diff --git a/ApiCore-Frontend/src/lib/inspections/types.ts b/ApiCore-Frontend/src/lib/inspections/types.ts index d668143..41b7668 100644 --- a/ApiCore-Frontend/src/lib/inspections/types.ts +++ b/ApiCore-Frontend/src/lib/inspections/types.ts @@ -50,6 +50,8 @@ export interface CreateInspectionInput { } export interface UpdateInspectionInput { + /** Re-files the inspection under a different colony. */ + colonyId?: string; date?: string; weather?: string; temperature?: number; diff --git a/ApiCore-Frontend/src/lib/storage/types.ts b/ApiCore-Frontend/src/lib/storage/types.ts index 3462191..27d95e3 100644 --- a/ApiCore-Frontend/src/lib/storage/types.ts +++ b/ApiCore-Frontend/src/lib/storage/types.ts @@ -42,6 +42,7 @@ export interface CreateStorageContainerInput { } export interface UpdateStorageContainerInput { + code?: string; containerType?: string; capacity?: number; location?: string; diff --git a/ApiCore-Frontend/src/lib/swarm-control/types.ts b/ApiCore-Frontend/src/lib/swarm-control/types.ts index 37338e4..5a552df 100644 --- a/ApiCore-Frontend/src/lib/swarm-control/types.ts +++ b/ApiCore-Frontend/src/lib/swarm-control/types.ts @@ -32,6 +32,8 @@ export interface CreateSwarmControlCheckInput { } export interface UpdateSwarmControlCheckInput { + /** Re-files the check under a different colony. */ + colonyId?: string; date?: string; queenCellsFound?: number; queenCellsCapped?: boolean; diff --git a/ApiCore-Frontend/src/lib/tasks/labels.ts b/ApiCore-Frontend/src/lib/tasks/labels.ts index 9710974..115678a 100644 --- a/ApiCore-Frontend/src/lib/tasks/labels.ts +++ b/ApiCore-Frontend/src/lib/tasks/labels.ts @@ -1,4 +1,4 @@ -import { TaskPriority, TaskStatus } from "./types"; +import { TaskPriority, TaskRelatedType, TaskStatus } from "./types"; export const TASK_STATUSES: TaskStatus[] = ["OPEN", "IN_PROGRESS", "DONE", "CANCELLED"]; @@ -17,3 +17,21 @@ export const TASK_PRIORITY_LABELS: Record = { HIGH: "Hoch", URGENT: "Dringend", }; + +export const TASK_RELATED_TYPES: TaskRelatedType[] = [ + "GENERAL", + "COLONY", + "APIARY", + "QUEEN", + "HONEY_BATCH", + "STORAGE", +]; + +export const TASK_RELATED_TYPE_LABELS: Record = { + GENERAL: "Allgemein", + COLONY: "Volk", + APIARY: "Bienenstand", + QUEEN: "Königin", + HONEY_BATCH: "Honigcharge", + STORAGE: "Lagerbehälter", +}; diff --git a/ApiCore-Frontend/src/lib/tasks/types.ts b/ApiCore-Frontend/src/lib/tasks/types.ts index 1e8e817..4fae51b 100644 --- a/ApiCore-Frontend/src/lib/tasks/types.ts +++ b/ApiCore-Frontend/src/lib/tasks/types.ts @@ -44,4 +44,6 @@ export interface UpdateTaskInput { dueDate?: string; assignedUserId?: string; status?: TaskStatus; + relatedType?: TaskRelatedType; + relatedId?: string; } diff --git a/ApiCore-Frontend/src/lib/treatments/types.ts b/ApiCore-Frontend/src/lib/treatments/types.ts index 1c741f4..6d79eb0 100644 --- a/ApiCore-Frontend/src/lib/treatments/types.ts +++ b/ApiCore-Frontend/src/lib/treatments/types.ts @@ -32,6 +32,8 @@ export interface CreateTreatmentInput { } export interface UpdateTreatmentInput { + /** Re-files the treatment under a different colony. */ + colonyId?: string; date?: string; startDate?: string; endDate?: string; diff --git a/ApiCore-Frontend/src/lib/varroa/types.ts b/ApiCore-Frontend/src/lib/varroa/types.ts index d1f661d..e7a8650 100644 --- a/ApiCore-Frontend/src/lib/varroa/types.ts +++ b/ApiCore-Frontend/src/lib/varroa/types.ts @@ -30,6 +30,8 @@ export interface CreateVarroaMeasurementInput { } export interface UpdateVarroaMeasurementInput { + /** Re-files the measurement under a different colony. */ + colonyId?: string; date?: string; method?: VarroaMethod; durationMinutes?: number;