From 5cf4e87ca3e10ceccf9514424778aacc9d268c68 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Mon, 28 Sep 2026 06:23:13 +0200 Subject: [PATCH] feat(automation): suggest follow-up checks after treatments and feedings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new rules, "Kontrolle nach Behandlung" and "Kontrolle nach Fütterung", create a check task when a treatment or feeding is recorded. The due date counts from the measure itself (end of treatment, else its date; feeding date) plus the configured number of days, and the task names the product and date. At most one open check per colony and rule. Both rules ship switched off and without an interval: the right timing depends on the product (package leaflet) and season, so the beekeeper sets it. Every task-creating rule can now carry a "due after N days" setting in the automation page. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../migration.sql | 11 ++ ApiCore-Backend/prisma/schema.prisma | 2 + .../src/automation/automation.service.spec.ts | 5 +- .../src/automation/automation.service.ts | 77 +++++++- .../src/feedings/feedings.module.ts | 2 + .../src/feedings/feedings.service.spec.ts | 2 + .../src/feedings/feedings.service.ts | 3 + .../src/treatments/treatments.module.ts | 2 + .../src/treatments/treatments.service.spec.ts | 2 + .../src/treatments/treatments.service.ts | 3 + .../test/follow-up-controls.e2e-spec.ts | 173 ++++++++++++++++++ .../automation/automation-rules-tab.tsx | 43 ++++- ApiCore-Frontend/src/lib/automation/types.ts | 4 +- 13 files changed, 318 insertions(+), 11 deletions(-) create mode 100644 ApiCore-Backend/prisma/migrations/20260928042023_follow_up_triggers/migration.sql create mode 100644 ApiCore-Backend/test/follow-up-controls.e2e-spec.ts diff --git a/ApiCore-Backend/prisma/migrations/20260928042023_follow_up_triggers/migration.sql b/ApiCore-Backend/prisma/migrations/20260928042023_follow_up_triggers/migration.sql new file mode 100644 index 0000000..ebc59e4 --- /dev/null +++ b/ApiCore-Backend/prisma/migrations/20260928042023_follow_up_triggers/migration.sql @@ -0,0 +1,11 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "AutomationTriggerType" ADD VALUE 'TREATMENT_FOLLOW_UP'; +ALTER TYPE "AutomationTriggerType" ADD VALUE 'FEEDING_FOLLOW_UP'; + diff --git a/ApiCore-Backend/prisma/schema.prisma b/ApiCore-Backend/prisma/schema.prisma index 1687d23..82ad6bc 100644 --- a/ApiCore-Backend/prisma/schema.prisma +++ b/ApiCore-Backend/prisma/schema.prisma @@ -1074,6 +1074,8 @@ enum AutomationTriggerType { STOCK_ITEM_LOW HONEY_BATCH_NUMBER_FORMAT BOTTLING_LOT_NUMBER_FORMAT + TREATMENT_FOLLOW_UP + FEEDING_FOLLOW_UP } model AutomationRule { diff --git a/ApiCore-Backend/src/automation/automation.service.spec.ts b/ApiCore-Backend/src/automation/automation.service.spec.ts index 08e156b..9534c98 100644 --- a/ApiCore-Backend/src/automation/automation.service.spec.ts +++ b/ApiCore-Backend/src/automation/automation.service.spec.ts @@ -4,6 +4,7 @@ import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; import { NotificationsService } from '../notifications/notifications.service'; import { WebhooksService } from '../webhooks/webhooks.service'; +import { AutomationTriggerType } from '../../generated/prisma'; describe('AutomationService', () => { let service: AutomationService; @@ -82,7 +83,9 @@ describe('AutomationService', () => { const result = await service.findAll(); - expect(prisma.automationRule.upsert).toHaveBeenCalledTimes(8); + expect(prisma.automationRule.upsert).toHaveBeenCalledTimes( + Object.values(AutomationTriggerType).length, + ); expect(result).toHaveLength(3); }); diff --git a/ApiCore-Backend/src/automation/automation.service.ts b/ApiCore-Backend/src/automation/automation.service.ts index 62cc842..9a2fb24 100644 --- a/ApiCore-Backend/src/automation/automation.service.ts +++ b/ApiCore-Backend/src/automation/automation.service.ts @@ -8,10 +8,12 @@ import { AutomationTriggerType, Colony, Inspection, + Feeding, Prisma, StockItem, SwarmControlCheck, TaskRelatedType, + Treatment, VarroaMeasurement, } from '../../generated/prisma'; import { UpdateAutomationRuleDto } from './dto/update-automation-rule.dto'; @@ -20,6 +22,8 @@ import { formatGeneratedNumber } from './number-format'; interface RuleDefaults { config: Record; actionTemplate: string; + /** Rules without a sensible built-in setting start switched off. */ + active?: boolean; } const DEFAULT_RULES: Record = { @@ -60,6 +64,20 @@ const DEFAULT_RULES: Record = { config: { format: 'WH-{YYYY}-{SEQ:4}' }, actionTemplate: '', }, + // Follow-up checks: how many days after a treatment or feeding a check + // makes sense depends on the product and the season, so there is no + // built-in interval - the beekeeper switches the rule on and sets + // config.dueInDays. + TREATMENT_FOLLOW_UP: { + config: {}, + actionTemplate: 'Behandlungserfolg kontrollieren', + active: false, + }, + FEEDING_FOLLOW_UP: { + config: {}, + actionTemplate: 'Futterabnahme und Futtervorrat kontrollieren', + active: false, + }, }; /** @@ -68,7 +86,19 @@ const DEFAULT_RULES: Record = { * look depends on season and practice, so the rule stays date-less unless the * beekeeper configures one. */ -function dueDateFromConfig(dueInDays: unknown): Date | undefined { +function formatDay(date: Date): string { + return date.toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + timeZone: 'UTC', + }); +} + +function dueDateFromConfig( + dueInDays: unknown, + baseDate: Date = new Date(), +): Date | undefined { if ( typeof dueInDays !== 'number' || !Number.isInteger(dueInDays) || @@ -77,7 +107,7 @@ function dueDateFromConfig(dueInDays: unknown): Date | undefined { ) { return undefined; } - const due = new Date(); + const due = new Date(baseDate); due.setUTCHours(0, 0, 0, 0); due.setUTCDate(due.getUTCDate() + dueInDays); return due; @@ -102,6 +132,7 @@ export class AutomationService { update: {}, create: { triggerType, + active: defaults.active ?? true, config: defaults.config as Prisma.InputJsonValue, actionTemplate: defaults.actionTemplate, }, @@ -255,6 +286,44 @@ export class AutomationService { await this.triggerAction(rule, 'STORAGE', item.id); } + async evaluateTreatment(treatment: Treatment): Promise { + await this.evaluateFollowUp( + 'TREATMENT_FOLLOW_UP', + treatment.colonyId, + treatment.endDate ?? treatment.date, + `Nach Behandlung mit ${treatment.product} am ${formatDay(treatment.endDate ?? treatment.date)}.`, + ); + } + + async evaluateFeeding(feeding: Feeding): Promise { + await this.evaluateFollowUp( + 'FEEDING_FOLLOW_UP', + feeding.colonyId, + feeding.date, + `Nach Fütterung (${feeding.amount} ${feeding.unit}) am ${formatDay(feeding.date)}.`, + ); + } + + /** A check counted from the measure's own date, not from today. */ + private async evaluateFollowUp( + triggerType: 'TREATMENT_FOLLOW_UP' | 'FEEDING_FOLLOW_UP', + colonyId: string, + measureDate: Date, + description: string, + ): Promise { + await this.ensureDefaults(); + const rule = await this.prisma.automationRule.findUnique({ + where: { triggerType }, + }); + if (!rule || !rule.active) { + return; + } + await this.triggerAction(rule, 'COLONY', colonyId, undefined, { + baseDate: measureDate, + description, + }); + } + // Returns null (never throws) when the rule is inactive or has no format // configured, so the caller can fall back to requiring manual entry. async generateNumber( @@ -315,6 +384,7 @@ export class AutomationService { relatedType: TaskRelatedType, relatedId: string, sourceInspectionId?: string, + options: { baseDate?: Date; description?: string } = {}, ): Promise { const alreadyOpen = await this.prisma.task.findFirst({ where: { @@ -339,7 +409,8 @@ export class AutomationService { relatedId, sourceInspectionId, sourceAutomationTrigger: rule.triggerType, - dueDate: dueDateFromConfig(config?.dueInDays), + description: options.description, + dueDate: dueDateFromConfig(config?.dueInDays, options.baseDate), }, }); await this.webhooks.dispatch('task.created', task); diff --git a/ApiCore-Backend/src/feedings/feedings.module.ts b/ApiCore-Backend/src/feedings/feedings.module.ts index 6239eb2..8f88131 100644 --- a/ApiCore-Backend/src/feedings/feedings.module.ts +++ b/ApiCore-Backend/src/feedings/feedings.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; +import { AutomationModule } from '../automation/automation.module'; import { FeedingsController } from './feedings.controller'; import { ColonyFeedingsController } from './colony-feedings.controller'; import { FeedingsService } from './feedings.service'; @Module({ + imports: [AutomationModule], controllers: [FeedingsController, ColonyFeedingsController], providers: [FeedingsService], exports: [FeedingsService], diff --git a/ApiCore-Backend/src/feedings/feedings.service.spec.ts b/ApiCore-Backend/src/feedings/feedings.service.spec.ts index 6a58c00..94f3710 100644 --- a/ApiCore-Backend/src/feedings/feedings.service.spec.ts +++ b/ApiCore-Backend/src/feedings/feedings.service.spec.ts @@ -1,4 +1,5 @@ import { NotFoundException } from '@nestjs/common'; +import { AutomationService } from '../automation/automation.service'; import { FeedingsService } from './feedings.service'; import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; @@ -35,6 +36,7 @@ describe('FeedingsService', () => { prisma as unknown as PrismaService, auditLog as unknown as AuditLogService, calendarSync as unknown as CalendarSyncService, + { evaluateFeeding: jest.fn() } as unknown as AutomationService, ); }); diff --git a/ApiCore-Backend/src/feedings/feedings.service.ts b/ApiCore-Backend/src/feedings/feedings.service.ts index 33eaa90..8ce102c 100644 --- a/ApiCore-Backend/src/feedings/feedings.service.ts +++ b/ApiCore-Backend/src/feedings/feedings.service.ts @@ -1,4 +1,5 @@ import { Injectable, NotFoundException } from '@nestjs/common'; +import { AutomationService } from '../automation/automation.service'; import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; import { CalendarSyncService } from '../calendar-sync/calendar-sync.service'; @@ -17,6 +18,7 @@ export class FeedingsService { private readonly prisma: PrismaService, private readonly auditLog: AuditLogService, private readonly calendarSync: CalendarSyncService, + private readonly automation: AutomationService, ) {} async findAll(): Promise { @@ -62,6 +64,7 @@ export class FeedingsService { afterData: feeding, }); await this.calendarSync.enqueue('FEEDING', feeding.id); + await this.automation.evaluateFeeding(feeding); return feeding; } diff --git a/ApiCore-Backend/src/treatments/treatments.module.ts b/ApiCore-Backend/src/treatments/treatments.module.ts index 53b63d1..cbb3c6b 100644 --- a/ApiCore-Backend/src/treatments/treatments.module.ts +++ b/ApiCore-Backend/src/treatments/treatments.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; +import { AutomationModule } from '../automation/automation.module'; import { TreatmentsController } from './treatments.controller'; import { ColonyTreatmentsController } from './colony-treatments.controller'; import { TreatmentsService } from './treatments.service'; @Module({ + imports: [AutomationModule], controllers: [TreatmentsController, ColonyTreatmentsController], providers: [TreatmentsService], exports: [TreatmentsService], diff --git a/ApiCore-Backend/src/treatments/treatments.service.spec.ts b/ApiCore-Backend/src/treatments/treatments.service.spec.ts index 6468f7b..365e85e 100644 --- a/ApiCore-Backend/src/treatments/treatments.service.spec.ts +++ b/ApiCore-Backend/src/treatments/treatments.service.spec.ts @@ -1,4 +1,5 @@ import { NotFoundException } from '@nestjs/common'; +import { AutomationService } from '../automation/automation.service'; import { TreatmentsService } from './treatments.service'; import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; @@ -37,6 +38,7 @@ describe('TreatmentsService', () => { prisma as unknown as PrismaService, auditLog as unknown as AuditLogService, calendarSync as unknown as CalendarSyncService, + { evaluateTreatment: jest.fn() } as unknown as AutomationService, ); }); diff --git a/ApiCore-Backend/src/treatments/treatments.service.ts b/ApiCore-Backend/src/treatments/treatments.service.ts index c8b605f..aeaca5b 100644 --- a/ApiCore-Backend/src/treatments/treatments.service.ts +++ b/ApiCore-Backend/src/treatments/treatments.service.ts @@ -1,4 +1,5 @@ import { Injectable, NotFoundException } from '@nestjs/common'; +import { AutomationService } from '../automation/automation.service'; import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; import { CalendarSyncService } from '../calendar-sync/calendar-sync.service'; @@ -18,6 +19,7 @@ export class TreatmentsService { private readonly prisma: PrismaService, private readonly auditLog: AuditLogService, private readonly calendarSync: CalendarSyncService, + private readonly automation: AutomationService, ) {} async findAll(): Promise { @@ -98,6 +100,7 @@ export class TreatmentsService { afterData: treatment, }); await this.calendarSync.enqueue('TREATMENT', treatment.id); + await this.automation.evaluateTreatment(treatment); return treatment; } diff --git a/ApiCore-Backend/test/follow-up-controls.e2e-spec.ts b/ApiCore-Backend/test/follow-up-controls.e2e-spec.ts new file mode 100644 index 0000000..ef151f8 --- /dev/null +++ b/ApiCore-Backend/test/follow-up-controls.e2e-spec.ts @@ -0,0 +1,173 @@ +import { INestApplication } from '@nestjs/common'; +import * as bcrypt from 'bcryptjs'; +import request from 'supertest'; +import { App } from 'supertest/types'; +import { PrismaService } from '../src/prisma/prisma.service'; +import { createTestApp } from './utils/create-test-app'; +import { login } from './utils/api'; + +interface RuleBody { + id: string; + triggerType: string; + active: boolean; +} + +/** + * Follow-up checks after a treatment or feeding: off by default (no built-in + * intervals), due relative to the measure itself, never duplicated. + */ +describe('Follow-up controls (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`; + let token: string; + let apiaryId: string; + + const server = () => app.getHttpServer(); + const auth = () => ({ Authorization: `Bearer ${token}` }); + + async function rule(triggerType: string): Promise { + const response = await request(server()) + .get('/api/v1/automation-rules') + .set(auth()) + .expect(200); + const found = (response.body as RuleBody[]).find( + (r) => r.triggerType === triggerType, + ); + if (!found) throw new Error(`rule ${triggerType} missing`); + return found; + } + + async function newColony(code: string) { + return prisma.colony.create({ + data: { + code: `${code}-${suffix}`, + apiaryId, + colonyType: 'PRODUCTION_COLONY', + }, + }); + } + + function treat(colonyId: string, body: Record = {}) { + return request(server()) + .post('/api/v1/treatments') + .set(auth()) + .send({ + colonyId, + date: '2026-08-01T00:00:00.000Z', + product: 'Testmittel', + ...body, + }) + .expect(201); + } + + beforeAll(async () => { + app = await createTestApp(); + prisma = app.get(PrismaService); + await prisma.user.create({ + data: { + email: `followup-${suffix}@example.com`, + passwordHash: await bcrypt.hash('SuperSecret123!', 12), + name: 'Follow-up', + role: { connect: { key: 'ADMIN' } }, + }, + }); + token = ( + await login(app, `followup-${suffix}@example.com`, 'SuperSecret123!') + ).accessToken; + apiaryId = ( + await prisma.apiary.create({ data: { name: `Follow-up ${suffix}` } }) + ).id; + }); + + afterAll(async () => { + // Leave the shared rules as other suites expect them: off. + await prisma.automationRule.updateMany({ + where: { + triggerType: { in: ['TREATMENT_FOLLOW_UP', 'FEEDING_FOLLOW_UP'] }, + }, + data: { active: false, config: {} }, + }); + await app.close(); + }); + + it('ships both follow-up rules switched off, without a built-in interval', async () => { + const treatmentRule = await rule('TREATMENT_FOLLOW_UP'); + const feedingRule = await rule('FEEDING_FOLLOW_UP'); + expect(treatmentRule.active).toBe(false); + expect(feedingRule.active).toBe(false); + + const colony = await newColony('OFF'); + await treat(colony.id); + expect(await prisma.task.count({ where: { relatedId: colony.id } })).toBe( + 0, + ); + }); + + it('suggests a check due the configured days after the treatment ended', async () => { + const treatmentRule = await rule('TREATMENT_FOLLOW_UP'); + await request(server()) + .patch(`/api/v1/automation-rules/${treatmentRule.id}`) + .set(auth()) + .send({ active: true, config: { dueInDays: 14 } }) + .expect(200); + + const colony = await newColony('TREAT'); + await treat(colony.id, { + startDate: '2026-08-01T00:00:00.000Z', + endDate: '2026-08-10T00:00:00.000Z', + }); + + const tasks = await prisma.task.findMany({ + where: { + relatedId: colony.id, + sourceAutomationTrigger: 'TREATMENT_FOLLOW_UP', + }, + }); + expect(tasks).toHaveLength(1); + expect(tasks[0].dueDate?.toISOString()).toBe('2026-08-24T00:00:00.000Z'); + expect(tasks[0].description).toContain('Testmittel'); + expect(tasks[0].status).toBe('OPEN'); + + // A second treatment while the check is still open adds nothing. + await treat(colony.id, { date: '2026-08-12T00:00:00.000Z' }); + expect( + await prisma.task.count({ + where: { + relatedId: colony.id, + sourceAutomationTrigger: 'TREATMENT_FOLLOW_UP', + }, + }), + ).toBe(1); + }); + + it('suggests a check after a feeding, counted from the feeding date', async () => { + const feedingRule = await rule('FEEDING_FOLLOW_UP'); + await request(server()) + .patch(`/api/v1/automation-rules/${feedingRule.id}`) + .set(auth()) + .send({ active: true, config: { dueInDays: 7 } }) + .expect(200); + + const colony = await newColony('FEED'); + await request(server()) + .post('/api/v1/feedings') + .set(auth()) + .send({ + colonyId: colony.id, + date: '2026-09-01T00:00:00.000Z', + feedType: 'SUGAR_WATER', + amount: 5, + unit: 'l', + }) + .expect(201); + + const task = await prisma.task.findFirstOrThrow({ + where: { + relatedId: colony.id, + sourceAutomationTrigger: 'FEEDING_FOLLOW_UP', + }, + }); + expect(task.dueDate?.toISOString()).toBe('2026-09-08T00:00:00.000Z'); + }); +}); diff --git a/ApiCore-Frontend/src/app/(app)/settings/automation/automation-rules-tab.tsx b/ApiCore-Frontend/src/app/(app)/settings/automation/automation-rules-tab.tsx index 34a8602..032cc93 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/automation/automation-rules-tab.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/automation/automation-rules-tab.tsx @@ -25,6 +25,8 @@ const TRIGGER_LABELS: Record = { STOCK_ITEM_LOW: "Lagerartikel unter Mindestbestand", HONEY_BATCH_NUMBER_FORMAT: "Chargennummer-Format (Honigchargen)", BOTTLING_LOT_NUMBER_FORMAT: "Los-/Chargennummer-Format (Abfüllungen)", + TREATMENT_FOLLOW_UP: "Kontrolle nach Behandlung", + FEEDING_FOLLOW_UP: "Kontrolle nach Fütterung", }; const TRIGGER_DESCRIPTIONS: Record = { @@ -44,6 +46,10 @@ const TRIGGER_DESCRIPTIONS: Record = { "Wenn aktiv, wird die Chargennummer neuer Honigchargen automatisch nach diesem Format erzeugt, statt sie manuell einzugeben.", BOTTLING_LOT_NUMBER_FORMAT: "Wenn aktiv, wird die Los-/Chargennummer neuer Abfüllungen automatisch nach diesem Format erzeugt, statt sie manuell einzugeben.", + TREATMENT_FOLLOW_UP: + "Nach jeder erfassten Behandlung eine Kontrollaufgabe vorschlagen, fällig die eingestellten Tage nach Behandlungsende. Die passende Frist hängt vom Mittel ab (Packungsbeilage) – deshalb ab Werk aus und ohne voreingestellte Frist.", + FEEDING_FOLLOW_UP: + "Nach jeder erfassten Fütterung eine Kontrollaufgabe vorschlagen, fällig die eingestellten Tage nach der Fütterung. Ab Werk aus und ohne voreingestellte Frist.", }; const NUMBER_FORMAT_TRIGGER_TYPES: AutomationTriggerType[] = [ @@ -60,6 +66,11 @@ function ruleFormat(rule: AutomationRule): string { return config?.format ?? ""; } +function ruleDueInDays(rule: AutomationRule): string { + const config = rule.config as { dueInDays?: number } | null; + return config?.dueInDays === undefined ? "" : String(config.dueInDays); +} + function ruleThreshold(rule: AutomationRule): string { const config = rule.config as { thresholdMitesPerDay?: number } | null; return String(config?.thresholdMitesPerDay ?? ""); @@ -70,6 +81,7 @@ function RuleCard({ rule }: { rule: AutomationRule }) { const [actionTemplate, setActionTemplate] = useState(rule.actionTemplate ?? ""); const [threshold, setThreshold] = useState(ruleThreshold(rule)); const [format, setFormat] = useState(ruleFormat(rule)); + const [dueInDays, setDueInDays] = useState(ruleDueInDays(rule)); const numberFormatRule = isNumberFormatRule(rule); const mutation = useMutation({ @@ -96,13 +108,17 @@ function RuleCard({ rule }: { rule: AutomationRule }) { mutation.mutate({ config: { format } }); return; } - const input: { config?: Record; actionTemplate?: string } = { - actionTemplate: actionTemplate || undefined, - }; - if (rule.triggerType === "VARROA_THRESHOLD") { - input.config = { thresholdMitesPerDay: Number(threshold) || 10 }; + // Empty = task without a due date; otherwise whole days, 0-365. + const days = dueInDays.trim() === "" ? undefined : Math.round(Number(dueInDays)); + if (days !== undefined && (!Number.isFinite(days) || days < 0 || days > 365)) { + toast.error("Frist bitte als ganze Tage zwischen 0 und 365 angeben – oder leer lassen."); + return; } - mutation.mutate(input); + const config: Record = { dueInDays: days }; + if (rule.triggerType === "VARROA_THRESHOLD") { + config.thresholdMitesPerDay = Number(threshold) || 10; + } + mutation.mutate({ actionTemplate: actionTemplate || undefined, config }); } return ( @@ -150,6 +166,21 @@ function RuleCard({ rule }: { rule: AutomationRule }) { /> )} + {!numberFormatRule ? ( +
+ + setDueInDays(event.target.value)} + /> +
+ ) : null} {rule.triggerType === "VARROA_THRESHOLD" ? (
diff --git a/ApiCore-Frontend/src/lib/automation/types.ts b/ApiCore-Frontend/src/lib/automation/types.ts index fe257e0..9b93d59 100644 --- a/ApiCore-Frontend/src/lib/automation/types.ts +++ b/ApiCore-Frontend/src/lib/automation/types.ts @@ -6,7 +6,9 @@ export type AutomationTriggerType = | "COLONY_HEALTH_CRITICAL" | "STOCK_ITEM_LOW" | "HONEY_BATCH_NUMBER_FORMAT" - | "BOTTLING_LOT_NUMBER_FORMAT"; + | "BOTTLING_LOT_NUMBER_FORMAT" + | "TREATMENT_FOLLOW_UP" + | "FEEDING_FOLLOW_UP"; export interface AutomationRule { id: string;