From 68f459001a15d16180708847b00ed0c594521575 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Tue, 25 Aug 2026 04:28:37 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Bearbeiten=20f=C3=BCr=20alle=20Log-Eint?= =?UTF-8?q?r=C3=A4ge=20(Aufgaben,=20Durchsichten,=20Behandlungen,=20F?= =?UTF-8?q?=C3=BCtterungen,=20Varroa-Messungen,=20Ernten,=20Schleuderungen?= =?UTF-8?q?,=20Abf=C3=BCllungen)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher liessen sich diese acht Objekttypen nur anlegen, nie nachträglich korrigieren – Aufgaben hatten zwar schon einen PATCH-Endpunkt, aber keine Bearbeiten-UI, die übrigen sieben hatten weder Backend- noch Frontend-Unterstützung. Jetzt bekommt jeder Typ einen PATCH :id Endpunkt (UpdateXDto per PartialType(PickType(...)) nach demselben Muster wie UpdateColonyDto/UpdateApiaryDto, mit Audit-Log-Eintrag inkl. Vorher-/Nachher-Stand) sowie eine Bearbeiten-UI: - Aufgaben: Dialog auf der Aufgabenliste - Durchsichten/Behandlungen/Fütterungen/Varroa-Messungen: Dialog pro Listeneintrag auf der Volk-Detailseite - Ernten: Dialog auf der eigenen Ernte-Detailseite - Schleuderungen: Dialog direkt in der Rückverfolgbarkeits-Karte der Honigcharge (keine eigene Detailseite nötig); Erntezeilen dort verlinken jetzt zur Ernte-Detailseite statt sie zu duplizieren - Abfüllungen: Dialog auf der Abfüllungs-Detailseite, bewusst nur Datum/MHD/Losnummer editierbar – Glasgröße/-anzahl bleiben unveränderlich, weil sie bereits unumkehrbar die Honigcharge-Menge reduziert haben Relationsstiftende Fremdschlüssel (colonyId, apiaryId/colonyIds, harvestIds, honeyBatchId) bleiben überall bewusst nicht editierbar, konsistent mit dem bestehenden apiaryId-Ausschluss bei UpdateColonyDto. --- .../src/bottling/bottling.controller.ts | 21 ++- .../src/bottling/bottling.service.spec.ts | 33 ++++ .../src/bottling/bottling.service.ts | 33 ++++ .../bottling/dto/update-bottling-batch.dto.ts | 11 ++ .../extractions/dto/update-extraction.dto.ts | 13 ++ .../src/extractions/extractions.controller.ts | 13 +- .../extractions/extractions.service.spec.ts | 29 ++- .../src/extractions/extractions.service.ts | 19 ++ .../src/feedings/dto/update-feeding.dto.ts | 14 ++ .../src/feedings/feedings.controller.ts | 13 +- .../src/feedings/feedings.service.spec.ts | 25 +++ .../src/feedings/feedings.service.ts | 22 +++ .../src/harvests/dto/update-harvest.dto.ts | 13 ++ .../src/harvests/harvests.controller.ts | 13 +- .../src/harvests/harvests.service.spec.ts | 29 ++- .../src/harvests/harvests.service.ts | 19 ++ .../inspections/dto/update-inspection.dto.ts | 27 +++ .../src/inspections/inspections.controller.ts | 13 +- .../inspections/inspections.service.spec.ts | 29 +++ .../src/inspections/inspections.service.ts | 24 +++ .../treatments/dto/update-treatment.dto.ts | 17 ++ .../src/treatments/treatments.controller.ts | 13 +- .../src/treatments/treatments.service.spec.ts | 25 +++ .../src/treatments/treatments.service.ts | 27 +++ .../dto/update-varroa-measurement.dto.ts | 14 ++ .../src/varroa/varroa.controller.ts | 13 +- .../src/varroa/varroa.service.spec.ts | 29 ++- ApiCore-Backend/src/varroa/varroa.service.ts | 23 +++ .../bottling/[id]/bottling-edit-dialog.tsx | 97 ++++++++++ .../src/app/(app)/bottling/[id]/page.tsx | 14 +- .../(app)/colonies/[id]/colony-feedings.tsx | 10 +- .../colonies/[id]/colony-inspections.tsx | 10 +- .../(app)/colonies/[id]/colony-treatments.tsx | 10 +- .../app/(app)/colonies/[id]/colony-varroa.tsx | 10 +- .../colonies/[id]/feeding-edit-dialog.tsx | 152 +++++++++++++++ .../colonies/[id]/inspection-edit-dialog.tsx | 176 ++++++++++++++++++ .../colonies/[id]/treatment-edit-dialog.tsx | 125 +++++++++++++ .../colonies/[id]/varroa-edit-dialog.tsx | 153 +++++++++++++++ .../harvests/[id]/harvest-edit-dialog.tsx | 118 ++++++++++++ .../src/app/(app)/harvests/[id]/page.tsx | 20 +- .../[id]/extraction-edit-dialog.tsx | 158 ++++++++++++++++ .../src/app/(app)/honey-batches/[id]/page.tsx | 25 ++- ApiCore-Frontend/src/app/(app)/tasks/page.tsx | 24 ++- .../src/app/(app)/tasks/task-edit-dialog.tsx | 122 ++++++++++++ ApiCore-Frontend/src/lib/bottling/api.ts | 17 +- ApiCore-Frontend/src/lib/bottling/types.ts | 7 + ApiCore-Frontend/src/lib/extractions/api.ts | 16 +- ApiCore-Frontend/src/lib/extractions/types.ts | 9 + ApiCore-Frontend/src/lib/feedings/api.ts | 17 +- ApiCore-Frontend/src/lib/feedings/types.ts | 10 + ApiCore-Frontend/src/lib/harvests/api.ts | 9 +- ApiCore-Frontend/src/lib/harvests/types.ts | 9 + ApiCore-Frontend/src/lib/inspections/api.ts | 11 ++ ApiCore-Frontend/src/lib/inspections/types.ts | 15 ++ ApiCore-Frontend/src/lib/treatments/api.ts | 12 +- ApiCore-Frontend/src/lib/treatments/types.ts | 13 ++ ApiCore-Frontend/src/lib/varroa/api.ts | 16 +- ApiCore-Frontend/src/lib/varroa/types.ts | 10 + 58 files changed, 1909 insertions(+), 60 deletions(-) create mode 100644 ApiCore-Backend/src/bottling/dto/update-bottling-batch.dto.ts create mode 100644 ApiCore-Backend/src/extractions/dto/update-extraction.dto.ts create mode 100644 ApiCore-Backend/src/feedings/dto/update-feeding.dto.ts create mode 100644 ApiCore-Backend/src/harvests/dto/update-harvest.dto.ts create mode 100644 ApiCore-Backend/src/inspections/dto/update-inspection.dto.ts create mode 100644 ApiCore-Backend/src/treatments/dto/update-treatment.dto.ts create mode 100644 ApiCore-Backend/src/varroa/dto/update-varroa-measurement.dto.ts create mode 100644 ApiCore-Frontend/src/app/(app)/bottling/[id]/bottling-edit-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/colonies/[id]/feeding-edit-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/colonies/[id]/inspection-edit-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/colonies/[id]/treatment-edit-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/colonies/[id]/varroa-edit-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/harvests/[id]/harvest-edit-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/honey-batches/[id]/extraction-edit-dialog.tsx create mode 100644 ApiCore-Frontend/src/app/(app)/tasks/task-edit-dialog.tsx diff --git a/ApiCore-Backend/src/bottling/bottling.controller.ts b/ApiCore-Backend/src/bottling/bottling.controller.ts index 56ddb73..8c455aa 100644 --- a/ApiCore-Backend/src/bottling/bottling.controller.ts +++ b/ApiCore-Backend/src/bottling/bottling.controller.ts @@ -1,10 +1,19 @@ -import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Query, +} 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 { CreateBottlingBatchDto } from './dto/create-bottling-batch.dto'; +import { UpdateBottlingBatchDto } from './dto/update-bottling-batch.dto'; import { BottlingService } from './bottling.service'; @ApiTags('bottling') @@ -31,4 +40,14 @@ export class BottlingController { ) { return this.bottlingService.create(dto, actor.id); } + + @Roles(Role.ADMIN, Role.BEEKEEPER) + @Patch(':id') + update( + @Param('id') id: string, + @Body() dto: UpdateBottlingBatchDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.bottlingService.update(id, dto, actor.id); + } } diff --git a/ApiCore-Backend/src/bottling/bottling.service.spec.ts b/ApiCore-Backend/src/bottling/bottling.service.spec.ts index b9c9476..9464bd4 100644 --- a/ApiCore-Backend/src/bottling/bottling.service.spec.ts +++ b/ApiCore-Backend/src/bottling/bottling.service.spec.ts @@ -13,6 +13,7 @@ describe('BottlingService', () => { findUnique: jest.Mock; findMany: jest.Mock; create: jest.Mock; + update: jest.Mock; }; $transaction: jest.Mock; }; @@ -34,6 +35,7 @@ describe('BottlingService', () => { findUnique: jest.fn(), findMany: jest.fn(), create: jest.fn(), + update: jest.fn(), }, $transaction: jest.fn((callback: (txArg: typeof tx) => unknown) => callback(tx), @@ -124,4 +126,35 @@ describe('BottlingService', () => { }), ); }); + + it('update writes an audit log entry with before and after data', async () => { + const before = { id: 'b1', lotNumber: 'L-1' }; + const updated = { id: 'b1', lotNumber: 'L-2' }; + prisma.bottlingBatch.findUnique + .mockResolvedValueOnce(before) + .mockResolvedValueOnce(updated); + prisma.bottlingBatch.update.mockResolvedValue(updated); + + const result = await service.update('b1', { lotNumber: 'L-2' }, 'user-1'); + + expect(result).toEqual(updated); + expect(auditLog.record).toHaveBeenCalledWith( + expect.objectContaining({ + entityType: 'BottlingBatch', + entityId: 'b1', + action: 'UPDATE', + beforeData: before, + afterData: updated, + }), + ); + }); + + it('rejects updating to a product that does not exist', async () => { + prisma.bottlingBatch.findUnique.mockResolvedValueOnce({ id: 'b1' }); + prisma.product.findUnique.mockResolvedValueOnce(null); + + await expect( + service.update('b1', { productId: 'missing' }, 'user-1'), + ).rejects.toBeInstanceOf(NotFoundException); + }); }); diff --git a/ApiCore-Backend/src/bottling/bottling.service.ts b/ApiCore-Backend/src/bottling/bottling.service.ts index 457f902..b97582b 100644 --- a/ApiCore-Backend/src/bottling/bottling.service.ts +++ b/ApiCore-Backend/src/bottling/bottling.service.ts @@ -8,6 +8,7 @@ import { AuditLogService } from '../audit/audit-log.service'; import { WebhooksService } from '../webhooks/webhooks.service'; import { BottlingBatch } from '../../generated/prisma'; import { CreateBottlingBatchDto } from './dto/create-bottling-batch.dto'; +import { UpdateBottlingBatchDto } from './dto/update-bottling-batch.dto'; const bottlingBatchInclude = { honeyBatch: true, @@ -109,4 +110,36 @@ export class BottlingService { await this.webhooks.dispatch('batch.updated', created.honeyBatch); return created; } + + async update(id: string, dto: UpdateBottlingBatchDto, actingUserId: string) { + const before = await this.findById(id); + if (dto.productId) { + const product = await this.prisma.product.findUnique({ + where: { id: dto.productId }, + }); + if (!product) { + throw new NotFoundException('Product not found'); + } + } + await this.prisma.bottlingBatch.update({ + where: { id }, + data: { + ...dto, + date: dto.date ? new Date(dto.date) : undefined, + bestBeforeDate: dto.bestBeforeDate + ? new Date(dto.bestBeforeDate) + : undefined, + }, + }); + const bottlingBatch = await this.findById(id); + await this.auditLog.record({ + entityType: 'BottlingBatch', + entityId: id, + action: 'UPDATE', + userId: actingUserId, + beforeData: before, + afterData: bottlingBatch, + }); + return bottlingBatch; + } } diff --git a/ApiCore-Backend/src/bottling/dto/update-bottling-batch.dto.ts b/ApiCore-Backend/src/bottling/dto/update-bottling-batch.dto.ts new file mode 100644 index 0000000..c6a4c6a --- /dev/null +++ b/ApiCore-Backend/src/bottling/dto/update-bottling-batch.dto.ts @@ -0,0 +1,11 @@ +import { PartialType, PickType } from '@nestjs/swagger'; +import { CreateBottlingBatchDto } from './create-bottling-batch.dto'; + +export class UpdateBottlingBatchDto extends PartialType( + PickType(CreateBottlingBatchDto, [ + 'productId', + 'date', + 'bestBeforeDate', + 'lotNumber', + ] as const), +) {} diff --git a/ApiCore-Backend/src/extractions/dto/update-extraction.dto.ts b/ApiCore-Backend/src/extractions/dto/update-extraction.dto.ts new file mode 100644 index 0000000..0c94f35 --- /dev/null +++ b/ApiCore-Backend/src/extractions/dto/update-extraction.dto.ts @@ -0,0 +1,13 @@ +import { PartialType, PickType } from '@nestjs/swagger'; +import { CreateExtractionDto } from './create-extraction.dto'; + +export class UpdateExtractionDto extends PartialType( + PickType(CreateExtractionDto, [ + 'date', + 'amount', + 'room', + 'waterContent', + 'temperature', + 'notes', + ] as const), +) {} diff --git a/ApiCore-Backend/src/extractions/extractions.controller.ts b/ApiCore-Backend/src/extractions/extractions.controller.ts index c6bfc5e..12529c5 100644 --- a/ApiCore-Backend/src/extractions/extractions.controller.ts +++ b/ApiCore-Backend/src/extractions/extractions.controller.ts @@ -1,10 +1,11 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +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 { CreateExtractionDto } from './dto/create-extraction.dto'; +import { UpdateExtractionDto } from './dto/update-extraction.dto'; import { ExtractionsService } from './extractions.service'; @ApiTags('extractions') @@ -31,4 +32,14 @@ export class ExtractionsController { ) { return this.extractionsService.create(dto, actor.id); } + + @Roles(Role.ADMIN, Role.BEEKEEPER) + @Patch(':id') + update( + @Param('id') id: string, + @Body() dto: UpdateExtractionDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.extractionsService.update(id, dto, actor.id); + } } diff --git a/ApiCore-Backend/src/extractions/extractions.service.spec.ts b/ApiCore-Backend/src/extractions/extractions.service.spec.ts index 1f0ce9e..0b12b48 100644 --- a/ApiCore-Backend/src/extractions/extractions.service.spec.ts +++ b/ApiCore-Backend/src/extractions/extractions.service.spec.ts @@ -6,7 +6,7 @@ import { AuditLogService } from '../audit/audit-log.service'; describe('ExtractionsService', () => { let service: ExtractionsService; let prisma: { - extraction: { findUnique: jest.Mock }; + extraction: { findUnique: jest.Mock; update: jest.Mock }; $transaction: jest.Mock; }; let auditLog: { record: jest.Mock }; @@ -23,7 +23,7 @@ describe('ExtractionsService', () => { extractionHarvest: { createMany: jest.fn() }, }; prisma = { - extraction: { findUnique: jest.fn() }, + extraction: { findUnique: jest.fn(), update: jest.fn() }, $transaction: jest.fn((callback: (txArg: typeof tx) => unknown) => callback(tx), ), @@ -58,4 +58,29 @@ describe('ExtractionsService', () => { { extractionId: extraction.id, harvestId: 'harvest-2' }, ]); }); + + it('update writes an audit log entry with before and after data', async () => { + const updated = { ...extraction, notes: 'geändert' }; + prisma.extraction.findUnique + .mockResolvedValueOnce(extraction) + .mockResolvedValueOnce(updated); + prisma.extraction.update.mockResolvedValue(updated); + + const result = await service.update( + 'extraction-1', + { notes: 'geändert' }, + 'user-1', + ); + + expect(result).toEqual(updated); + expect(auditLog.record).toHaveBeenCalledWith( + expect.objectContaining({ + entityType: 'Extraction', + entityId: 'extraction-1', + action: 'UPDATE', + beforeData: extraction, + afterData: updated, + }), + ); + }); }); diff --git a/ApiCore-Backend/src/extractions/extractions.service.ts b/ApiCore-Backend/src/extractions/extractions.service.ts index 3f94ec5..75d509e 100644 --- a/ApiCore-Backend/src/extractions/extractions.service.ts +++ b/ApiCore-Backend/src/extractions/extractions.service.ts @@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; import { CreateExtractionDto } from './dto/create-extraction.dto'; +import { UpdateExtractionDto } from './dto/update-extraction.dto'; const extractionInclude = { harvests: { @@ -66,4 +67,22 @@ export class ExtractionsService { }); return this.findById(extraction.id); } + + 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 extraction = await this.findById(id); + await this.auditLog.record({ + entityType: 'Extraction', + entityId: id, + action: 'UPDATE', + userId: actingUserId, + beforeData: before, + afterData: extraction, + }); + return extraction; + } } diff --git a/ApiCore-Backend/src/feedings/dto/update-feeding.dto.ts b/ApiCore-Backend/src/feedings/dto/update-feeding.dto.ts new file mode 100644 index 0000000..aef079f --- /dev/null +++ b/ApiCore-Backend/src/feedings/dto/update-feeding.dto.ts @@ -0,0 +1,14 @@ +import { PartialType, PickType } 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), +) {} diff --git a/ApiCore-Backend/src/feedings/feedings.controller.ts b/ApiCore-Backend/src/feedings/feedings.controller.ts index 743601e..5c3aea0 100644 --- a/ApiCore-Backend/src/feedings/feedings.controller.ts +++ b/ApiCore-Backend/src/feedings/feedings.controller.ts @@ -1,10 +1,11 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +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 { CreateFeedingDto } from './dto/create-feeding.dto'; +import { UpdateFeedingDto } from './dto/update-feeding.dto'; import { FeedingsService } from './feedings.service'; @ApiTags('feedings') @@ -31,4 +32,14 @@ export class FeedingsController { ) { return this.feedingsService.create(dto, actor.id); } + + @Roles(Role.ADMIN, Role.BEEKEEPER, Role.STAFF) + @Patch(':id') + update( + @Param('id') id: string, + @Body() dto: UpdateFeedingDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.feedingsService.update(id, dto, actor.id); + } } diff --git a/ApiCore-Backend/src/feedings/feedings.service.spec.ts b/ApiCore-Backend/src/feedings/feedings.service.spec.ts index f53a0f2..7e3f4fc 100644 --- a/ApiCore-Backend/src/feedings/feedings.service.spec.ts +++ b/ApiCore-Backend/src/feedings/feedings.service.spec.ts @@ -10,6 +10,7 @@ describe('FeedingsService', () => { findMany: jest.Mock; findUnique: jest.Mock; create: jest.Mock; + update: jest.Mock; groupBy: jest.Mock; }; }; @@ -23,6 +24,7 @@ describe('FeedingsService', () => { findMany: jest.fn(), findUnique: jest.fn(), create: jest.fn(), + update: jest.fn(), groupBy: jest.fn(), }, }; @@ -61,6 +63,29 @@ describe('FeedingsService', () => { expect(createArg.data.userId).toBe('user-1'); }); + it('update writes an audit log entry with before and after data', async () => { + prisma.feeding.findUnique.mockResolvedValue(feeding); + const updated = { ...feeding, notes: 'geändert' }; + prisma.feeding.update.mockResolvedValue(updated); + + const result = await service.update( + 'feeding-1', + { notes: 'geändert' }, + 'user-1', + ); + + expect(result).toBe(updated); + expect(auditLog.record).toHaveBeenCalledWith( + expect.objectContaining({ + entityType: 'Feeding', + entityId: 'feeding-1', + action: 'UPDATE', + beforeData: feeding, + afterData: updated, + }), + ); + }); + it('summaryByColony sums amounts per unit', async () => { prisma.feeding.groupBy.mockResolvedValue([ { unit: 'l', _sum: { amount: 5.5 } }, diff --git a/ApiCore-Backend/src/feedings/feedings.service.ts b/ApiCore-Backend/src/feedings/feedings.service.ts index 671dc88..8a00462 100644 --- a/ApiCore-Backend/src/feedings/feedings.service.ts +++ b/ApiCore-Backend/src/feedings/feedings.service.ts @@ -3,6 +3,7 @@ import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; import { Feeding } from '../../generated/prisma'; import { CreateFeedingDto } from './dto/create-feeding.dto'; +import { UpdateFeedingDto } from './dto/update-feeding.dto'; export interface FeedingSummaryEntry { unit: string; @@ -60,4 +61,25 @@ export class FeedingsService { }); return feeding; } + + async update( + id: string, + dto: UpdateFeedingDto, + actingUserId: string, + ): Promise { + const before = await this.findById(id); + const feeding = await this.prisma.feeding.update({ + where: { id }, + data: { ...dto, date: dto.date ? new Date(dto.date) : undefined }, + }); + await this.auditLog.record({ + entityType: 'Feeding', + entityId: id, + action: 'UPDATE', + userId: actingUserId, + beforeData: before, + afterData: feeding, + }); + return feeding; + } } diff --git a/ApiCore-Backend/src/harvests/dto/update-harvest.dto.ts b/ApiCore-Backend/src/harvests/dto/update-harvest.dto.ts new file mode 100644 index 0000000..b292218 --- /dev/null +++ b/ApiCore-Backend/src/harvests/dto/update-harvest.dto.ts @@ -0,0 +1,13 @@ +import { PartialType, PickType } from '@nestjs/swagger'; +import { CreateHarvestDto } from './create-harvest.dto'; + +export class UpdateHarvestDto extends PartialType( + PickType(CreateHarvestDto, [ + 'date', + 'honeyType', + 'honeySuperCount', + 'grossWeight', + 'netWeight', + 'notes', + ] as const), +) {} diff --git a/ApiCore-Backend/src/harvests/harvests.controller.ts b/ApiCore-Backend/src/harvests/harvests.controller.ts index 947a241..17d7268 100644 --- a/ApiCore-Backend/src/harvests/harvests.controller.ts +++ b/ApiCore-Backend/src/harvests/harvests.controller.ts @@ -1,10 +1,11 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +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 { CreateHarvestDto } from './dto/create-harvest.dto'; +import { UpdateHarvestDto } from './dto/update-harvest.dto'; import { HarvestsService } from './harvests.service'; @ApiTags('harvests') @@ -31,4 +32,14 @@ export class HarvestsController { ) { return this.harvestsService.create(dto, actor.id); } + + @Roles(Role.ADMIN, Role.BEEKEEPER, Role.STAFF) + @Patch(':id') + update( + @Param('id') id: string, + @Body() dto: UpdateHarvestDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.harvestsService.update(id, dto, actor.id); + } } diff --git a/ApiCore-Backend/src/harvests/harvests.service.spec.ts b/ApiCore-Backend/src/harvests/harvests.service.spec.ts index a0eea90..cde3a11 100644 --- a/ApiCore-Backend/src/harvests/harvests.service.spec.ts +++ b/ApiCore-Backend/src/harvests/harvests.service.spec.ts @@ -7,7 +7,7 @@ import { WebhooksService } from '../webhooks/webhooks.service'; describe('HarvestsService', () => { let service: HarvestsService; let prisma: { - harvest: { findUnique: jest.Mock }; + harvest: { findUnique: jest.Mock; update: jest.Mock }; $transaction: jest.Mock; }; let auditLog: { record: jest.Mock }; @@ -24,7 +24,7 @@ describe('HarvestsService', () => { harvestColony: { createMany: jest.fn() }, }; prisma = { - harvest: { findUnique: jest.fn() }, + harvest: { findUnique: jest.fn(), update: jest.fn() }, $transaction: jest.fn((callback: (txArg: typeof tx) => unknown) => callback(tx), ), @@ -65,4 +65,29 @@ describe('HarvestsService', () => { { harvestId: harvest.id, colonyId: 'colony-2' }, ]); }); + + it('update writes an audit log entry with before and after data', async () => { + const updated = { ...harvest, notes: 'geändert' }; + prisma.harvest.findUnique + .mockResolvedValueOnce(harvest) + .mockResolvedValueOnce(updated); + prisma.harvest.update.mockResolvedValue(updated); + + const result = await service.update( + 'harvest-1', + { notes: 'geändert' }, + 'user-1', + ); + + expect(result).toEqual(updated); + expect(auditLog.record).toHaveBeenCalledWith( + expect.objectContaining({ + entityType: 'Harvest', + entityId: 'harvest-1', + action: 'UPDATE', + beforeData: harvest, + afterData: updated, + }), + ); + }); }); diff --git a/ApiCore-Backend/src/harvests/harvests.service.ts b/ApiCore-Backend/src/harvests/harvests.service.ts index 30d4ce9..d66c20a 100644 --- a/ApiCore-Backend/src/harvests/harvests.service.ts +++ b/ApiCore-Backend/src/harvests/harvests.service.ts @@ -3,6 +3,7 @@ import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; import { WebhooksService } from '../webhooks/webhooks.service'; import { CreateHarvestDto } from './dto/create-harvest.dto'; +import { UpdateHarvestDto } from './dto/update-harvest.dto'; const harvestInclude = { colonies: { include: { colony: true } }, @@ -67,4 +68,22 @@ export class HarvestsService { await this.webhooks.dispatch('harvest.created', created); return created; } + + 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 harvest = await this.findById(id); + await this.auditLog.record({ + entityType: 'Harvest', + entityId: id, + action: 'UPDATE', + userId: actingUserId, + beforeData: before, + afterData: harvest, + }); + return harvest; + } } diff --git a/ApiCore-Backend/src/inspections/dto/update-inspection.dto.ts b/ApiCore-Backend/src/inspections/dto/update-inspection.dto.ts new file mode 100644 index 0000000..c71e5e5 --- /dev/null +++ b/ApiCore-Backend/src/inspections/dto/update-inspection.dto.ts @@ -0,0 +1,27 @@ +import { PartialType, PickType } 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), +) {} diff --git a/ApiCore-Backend/src/inspections/inspections.controller.ts b/ApiCore-Backend/src/inspections/inspections.controller.ts index f0cc7eb..f2993b2 100644 --- a/ApiCore-Backend/src/inspections/inspections.controller.ts +++ b/ApiCore-Backend/src/inspections/inspections.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +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'; @@ -6,6 +6,7 @@ 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 { UpdateInspectionDto } from './dto/update-inspection.dto'; import { InspectionsService } from './inspections.service'; @ApiTags('inspections') @@ -33,6 +34,16 @@ export class InspectionsController { return this.inspectionsService.create(dto, actor.id); } + @Roles(Role.ADMIN, Role.BEEKEEPER, Role.STAFF) + @Patch(':id') + update( + @Param('id') id: string, + @Body() dto: UpdateInspectionDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.inspectionsService.update(id, dto, actor.id); + } + @Roles(Role.ADMIN, Role.BEEKEEPER, Role.STAFF) @Post(':id/create-task') createTask( diff --git a/ApiCore-Backend/src/inspections/inspections.service.spec.ts b/ApiCore-Backend/src/inspections/inspections.service.spec.ts index 04bf7a4..51d85bb 100644 --- a/ApiCore-Backend/src/inspections/inspections.service.spec.ts +++ b/ApiCore-Backend/src/inspections/inspections.service.spec.ts @@ -3,6 +3,7 @@ import { InspectionsService } from './inspections.service'; import { PrismaService } from '../prisma/prisma.service'; import { WebhooksService } from '../webhooks/webhooks.service'; import { AutomationService } from '../automation/automation.service'; +import { AuditLogService } from '../audit/audit-log.service'; describe('InspectionsService', () => { let service: InspectionsService; @@ -11,9 +12,11 @@ describe('InspectionsService', () => { findMany: jest.Mock; findUnique: jest.Mock; create: jest.Mock; + update: jest.Mock; }; task: { create: jest.Mock }; }; + let auditLog: { record: jest.Mock }; const inspection = { id: 'inspection-1', @@ -27,6 +30,7 @@ describe('InspectionsService', () => { findMany: jest.fn(), findUnique: jest.fn(), create: jest.fn(), + update: jest.fn(), }, task: { create: jest.fn() }, }; @@ -34,10 +38,12 @@ describe('InspectionsService', () => { const automation = { evaluateInspection: jest.fn().mockResolvedValue(undefined), }; + auditLog = { record: jest.fn().mockResolvedValue(undefined) }; service = new InspectionsService( prisma as unknown as PrismaService, webhooks as unknown as WebhooksService, automation as unknown as AutomationService, + auditLog as unknown as AuditLogService, ); }); @@ -63,6 +69,29 @@ describe('InspectionsService', () => { expect(createArg.data.userId).toBe('user-1'); }); + it('update writes an audit log entry with before and after data', async () => { + prisma.inspection.findUnique.mockResolvedValue(inspection); + const updated = { ...inspection, notes: 'geändert' }; + prisma.inspection.update.mockResolvedValue(updated); + + const result = await service.update( + 'inspection-1', + { notes: 'geändert' }, + 'user-1', + ); + + expect(result).toBe(updated); + expect(auditLog.record).toHaveBeenCalledWith( + expect.objectContaining({ + entityType: 'Inspection', + entityId: 'inspection-1', + action: 'UPDATE', + beforeData: inspection, + afterData: updated, + }), + ); + }); + it('createTaskFromInspection links the task to the inspection and its colony', async () => { prisma.inspection.findUnique.mockResolvedValue(inspection); prisma.task.create.mockResolvedValue({ id: 'task-1' }); diff --git a/ApiCore-Backend/src/inspections/inspections.service.ts b/ApiCore-Backend/src/inspections/inspections.service.ts index 6695851..3330c79 100644 --- a/ApiCore-Backend/src/inspections/inspections.service.ts +++ b/ApiCore-Backend/src/inspections/inspections.service.ts @@ -2,9 +2,11 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { WebhooksService } from '../webhooks/webhooks.service'; import { AutomationService } from '../automation/automation.service'; +import { AuditLogService } from '../audit/audit-log.service'; import { Inspection, Task } from '../../generated/prisma'; import { CreateInspectionDto } from './dto/create-inspection.dto'; import { CreateTaskFromInspectionDto } from './dto/create-task-from-inspection.dto'; +import { UpdateInspectionDto } from './dto/update-inspection.dto'; @Injectable() export class InspectionsService { @@ -12,6 +14,7 @@ export class InspectionsService { private readonly prisma: PrismaService, private readonly webhooks: WebhooksService, private readonly automation: AutomationService, + private readonly auditLog: AuditLogService, ) {} async findAll(): Promise { @@ -47,6 +50,27 @@ export class InspectionsService { return inspection; } + async update( + id: string, + dto: UpdateInspectionDto, + actingUserId: string, + ): Promise { + const before = await this.findById(id); + const inspection = await this.prisma.inspection.update({ + where: { id }, + data: { ...dto, date: dto.date ? new Date(dto.date) : undefined }, + }); + await this.auditLog.record({ + entityType: 'Inspection', + entityId: id, + action: 'UPDATE', + userId: actingUserId, + beforeData: before, + afterData: inspection, + }); + return inspection; + } + async createTaskFromInspection( id: string, dto: CreateTaskFromInspectionDto, diff --git a/ApiCore-Backend/src/treatments/dto/update-treatment.dto.ts b/ApiCore-Backend/src/treatments/dto/update-treatment.dto.ts new file mode 100644 index 0000000..76ffa27 --- /dev/null +++ b/ApiCore-Backend/src/treatments/dto/update-treatment.dto.ts @@ -0,0 +1,17 @@ +import { PartialType, PickType } 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', + ] as const), +) {} diff --git a/ApiCore-Backend/src/treatments/treatments.controller.ts b/ApiCore-Backend/src/treatments/treatments.controller.ts index e9160e3..1c59889 100644 --- a/ApiCore-Backend/src/treatments/treatments.controller.ts +++ b/ApiCore-Backend/src/treatments/treatments.controller.ts @@ -1,10 +1,11 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +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 { CreateTreatmentDto } from './dto/create-treatment.dto'; +import { UpdateTreatmentDto } from './dto/update-treatment.dto'; import { TreatmentsService } from './treatments.service'; @ApiTags('treatments') @@ -31,4 +32,14 @@ export class TreatmentsController { ) { return this.treatmentsService.create(dto, actor.id); } + + @Roles(Role.ADMIN, Role.BEEKEEPER) + @Patch(':id') + update( + @Param('id') id: string, + @Body() dto: UpdateTreatmentDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.treatmentsService.update(id, dto, actor.id); + } } diff --git a/ApiCore-Backend/src/treatments/treatments.service.spec.ts b/ApiCore-Backend/src/treatments/treatments.service.spec.ts index b8c8717..3fdbe77 100644 --- a/ApiCore-Backend/src/treatments/treatments.service.spec.ts +++ b/ApiCore-Backend/src/treatments/treatments.service.spec.ts @@ -10,6 +10,7 @@ describe('TreatmentsService', () => { findMany: jest.Mock; findUnique: jest.Mock; create: jest.Mock; + update: jest.Mock; }; }; let auditLog: { record: jest.Mock }; @@ -26,6 +27,7 @@ describe('TreatmentsService', () => { findMany: jest.fn(), findUnique: jest.fn(), create: jest.fn(), + update: jest.fn(), }, }; auditLog = { record: jest.fn() }; @@ -58,6 +60,29 @@ describe('TreatmentsService', () => { expect(createArg.data.product).toBe('Ameisensäure'); }); + it('update writes an audit log entry with before and after data', async () => { + prisma.treatment.findUnique.mockResolvedValue(treatment); + const updated = { ...treatment, notes: 'geändert' }; + prisma.treatment.update.mockResolvedValue(updated); + + const result = await service.update( + 'treatment-1', + { notes: 'geändert' }, + 'user-1', + ); + + expect(result).toBe(updated); + expect(auditLog.record).toHaveBeenCalledWith( + expect.objectContaining({ + entityType: 'Treatment', + entityId: 'treatment-1', + action: 'UPDATE', + beforeData: treatment, + afterData: updated, + }), + ); + }); + it('findByColony orders results chronologically', async () => { prisma.treatment.findMany.mockResolvedValue([treatment]); diff --git a/ApiCore-Backend/src/treatments/treatments.service.ts b/ApiCore-Backend/src/treatments/treatments.service.ts index 30286f1..f625ac8 100644 --- a/ApiCore-Backend/src/treatments/treatments.service.ts +++ b/ApiCore-Backend/src/treatments/treatments.service.ts @@ -3,6 +3,7 @@ import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; import { Treatment } from '../../generated/prisma'; import { CreateTreatmentDto } from './dto/create-treatment.dto'; +import { UpdateTreatmentDto } from './dto/update-treatment.dto'; @Injectable() export class TreatmentsService { @@ -52,4 +53,30 @@ export class TreatmentsService { }); return treatment; } + + async update( + id: string, + dto: UpdateTreatmentDto, + actingUserId: string, + ): Promise { + const before = await this.findById(id); + const treatment = await this.prisma.treatment.update({ + where: { id }, + data: { + ...dto, + date: dto.date ? new Date(dto.date) : undefined, + startDate: dto.startDate ? new Date(dto.startDate) : undefined, + endDate: dto.endDate ? new Date(dto.endDate) : undefined, + }, + }); + await this.auditLog.record({ + entityType: 'Treatment', + entityId: id, + action: 'UPDATE', + userId: actingUserId, + beforeData: before, + afterData: treatment, + }); + return treatment; + } } diff --git a/ApiCore-Backend/src/varroa/dto/update-varroa-measurement.dto.ts b/ApiCore-Backend/src/varroa/dto/update-varroa-measurement.dto.ts new file mode 100644 index 0000000..697d628 --- /dev/null +++ b/ApiCore-Backend/src/varroa/dto/update-varroa-measurement.dto.ts @@ -0,0 +1,14 @@ +import { PartialType, PickType } from '@nestjs/swagger'; +import { CreateVarroaMeasurementDto } from './create-varroa-measurement.dto'; + +export class UpdateVarroaMeasurementDto extends PartialType( + PickType(CreateVarroaMeasurementDto, [ + 'date', + 'method', + 'durationMinutes', + 'miteCount', + 'calculatedValue', + 'assessment', + 'notes', + ] as const), +) {} diff --git a/ApiCore-Backend/src/varroa/varroa.controller.ts b/ApiCore-Backend/src/varroa/varroa.controller.ts index 5483908..bd54101 100644 --- a/ApiCore-Backend/src/varroa/varroa.controller.ts +++ b/ApiCore-Backend/src/varroa/varroa.controller.ts @@ -1,10 +1,11 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +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 { CreateVarroaMeasurementDto } from './dto/create-varroa-measurement.dto'; +import { UpdateVarroaMeasurementDto } from './dto/update-varroa-measurement.dto'; import { VarroaService } from './varroa.service'; @ApiTags('varroa-measurements') @@ -31,4 +32,14 @@ export class VarroaController { ) { return this.varroaService.create(dto, actor.id); } + + @Roles(Role.ADMIN, Role.BEEKEEPER) + @Patch(':id') + update( + @Param('id') id: string, + @Body() dto: UpdateVarroaMeasurementDto, + @CurrentUser() actor: AuthenticatedUser, + ) { + return this.varroaService.update(id, dto, actor.id); + } } diff --git a/ApiCore-Backend/src/varroa/varroa.service.spec.ts b/ApiCore-Backend/src/varroa/varroa.service.spec.ts index f747506..9237cc3 100644 --- a/ApiCore-Backend/src/varroa/varroa.service.spec.ts +++ b/ApiCore-Backend/src/varroa/varroa.service.spec.ts @@ -11,9 +11,11 @@ describe('VarroaService', () => { findMany: jest.Mock; findUnique: jest.Mock; create: jest.Mock; + update: jest.Mock; }; }; let auditLog: { record: jest.Mock }; + let automation: { evaluateVarroaMeasurement: jest.Mock }; const measurement = { id: 'varroa-1', @@ -27,10 +29,11 @@ describe('VarroaService', () => { findMany: jest.fn(), findUnique: jest.fn(), create: jest.fn(), + update: jest.fn(), }, }; auditLog = { record: jest.fn() }; - const automation = { + automation = { evaluateVarroaMeasurement: jest.fn().mockResolvedValue(undefined), }; service = new VarroaService( @@ -62,6 +65,30 @@ describe('VarroaService', () => { expect(createArg.data.userId).toBe('user-1'); }); + it('update writes an audit log entry and re-evaluates automation', async () => { + prisma.varroaMeasurement.findUnique.mockResolvedValue(measurement); + const updated = { ...measurement, miteCount: 12 }; + prisma.varroaMeasurement.update.mockResolvedValue(updated); + + const result = await service.update( + 'varroa-1', + { miteCount: 12 }, + 'user-1', + ); + + expect(result).toBe(updated); + expect(auditLog.record).toHaveBeenCalledWith( + expect.objectContaining({ + entityType: 'VarroaMeasurement', + entityId: 'varroa-1', + action: 'UPDATE', + beforeData: measurement, + afterData: updated, + }), + ); + expect(automation.evaluateVarroaMeasurement).toHaveBeenCalledWith(updated); + }); + it('findByColony orders results chronologically', async () => { prisma.varroaMeasurement.findMany.mockResolvedValue([measurement]); diff --git a/ApiCore-Backend/src/varroa/varroa.service.ts b/ApiCore-Backend/src/varroa/varroa.service.ts index 0b05d54..53e068c 100644 --- a/ApiCore-Backend/src/varroa/varroa.service.ts +++ b/ApiCore-Backend/src/varroa/varroa.service.ts @@ -4,6 +4,7 @@ import { AuditLogService } from '../audit/audit-log.service'; import { AutomationService } from '../automation/automation.service'; import { VarroaMeasurement } from '../../generated/prisma'; import { CreateVarroaMeasurementDto } from './dto/create-varroa-measurement.dto'; +import { UpdateVarroaMeasurementDto } from './dto/update-varroa-measurement.dto'; @Injectable() export class VarroaService { @@ -53,4 +54,26 @@ export class VarroaService { await this.automation.evaluateVarroaMeasurement(measurement); return measurement; } + + async update( + id: string, + dto: UpdateVarroaMeasurementDto, + actingUserId: string, + ): Promise { + const before = await this.findById(id); + const measurement = await this.prisma.varroaMeasurement.update({ + where: { id }, + data: { ...dto, date: dto.date ? new Date(dto.date) : undefined }, + }); + await this.auditLog.record({ + entityType: 'VarroaMeasurement', + entityId: id, + action: 'UPDATE', + userId: actingUserId, + beforeData: before, + afterData: measurement, + }); + await this.automation.evaluateVarroaMeasurement(measurement); + return measurement; + } } diff --git a/ApiCore-Frontend/src/app/(app)/bottling/[id]/bottling-edit-dialog.tsx b/ApiCore-Frontend/src/app/(app)/bottling/[id]/bottling-edit-dialog.tsx new file mode 100644 index 0000000..0ac3cd5 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/bottling/[id]/bottling-edit-dialog.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { Pencil } from "lucide-react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { updateBottlingBatch } from "@/lib/bottling/api"; +import { BottlingBatch } from "@/lib/bottling/types"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + +export function BottlingEditDialog({ bottlingBatch }: { bottlingBatch: BottlingBatch }) { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [date, setDate] = useState(bottlingBatch.date.slice(0, 10)); + const [bestBeforeDate, setBestBeforeDate] = useState( + bottlingBatch.bestBeforeDate.slice(0, 10), + ); + const [lotNumber, setLotNumber] = useState(bottlingBatch.lotNumber); + + const mutation = useMutation({ + mutationFn: () => + updateBottlingBatch(bottlingBatch.id, { + date: new Date(date).toISOString(), + bestBeforeDate: new Date(bestBeforeDate).toISOString(), + lotNumber, + }), + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: ["bottling-batches", bottlingBatch.id], + }); + setOpen(false); + }, + }); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + mutation.mutate(); + } + + return ( + + + + Bearbeiten + + } + /> + + + Abfüllung bearbeiten + +
+
+ + setDate(event.target.value)} + /> +
+
+ + setBestBeforeDate(event.target.value)} + /> +
+
+ + setLotNumber(event.target.value)} + /> +
+ +
+
+
+ ); +} diff --git a/ApiCore-Frontend/src/app/(app)/bottling/[id]/page.tsx b/ApiCore-Frontend/src/app/(app)/bottling/[id]/page.tsx index 4803dfe..c9592c4 100644 --- a/ApiCore-Frontend/src/app/(app)/bottling/[id]/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/bottling/[id]/page.tsx @@ -7,6 +7,7 @@ import { useQuery } from "@tanstack/react-query"; import { fetchBottlingBatch } from "@/lib/bottling/api"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { QrCodeDialog } from "@/components/qr-code-dialog"; +import { BottlingEditDialog } from "./bottling-edit-dialog"; export default function BottlingBatchDetailPage() { const params = useParams<{ id: string }>(); @@ -39,11 +40,14 @@ export default function BottlingBatchDetailPage() { Abfüllung {bottlingBatch.lotNumber} - +
+ + +

Honigcharge: {bottlingBatch.honeyBatch.batchNumber}

diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-feedings.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-feedings.tsx index 96aa45b..9bde6c8 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-feedings.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-feedings.tsx @@ -7,6 +7,7 @@ import { fetchColonyFeedings, fetchColonyFeedingsSummary } from "@/lib/feedings/ import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/empty-state"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { FeedingEditDialog } from "./feeding-edit-dialog"; export function ColonyFeedingsCard({ colonyId }: { colonyId: string }) { const { data: feedings, isLoading } = useQuery({ @@ -49,9 +50,12 @@ export function ColonyFeedingsCard({ colonyId }: { colonyId: string }) { ) : null} {feedings?.map((feeding) => (
-

- {new Date(feeding.date).toLocaleDateString("de-DE")} · {feeding.feedType} -

+
+

+ {new Date(feeding.date).toLocaleDateString("de-DE")} · {feeding.feedType} +

+ +

{feeding.amount} {feeding.unit} {feeding.product ? ` · ${feeding.product}` : ""} diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-inspections.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-inspections.tsx index 552f3a1..edc1e18 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-inspections.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-inspections.tsx @@ -7,6 +7,7 @@ import { fetchColonyInspections } from "@/lib/inspections/api"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/empty-state"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { InspectionEditDialog } from "./inspection-edit-dialog"; export function ColonyInspectionsCard({ colonyId }: { colonyId: string }) { const { data: inspections, isLoading } = useQuery({ @@ -37,9 +38,12 @@ export function ColonyInspectionsCard({ colonyId }: { colonyId: string }) { ) : null} {inspections?.map((inspection) => (

-

- {new Date(inspection.date).toLocaleDateString("de-DE")} -

+
+

+ {new Date(inspection.date).toLocaleDateString("de-DE")} +

+ +

{inspection.colonyStrength !== null ? `${inspection.colonyStrength} Waben` diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-treatments.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-treatments.tsx index f1c6006..488a1fd 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-treatments.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-treatments.tsx @@ -7,6 +7,7 @@ import { fetchColonyTreatments } from "@/lib/treatments/api"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/empty-state"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { TreatmentEditDialog } from "./treatment-edit-dialog"; export function ColonyTreatmentsCard({ colonyId }: { colonyId: string }) { const { data: treatments, isLoading } = useQuery({ @@ -37,9 +38,12 @@ export function ColonyTreatmentsCard({ colonyId }: { colonyId: string }) { ) : null} {treatments?.map((treatment) => (

-

- {new Date(treatment.date).toLocaleDateString("de-DE")} · {treatment.product} -

+
+

+ {new Date(treatment.date).toLocaleDateString("de-DE")} · {treatment.product} +

+ +

{treatment.amount ?? "—"} {treatment.method ? ` · ${treatment.method}` : ""} diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-varroa.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-varroa.tsx index f043aa6..6b81963 100644 --- a/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-varroa.tsx +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/colony-varroa.tsx @@ -7,6 +7,7 @@ import { fetchColonyVarroaMeasurements } from "@/lib/varroa/api"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/empty-state"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { VarroaEditDialog } from "./varroa-edit-dialog"; const CHART_WIDTH = 280; const CHART_HEIGHT = 80; @@ -75,9 +76,12 @@ export function ColonyVarroaCard({ colonyId }: { colonyId: string }) { {measurements?.map((measurement) => (

-

- {new Date(measurement.date).toLocaleDateString("de-DE")} · {measurement.method} -

+
+

+ {new Date(measurement.date).toLocaleDateString("de-DE")} · {measurement.method} +

+ +

{measurement.miteCount !== null ? `${measurement.miteCount} Milben` : "—"} {measurement.calculatedValue !== null diff --git a/ApiCore-Frontend/src/app/(app)/colonies/[id]/feeding-edit-dialog.tsx b/ApiCore-Frontend/src/app/(app)/colonies/[id]/feeding-edit-dialog.tsx new file mode 100644 index 0000000..ca801f8 --- /dev/null +++ b/ApiCore-Frontend/src/app/(app)/colonies/[id]/feeding-edit-dialog.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { Pencil } from "lucide-react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { updateFeeding } from "@/lib/feedings/api"; +import { Feeding, FeedType } from "@/lib/feedings/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"; + +const FEED_TYPE_OPTIONS: FeedType[] = [ + "SUGAR_WATER", + "FEED_SYRUP", + "FEED_PASTE", + "OWN_HONEY", + "OTHER", +]; + +export function FeedingEditDialog({ feeding }: { feeding: Feeding }) { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [date, setDate] = useState(feeding.date.slice(0, 10)); + const [feedType, setFeedType] = useState(feeding.feedType); + const [product, setProduct] = useState(feeding.product ?? ""); + const [amount, setAmount] = useState(feeding.amount.toString()); + const [unit, setUnit] = useState(feeding.unit); + const [notes, setNotes] = useState(feeding.notes ?? ""); + + const mutation = useMutation({ + mutationFn: () => + updateFeeding(feeding.id, { + date: new Date(date).toISOString(), + feedType, + product: product || undefined, + amount: amount ? Number(amount) : undefined, + unit, + notes: notes || undefined, + }), + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: ["colonies", feeding.colonyId, "feedings"], + }); + setOpen(false); + }, + }); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + mutation.mutate(); + } + + return ( +

+ + + Bearbeiten + + } + /> + + + Fütterung bearbeiten + +
+
+ + setDate(event.target.value)} + /> +
+
+ + +
+
+
+ + setAmount(event.target.value)} + /> +
+
+ + setUnit(event.target.value)} + /> +
+
+
+ + setProduct(event.target.value)} + /> +
+
+ +