import { useState, useEffect, useRef } from 'react'; import { motion } from 'framer-motion'; import { FileText, Download, Printer, Calendar, Thermometer, Droplets, Wind, AlertTriangle, CheckCircle, TrendingUp, Clock, Activity, ChevronDown, Loader2 } from 'lucide-react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart, ReferenceLine, BarChart, Bar } from 'recharts'; import { jsPDF } from 'jspdf'; import html2canvas from 'html2canvas'; import api from '../lib/api'; interface ReportData { period: { start: string; end: string }; devices: { id: string; name: string; readings: number; stats: { temperature: { min: number; max: number; avg: number }; humidity: { min: number; max: number; avg: number }; vpd: { min: number; max: number; avg: number }; }; }[]; alerts: { total: number; resolved: number; byType: { type: string; count: number }[]; }; history: { timestamp: string; temperature: number; humidity: number; vpd: number; }[]; hourlyBreakdown: { hour: string; avgTemp: number; avgHumidity: number; avgVpd: number; }[]; alertAnalytics?: { summary: { total: number; resolved: number; acknowledged: number; unresolved: number; resolutionRate: string | number; }; responseTimes: { avgMinutes: string | null; minMinutes: string | null; maxMinutes: string | null; }; resolutionTimes: { avgMinutes: string | null; minMinutes: string | null; maxMinutes: string | null; }; byType: { type: string; count: number; resolved: number; avgResolutionMin: string | null; }[]; recentAlerts: { id: string; type: string; severity: string; message: string; value: number; threshold: number; createdAt: string; acknowledgedAt: string | null; resolvedAt: string | null; responseTimeMin: number | null; resolutionTimeMin: number | null; }[]; }; } export default function EnvironmentReportPage() { const [reportData, setReportData] = useState(null); const [loading, setLoading] = useState(true); const [pdfExporting, setPdfExporting] = useState(false); const [dateRange, setDateRange] = useState<'24h' | '7d' | '30d'>('24h'); const reportRef = useRef(null); // Define light theme chart colors for the report const reportTheme = { background: '#ffffff', text: '#000000', grid: '#e2e8f0', temp: '#dc2626', // Red-600 humidity: '#2563eb', // Blue-600 vpd: '#9333ea', // Purple-600 success: '#16a34a', // Green-600 }; useEffect(() => { fetchReportData(); }, [dateRange]); async function fetchReportData() { setLoading(true); try { // Fetch Pulse readings and history const [readingsRes, devicesRes] = await Promise.all([ api.get('/pulse/readings'), api.get('/pulse/devices') ]); const devices = devicesRes.data.devices || []; const readings = readingsRes.data.readings || []; // Get history for first device (or all) const hours = dateRange === '24h' ? 24 : dateRange === '7d' ? 168 : 720; let history: any[] = []; if (devices.length > 0) { try { const historyRes = await api.get(`/pulse/devices/${devices[0].id}/history?hours=${hours}`); history = (historyRes.data.readings || []).map((r: any) => ({ timestamp: new Date(r.timestamp).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }), temperature: r.temperature, humidity: r.humidity, vpd: r.vpd })); } catch { // Use current readings as fallback } } // Calculate stats const deviceStats = readings.map((r: any) => ({ id: r.deviceId, name: r.deviceName, readings: history.length || 1, stats: { temperature: { min: history.length ? Math.min(...history.map(h => h.temperature)) : r.temperature, max: history.length ? Math.max(...history.map(h => h.temperature)) : r.temperature, avg: history.length ? history.reduce((a, h) => a + h.temperature, 0) / history.length : r.temperature }, humidity: { min: history.length ? Math.min(...history.map(h => h.humidity)) : r.humidity, max: history.length ? Math.max(...history.map(h => h.humidity)) : r.humidity, avg: history.length ? history.reduce((a, h) => a + h.humidity, 0) / history.length : r.humidity }, vpd: { min: history.length ? Math.min(...history.map(h => h.vpd)) : r.vpd, max: history.length ? Math.max(...history.map(h => h.vpd)) : r.vpd, avg: history.length ? history.reduce((a, h) => a + h.vpd, 0) / history.length : r.vpd } } })); // Calculate hourly breakdown const hourlyMap = new Map(); history.forEach(h => { const hour = new Date(h.timestamp).getHours().toString().padStart(2, '0') + ':00'; if (!hourlyMap.has(hour)) { hourlyMap.set(hour, { temps: [], humidities: [], vpds: [] }); } const bucket = hourlyMap.get(hour)!; bucket.temps.push(h.temperature); bucket.humidities.push(h.humidity); bucket.vpds.push(h.vpd); }); const hourlyBreakdown = Array.from(hourlyMap.entries()).map(([hour, data]) => ({ hour, avgTemp: data.temps.reduce((a, b) => a + b, 0) / data.temps.length, avgHumidity: data.humidities.reduce((a, b) => a + b, 0) / data.humidities.length, avgVpd: data.vpds.reduce((a, b) => a + b, 0) / data.vpds.length })).sort((a, b) => a.hour.localeCompare(b.hour)); // Fetch alerts let alertData = { total: 0, resolved: 0, byType: [] as any[] }; try { const alertsRes = await api.get('/environment/alerts'); const alerts = alertsRes.data || []; alertData = { total: alerts.length, resolved: alerts.filter((a: any) => a.resolvedAt).length, byType: Object.entries( alerts.reduce((acc: any, a: any) => { acc[a.type] = (acc[a.type] || 0) + 1; return acc; }, {}) ).map(([type, count]) => ({ type, count: count as number })) }; } catch { // Ignore } // Fetch alert analytics with response times let alertAnalytics = null; try { const analyticsRes = await api.get(`/environment/alerts/analytics?hours=${hours}`); alertAnalytics = analyticsRes.data; } catch { // Ignore } const now = new Date(); const startDate = new Date(); startDate.setHours(startDate.getHours() - hours); setReportData({ period: { start: startDate.toLocaleDateString(), end: now.toLocaleDateString() }, devices: deviceStats, alerts: alertData, history: history.slice(-100), // Limit for chart performance hourlyBreakdown, alertAnalytics }); } catch (error) { console.error('Failed to fetch report data:', error); } finally { setLoading(false); } } function handlePrint() { window.print(); } async function handleExportPDF() { if (!reportRef.current) return; setPdfExporting(true); try { const element = reportRef.current; const canvas = await html2canvas(element, { scale: 2, // Higher resolution useCORS: true, logging: false, backgroundColor: '#0f172a' // Dark background matching theme }); const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF('p', 'mm', 'a4'); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const imgWidth = pdfWidth; const imgHeight = (canvas.height * imgWidth) / canvas.width; let heightLeft = imgHeight; let position = 0; // First page pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pdfHeight; // Subsequent pages while (heightLeft >= 0) { position = heightLeft - imgHeight; pdf.addPage(); pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight); heightLeft -= pdfHeight; } pdf.save(`veridian-environment-report-${new Date().toISOString().split('T')[0]}.pdf`); } catch (error) { console.error('PDF export failed:', error); } finally { setPdfExporting(false); } } const formatDate = () => { return new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); }; if (loading) { return (
); } return ( <> {/* Print Styles */}
{/* --- WEB DASHBOARD VIEW --- */}

Environment Report

Generate and export environmental monitoring reports

{/* Date Range Selector */}
{/* Export Buttons */}
{/* Dashboard Header */}
Environment Monitoring Report

Veridian Grow Operations

{reportData?.period.start} – {reportData?.period.end}

Generated: {formatDate()}

{/* Summary Stats */} {/* Summary Stats */}
Avg Temperature

{reportData?.devices[0]?.stats.temperature.avg.toFixed(1) || '--'}°F

Range: {reportData?.devices[0]?.stats.temperature.min.toFixed(1)}° – {reportData?.devices[0]?.stats.temperature.max.toFixed(1)}°

Avg Humidity

{reportData?.devices[0]?.stats.humidity.avg.toFixed(0) || '--'}%

Range: {reportData?.devices[0]?.stats.humidity.min.toFixed(0)}% – {reportData?.devices[0]?.stats.humidity.max.toFixed(0)}%

Avg VPD

{reportData?.devices[0]?.stats.vpd.avg.toFixed(2) || '--'} kPa

Range: {reportData?.devices[0]?.stats.vpd.min.toFixed(2)} – {reportData?.devices[0]?.stats.vpd.max.toFixed(2)}

Alerts

{reportData?.alerts.total || 0}

{reportData?.alerts.resolved || 0} resolved

{/* Temperature Trend Chart */}

Temperature Trend

{/* Humidity and VPD Charts */}

Humidity Trend

VPD Trend

{/* Hourly Breakdown */} {reportData?.hourlyBreakdown && reportData.hourlyBreakdown.length > 0 && (

Hourly Average Temperature

)} {/* Device Summary Table */}

Device Summary

{reportData?.devices.map(device => ( ))}
Device Readings Temp (Avg) Humidity (Avg) VPD (Avg)
{device.name} {device.readings} {device.stats.temperature.avg.toFixed(1)}°F {device.stats.humidity.avg.toFixed(0)}% {device.stats.vpd.avg.toFixed(2)}
{/* Alert Summary */} {reportData?.alerts.byType && reportData.alerts.byType.length > 0 && (

Alert Summary

{reportData.alerts.byType.map(alert => (

{alert.type.replace('_', ' ')}

{alert.count}

))}
)} {/* Response Time Analytics */} {reportData?.alertAnalytics && (

Alert Response Time Analytics

{/* KPIs */}

Resolution Rate

{reportData.alertAnalytics.summary.resolutionRate}%

{reportData.alertAnalytics.summary.resolved} of {reportData.alertAnalytics.summary.total}

Avg Resolution Time

{reportData.alertAnalytics.resolutionTimes.avgMinutes || '--'}

minutes

Fastest Resolution

{reportData.alertAnalytics.resolutionTimes.minMinutes || '--'}

minutes

Slowest Resolution

{reportData.alertAnalytics.resolutionTimes.maxMinutes || '--'}

minutes

{/* By Alert Type */} {reportData.alertAnalytics.byType.length > 0 && (

Resolution Time by Alert Type

{reportData.alertAnalytics.byType.map(alertType => ( ))}
Alert Type Total Resolved Avg Resolution Resolution Rate
{alertType.type.replace('_', ' ')} {alertType.count} {alertType.resolved} {alertType.avgResolutionMin ? `${alertType.avgResolutionMin} min` : '--'} 0 && alertType.resolved / alertType.count >= 0.9 ? 'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400' : alertType.count > 0 && alertType.resolved / alertType.count >= 0.5 ? 'bg-amber-100 dark:bg-amber-500/20 text-amber-700 dark:text-amber-400' : 'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400' }`}> {alertType.count > 0 ? Math.round(alertType.resolved / alertType.count * 100) : 0}%
)} {/* Recent Alerts Timeline */} {reportData.alertAnalytics.recentAlerts.length > 0 && (

Recent Alert Timeline

{reportData.alertAnalytics.recentAlerts.slice(0, 5).map(alert => (
{alert.severity} {alert.type.replace('_', ' ')}
{alert.resolvedAt ? 'Resolved' : 'Pending'}

{alert.message}

Triggered: {new Date(alert.createdAt).toLocaleString()} {alert.resolutionTimeMin && ( Return to Normal: {alert.resolutionTimeMin.toFixed(1)} min )}
))}
)}
)} {/* Footer */}

Veridian Grow Operations Manager • Environment Monitoring Report

Generated automatically • Data subject to sensor accuracy

{/* --- CLEAN DOCUMENT LAYOUT (For PDF Generation) --- */} {/* Wrapper for off-screen positioning - MUST be visible for html2canvas */}
{/* Document Header */}

Environment Report

Veridian Grow Operations

Report Period

{reportData?.period.start} — {reportData?.period.end}

Generated: {new Date().toLocaleDateString()}

{/* Executive Summary Table */}

Executive Summary

Avg Temperature {reportData?.devices[0]?.stats.temperature.avg.toFixed(1)}°F Min: {reportData?.devices[0]?.stats.temperature.min.toFixed(1)}° / Max: {reportData?.devices[0]?.stats.temperature.max.toFixed(1)}°
Avg Humidity {reportData?.devices[0]?.stats.humidity.avg.toFixed(0)}% Min: {reportData?.devices[0]?.stats.humidity.min.toFixed(0)}% / Max: {reportData?.devices[0]?.stats.humidity.max.toFixed(0)}%
Avg VPD {reportData?.devices[0]?.stats.vpd.avg.toFixed(2)} Min: {reportData?.devices[0]?.stats.vpd.min.toFixed(2)} / Max: {reportData?.devices[0]?.stats.vpd.max.toFixed(2)}
Total Alerts {reportData?.alertAnalytics?.summary.total || 0} Resolution Rate: {reportData?.alertAnalytics?.summary.resolutionRate}%
{/* Historical Charts (Clean Style) */}

Environmental Trends

Temperature History (°F)

val.split(',')[1]} />

Humidity History (%)

VPD History (kPa)

{/* Data Tables Section */}

Alert & Intervention Analysis

{/* KPI Table */}
Metric Value Notes
Avg Resolution Time {reportData?.alertAnalytics?.resolutionTimes.avgMinutes || '--'} min Average time to return to normal range
Resolution Rate {reportData?.alertAnalytics?.summary.resolutionRate}% {reportData?.alertAnalytics?.summary.resolved} resolved out of {reportData?.alertAnalytics?.summary.total} total
Slowest Response {reportData?.alertAnalytics?.resolutionTimes.maxMinutes || '--'} min Max deviation duration
{/* Recent Alerts List */} {reportData?.alertAnalytics?.recentAlerts && reportData.alertAnalytics.recentAlerts.length > 0 ? ( {reportData.alertAnalytics.recentAlerts.slice(0, 15).map((alert, idx) => ( ))}
Time Severity Alert Type Message Duration
{new Date(alert.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' })} {alert.severity} {alert.type.replace('_', ' ')} {alert.message} {alert.resolutionTimeMin ? `${alert.resolutionTimeMin.toFixed(1)}m` : '-'}
) : (

No recent alerts found for this period.

)}
{/* Footer */}

© {new Date().getFullYear()} Veridian Systems • Generated via Environment Operations Manager

); }