/** * Data Aggregator for Analytics * Aggregates data from various sources for analytics dashboards */ import { AnalyticsOverview, PlantAnalytics, TransportAnalytics, FarmAnalytics, SustainabilityAnalytics, TimeRange, DateRange, TimeSeriesDataPoint, AnalyticsFilters, AggregationConfig, GroupByPeriod, } from './types'; import { subDays, subMonths, startOfDay, endOfDay, format, eachDayOfInterval, parseISO } from 'date-fns'; // Mock data generators for demonstration - in production these would query actual databases /** * Get date range from TimeRange enum */ export function getDateRangeFromTimeRange(timeRange: TimeRange): DateRange { const end = endOfDay(new Date()); let start: Date; switch (timeRange) { case '7d': start = startOfDay(subDays(new Date(), 7)); break; case '30d': start = startOfDay(subDays(new Date(), 30)); break; case '90d': start = startOfDay(subDays(new Date(), 90)); break; case '365d': start = startOfDay(subDays(new Date(), 365)); break; case 'all': default: start = startOfDay(subMonths(new Date(), 24)); // Default to 2 years } return { start, end }; } /** * Generate time series data points for a date range */ export function generateTimeSeriesPoints( dateRange: DateRange, valueGenerator: (date: Date, index: number) => number ): TimeSeriesDataPoint[] { const days = eachDayOfInterval({ start: dateRange.start, end: dateRange.end }); return days.map((day, index) => ({ timestamp: format(day, 'yyyy-MM-dd'), value: valueGenerator(day, index), label: format(day, 'MMM d'), })); } /** * Aggregate data by time period */ export function aggregateByPeriod( data: T[], dateField: keyof T, valueField: keyof T, period: GroupByPeriod ): Record { const aggregated: Record = {}; data.forEach((item) => { const date = parseISO(item[dateField] as string); let key: string; switch (period) { case 'hour': key = format(date, 'yyyy-MM-dd HH:00'); break; case 'day': key = format(date, 'yyyy-MM-dd'); break; case 'week': key = format(date, "yyyy-'W'ww"); break; case 'month': key = format(date, 'yyyy-MM'); break; case 'year': key = format(date, 'yyyy'); break; } aggregated[key] = (aggregated[key] || 0) + (item[valueField] as number); }); return aggregated; } /** * Calculate percentage change between two values */ export function calculateChange(current: number, previous: number): { change: number; percent: number } { const change = current - previous; const percent = previous !== 0 ? (change / previous) * 100 : current > 0 ? 100 : 0; return { change, percent }; } /** * Get analytics overview with aggregated metrics */ export async function getAnalyticsOverview( filters: AnalyticsFilters = { timeRange: '30d' } ): Promise { const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange); // In production, these would be actual database queries // For now, generate realistic mock data const baseValue = 1000 + Math.random() * 500; return { totalPlants: Math.floor(baseValue * 1.5), plantsRegisteredToday: Math.floor(Math.random() * 15 + 5), plantsRegisteredThisWeek: Math.floor(Math.random() * 80 + 40), plantsRegisteredThisMonth: Math.floor(Math.random() * 250 + 150), totalTransportEvents: Math.floor(baseValue * 2.3), totalCarbonKg: Math.round((Math.random() * 500 + 200) * 100) / 100, totalFoodMiles: Math.round((Math.random() * 10000 + 5000) * 10) / 10, activeUsers: Math.floor(Math.random() * 200 + 100), growthRate: Math.round((Math.random() * 20 + 5) * 10) / 10, trendsData: [ { metric: 'Plants', currentValue: Math.floor(baseValue * 1.5), previousValue: Math.floor(baseValue * 1.35), change: Math.floor(baseValue * 0.15), changePercent: 11.1, direction: 'up', period: filters.timeRange, }, { metric: 'Carbon Saved', currentValue: Math.round((Math.random() * 200 + 100) * 10) / 10, previousValue: Math.round((Math.random() * 180 + 90) * 10) / 10, change: Math.round((Math.random() * 20 + 10) * 10) / 10, changePercent: 12.5, direction: 'up', period: filters.timeRange, }, { metric: 'Active Users', currentValue: Math.floor(Math.random() * 200 + 100), previousValue: Math.floor(Math.random() * 180 + 90), change: Math.floor(Math.random() * 30 + 10), changePercent: 8.3, direction: 'up', period: filters.timeRange, }, { metric: 'Food Miles', currentValue: Math.round((Math.random() * 5000 + 2500) * 10) / 10, previousValue: Math.round((Math.random() * 5500 + 2800) * 10) / 10, change: -Math.round((Math.random() * 500 + 200) * 10) / 10, changePercent: -8.7, direction: 'down', period: filters.timeRange, }, ], }; } /** * Get plant-specific analytics */ export async function getPlantAnalytics( filters: AnalyticsFilters = { timeRange: '30d' } ): Promise { const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange); const speciesData = [ { species: 'Tomato', count: 245, percentage: 28.5, trend: 'up' as const }, { species: 'Lettuce', count: 198, percentage: 23.0, trend: 'up' as const }, { species: 'Pepper', count: 156, percentage: 18.1, trend: 'stable' as const }, { species: 'Basil', count: 134, percentage: 15.6, trend: 'up' as const }, { species: 'Cucumber', count: 87, percentage: 10.1, trend: 'down' as const }, { species: 'Other', count: 41, percentage: 4.7, trend: 'stable' as const }, ]; return { totalPlants: speciesData.reduce((sum, s) => sum + s.count, 0), plantsBySpecies: speciesData, plantsByGeneration: [ { generation: 1, count: 340, percentage: 39.5 }, { generation: 2, count: 280, percentage: 32.5 }, { generation: 3, count: 156, percentage: 18.1 }, { generation: 4, count: 68, percentage: 7.9 }, { generation: 5, count: 17, percentage: 2.0 }, ], registrationsTrend: generateTimeSeriesPoints(dateRange, (_, i) => Math.floor(Math.random() * 15 + 5 + Math.sin(i / 7) * 5) ), averageLineageDepth: 2.3, topGrowers: [ { userId: 'user-1', name: 'Green Gardens Co', totalPlants: 145, totalSpecies: 12, averageGeneration: 2.1 }, { userId: 'user-2', name: 'Urban Farm LLC', totalPlants: 98, totalSpecies: 8, averageGeneration: 1.8 }, { userId: 'user-3', name: 'Local Seeds Inc', totalPlants: 76, totalSpecies: 15, averageGeneration: 3.2 }, ], recentRegistrations: [ { id: 'plant-1', name: 'Cherry Tomato #245', species: 'Tomato', registeredAt: new Date().toISOString(), generation: 3 }, { id: 'plant-2', name: 'Butterhead Lettuce', species: 'Lettuce', registeredAt: new Date().toISOString(), generation: 2 }, { id: 'plant-3', name: 'Sweet Basil', species: 'Basil', registeredAt: new Date().toISOString(), generation: 1 }, ], }; } /** * Get transport analytics */ export async function getTransportAnalytics( filters: AnalyticsFilters = { timeRange: '30d' } ): Promise { const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange); return { totalEvents: 2847, totalDistanceKm: 15234.5, totalCarbonKg: 487.3, carbonSavedKg: 1256.8, eventsByType: [ { eventType: 'seed_acquisition', count: 423, percentage: 14.9, carbonKg: 52.3 }, { eventType: 'growing_transport', count: 687, percentage: 24.1, carbonKg: 112.4 }, { eventType: 'harvest', count: 534, percentage: 18.8, carbonKg: 45.2 }, { eventType: 'distribution', count: 756, percentage: 26.6, carbonKg: 178.9 }, { eventType: 'consumer_delivery', count: 447, percentage: 15.7, carbonKg: 98.5 }, ], eventsByMethod: [ { method: 'walking', count: 312, percentage: 11.0, distanceKm: 156, carbonKg: 0, efficiency: 100 }, { method: 'bicycle', count: 534, percentage: 18.8, distanceKm: 1602, carbonKg: 0, efficiency: 100 }, { method: 'electric_vehicle', count: 687, percentage: 24.1, distanceKm: 4806, carbonKg: 72.1, efficiency: 85 }, { method: 'gasoline_vehicle', count: 756, percentage: 26.6, distanceKm: 5292, carbonKg: 264.6, efficiency: 45 }, { method: 'local_delivery', count: 558, percentage: 19.6, distanceKm: 3378, carbonKg: 150.6, efficiency: 60 }, ], dailyStats: generateTimeSeriesPoints(dateRange, (_, i) => ({ date: format(dateRange.start, 'yyyy-MM-dd'), eventCount: Math.floor(Math.random() * 80 + 40), distanceKm: Math.round((Math.random() * 500 + 200) * 10) / 10, carbonKg: Math.round((Math.random() * 20 + 5) * 100) / 100, })).map(p => ({ date: p.timestamp, eventCount: p.value, distanceKm: Math.round((Math.random() * 500 + 200) * 10) / 10, carbonKg: Math.round((Math.random() * 20 + 5) * 100) / 100, })), averageDistancePerEvent: 5.35, mostEfficientRoutes: [ { from: 'Local Farm A', to: 'Community Center', method: 'bicycle', distanceKm: 2.3, carbonKg: 0, frequency: 45 }, { from: 'Urban Garden', to: 'Farmers Market', method: 'walking', distanceKm: 0.8, carbonKg: 0, frequency: 38 }, { from: 'Rooftop Farm', to: 'Restaurant Row', method: 'electric_vehicle', distanceKm: 4.5, carbonKg: 0.07, frequency: 32 }, ], carbonTrend: generateTimeSeriesPoints(dateRange, (_, i) => Math.round((Math.random() * 15 + 10 - i * 0.1) * 100) / 100 ), }; } /** * Get farm analytics */ export async function getFarmAnalytics( filters: AnalyticsFilters = { timeRange: '30d' } ): Promise { const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange); return { totalFarms: 24, totalZones: 156, activeBatches: 89, completedBatches: 234, averageYieldKg: 45.6, resourceUsage: { waterLiters: 125000, energyKwh: 8500, nutrientsKg: 450, waterEfficiency: 87.5, energyEfficiency: 92.3, }, performanceByZone: [ { zoneId: 'zone-1', zoneName: 'Zone A - Leafy Greens', currentCrop: 'Lettuce', healthScore: 94, yieldKg: 52.3, efficiency: 91 }, { zoneId: 'zone-2', zoneName: 'Zone B - Herbs', currentCrop: 'Basil', healthScore: 88, yieldKg: 38.7, efficiency: 85 }, { zoneId: 'zone-3', zoneName: 'Zone C - Tomatoes', currentCrop: 'Cherry Tomato', healthScore: 92, yieldKg: 67.4, efficiency: 89 }, { zoneId: 'zone-4', zoneName: 'Zone D - Microgreens', currentCrop: 'Mixed Micro', healthScore: 96, yieldKg: 24.1, efficiency: 94 }, ], batchCompletionTrend: generateTimeSeriesPoints(dateRange, (_, i) => Math.floor(Math.random() * 5 + 2) ), yieldPredictions: [ { cropType: 'Lettuce', predictedYieldKg: 156.5, confidence: 0.92, harvestDate: format(subDays(new Date(), -7), 'yyyy-MM-dd') }, { cropType: 'Tomato', predictedYieldKg: 234.8, confidence: 0.87, harvestDate: format(subDays(new Date(), -14), 'yyyy-MM-dd') }, { cropType: 'Basil', predictedYieldKg: 45.2, confidence: 0.94, harvestDate: format(subDays(new Date(), -5), 'yyyy-MM-dd') }, ], topPerformingCrops: [ { cropType: 'Lettuce', averageYieldKg: 48.3, growthDays: 28, successRate: 94.5, batches: 45 }, { cropType: 'Basil', averageYieldKg: 12.4, growthDays: 21, successRate: 91.2, batches: 38 }, { cropType: 'Cherry Tomato', averageYieldKg: 67.8, growthDays: 65, successRate: 88.7, batches: 22 }, { cropType: 'Microgreens', averageYieldKg: 5.6, growthDays: 14, successRate: 96.8, batches: 67 }, ], }; } /** * Get sustainability analytics */ export async function getSustainabilityAnalytics( filters: AnalyticsFilters = { timeRange: '30d' } ): Promise { const dateRange = filters.dateRange || getDateRangeFromTimeRange(filters.timeRange); return { overallScore: 82.5, carbonFootprint: { totalEmittedKg: 487.3, totalSavedKg: 1256.8, netImpactKg: -769.5, reductionPercentage: 72.1, equivalentTrees: 38.4, monthlyTrend: generateTimeSeriesPoints(dateRange, (_, i) => Math.round((50 - i * 0.5 + Math.random() * 10) * 10) / 10 ), }, foodMiles: { totalMiles: 15234.5, averageMilesPerPlant: 17.7, savedMiles: 48672.3, localPercentage: 76.2, monthlyTrend: generateTimeSeriesPoints(dateRange, (_, i) => Math.round((600 - i * 5 + Math.random() * 100) * 10) / 10 ), }, waterUsage: { totalUsedLiters: 125000, savedLiters: 87500, efficiencyScore: 87.5, perKgProduce: 2.8, }, localProduction: { localCount: 654, totalCount: 861, percentage: 76.0, trend: 'up', }, goals: [ { id: 'goal-1', name: 'Carbon Neutral by 2025', target: 0, current: 487.3, unit: 'kg CO2', progress: 72, deadline: '2025-12-31', status: 'on_track' }, { id: 'goal-2', name: '80% Local Production', target: 80, current: 76, unit: '%', progress: 95, deadline: '2024-12-31', status: 'on_track' }, { id: 'goal-3', name: 'Reduce Food Miles 50%', target: 50, current: 38, unit: '%', progress: 76, deadline: '2024-06-30', status: 'at_risk' }, { id: 'goal-4', name: 'Water Efficiency 90%', target: 90, current: 87.5, unit: '%', progress: 97, deadline: '2024-12-31', status: 'on_track' }, ], trends: [ { metric: 'Carbon Reduction', values: generateTimeSeriesPoints(dateRange, (_, i) => Math.round((65 + i * 0.3 + Math.random() * 5) * 10) / 10 ), }, { metric: 'Local Production', values: generateTimeSeriesPoints(dateRange, (_, i) => Math.round((70 + i * 0.2 + Math.random() * 3) * 10) / 10 ), }, ], }; } /** * Cache management for analytics data */ const analyticsCache = new Map(); const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes export function getCachedData(key: string): T | null { const cached = analyticsCache.get(key); if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { return cached.data as T; } return null; } export function setCachedData(key: string, data: T): void { analyticsCache.set(key, { data, timestamp: Date.now() }); } export function clearCache(): void { analyticsCache.clear(); } /** * Generate cache key from filters */ export function generateCacheKey(prefix: string, filters: AnalyticsFilters): string { return `${prefix}-${JSON.stringify(filters)}`; }