import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { LIVING_COLONY_WHERE } from '../colonies/colony-status'; export interface DashboardSummary { apiariesActive: number; coloniesActive: number; queensActive: number; openTasks: number; honeyBatchesTotal: number; stockItemsLowCount: number; } export interface DashboardAlert { type: | 'OVERDUE_TASK' | 'COLONY_ATTENTION' | 'LOW_STOCK' | 'OVERDUE_SWARM_CHECK' | 'OLD_QUEEN'; message: string; relatedType: string; relatedId: string; } const ATTENTION_COLONY_STATUSES = ['WEAK', 'QUEENLESS', 'CONSPICUOUS'] as const; // Queens rarely stay reliably productive past their third season - laying // rate and swarming risk both worsen well before then, so this flags a // colony for evaluation rather than demanding immediate requeening. const OLD_QUEEN_AGE_YEARS = 3; @Injectable() export class DashboardService { constructor(private readonly prisma: PrismaService) {} async summary(): Promise { const [ apiariesActive, coloniesActive, queensActive, openTasks, honeyBatchesTotal, stockItems, ] = await Promise.all([ this.prisma.apiary.count({ where: { status: 'ACTIVE' } }), this.prisma.colony.count({ where: LIVING_COLONY_WHERE }), this.prisma.queen.count({ where: { status: { in: ['ACTIVE', 'RESERVE'] } }, }), this.prisma.task.count({ where: { status: { in: ['OPEN', 'IN_PROGRESS'] } }, }), this.prisma.honeyBatch.count(), this.prisma.stockItem.findMany({ where: { minThreshold: { not: null } }, }), ]); const stockItemsLowCount = stockItems.filter( (item) => item.minThreshold !== null && item.quantity < item.minThreshold, ).length; return { apiariesActive, coloniesActive, queensActive, openTasks, honeyBatchesTotal, stockItemsLowCount, }; } async alerts(): Promise { const [ overdueTasks, attentionColonies, stockItems, swarmChecks, queenedColonies, ] = await Promise.all([ this.prisma.task.findMany({ where: { status: { in: ['OPEN', 'IN_PROGRESS'] }, dueDate: { lt: new Date() }, }, }), this.prisma.colony.findMany({ where: { status: { in: [...ATTENTION_COLONY_STATUSES] } }, }), this.prisma.stockItem.findMany({ where: { minThreshold: { not: null } }, }), this.prisma.swarmControlCheck.findMany({ where: { colony: LIVING_COLONY_WHERE }, orderBy: [{ colonyId: 'asc' }, { date: 'desc' }], select: { colonyId: true, nextCheckDate: true, colony: { select: { code: true } }, }, }), this.prisma.colony.findMany({ where: { ...LIVING_COLONY_WHERE, currentQueen: { hatchYear: { not: null } }, }, select: { id: true, code: true, currentQueen: { select: { hatchYear: true } }, }, }), ]); const alerts: DashboardAlert[] = []; for (const task of overdueTasks) { alerts.push({ type: 'OVERDUE_TASK', message: `Aufgabe "${task.title}" ist überfällig`, relatedType: 'Task', relatedId: task.id, }); } for (const colony of attentionColonies) { alerts.push({ type: 'COLONY_ATTENTION', message: `Volk ${colony.code} benötigt Aufmerksamkeit (${colony.status})`, relatedType: 'Colony', relatedId: colony.id, }); } for (const item of stockItems) { if (item.minThreshold !== null && item.quantity < item.minThreshold) { alerts.push({ type: 'LOW_STOCK', message: `Lagerartikel "${item.name}" unter Mindestbestand`, relatedType: 'StockItem', relatedId: item.id, }); } } // swarmChecks is ordered by colonyId then date desc, so the first entry // seen per colonyId is that colony's most recent check - only the most // recent nextCheckDate matters, an older overdue one has been superseded. const now = new Date(); const latestCheckByColony = new Map(); for (const check of swarmChecks) { if (!latestCheckByColony.has(check.colonyId)) { latestCheckByColony.set(check.colonyId, check); } } for (const check of latestCheckByColony.values()) { if (check.nextCheckDate && check.nextCheckDate < now) { alerts.push({ type: 'OVERDUE_SWARM_CHECK', message: `Schwarmkontrolle für Volk ${check.colony.code} überfällig`, relatedType: 'Colony', relatedId: check.colonyId, }); } } const currentYear = now.getFullYear(); for (const colony of queenedColonies) { // The where clause above already requires currentQueen and its // hatchYear to exist, but the relation is still nullable in the type. if (!colony.currentQueen?.hatchYear) { continue; } const ageYears = currentYear - colony.currentQueen.hatchYear; if (ageYears >= OLD_QUEEN_AGE_YEARS) { alerts.push({ type: 'OLD_QUEEN', message: `Königin in Volk ${colony.code} ist ${ageYears} Jahre alt - Nachzucht prüfen`, relatedType: 'Colony', relatedId: colony.id, }); } } return alerts; } }