feat: Dedicated professional print layout for environment reports
Some checks are pending
Test / frontend-test (push) Waiting to run
Test / backend-test (push) Waiting to run

This commit is contained in:
fullsizemalt 2026-01-06 02:42:53 -08:00
parent add6c6d305
commit 2998b90fe0

View file

@ -85,6 +85,17 @@ export default function EnvironmentReportPage() {
const [dateRange, setDateRange] = useState<'24h' | '7d' | '30d'>('24h');
const reportRef = useRef<HTMLDivElement>(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]);
@ -299,7 +310,7 @@ export default function EnvironmentReportPage() {
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-8">
<div className="max-w-5xl mx-auto">
{/* Header Controls - Hidden in print */}
{/* --- WEB DASHBOARD VIEW --- */}
<div className="no-print mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-white flex items-center gap-3">
@ -351,9 +362,9 @@ export default function EnvironmentReportPage() {
</div>
</div>
{/* Report Content */}
<div ref={reportRef} className="space-y-6">
{/* Report Header */}
{/* Dashboard Content (Web View) */}
<div className={`space-y-6 no-print transition-opacity duration-300 ${pdfExporting ? 'opacity-50 pointer-events-none grayscale' : ''}`}>
{/* Dashboard Header */}
<div className="p-8 rounded-2xl bg-slate-800/50 border border-slate-700/50 text-center">
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-emerald-500/20 text-emerald-400 rounded-full text-sm font-medium mb-4">
<Activity className="w-4 h-4" />
@ -741,6 +752,186 @@ export default function EnvironmentReportPage() {
<p className="mt-1">Generated automatically Data subject to sensor accuracy</p>
</div>
</div>
{/* --- CLEAN DOCUMENT LAYOUT (For PDF Generation) --- */}
{/* This div is referenced by reportRef for html2canvas */}
<div
ref={reportRef}
className={`bg-white text-black p-12 mx-auto shadow-2xl ${pdfExporting ? 'block' : 'hidden'}`}
style={{
width: '210mm', // A4 Width
minHeight: '297mm', // A4 Height
fontFamily: '"Inter", "Helvetica Neue", Arial, sans-serif'
}}
>
{/* Document Header */}
<div className="border-b-2 border-slate-900 pb-6 mb-8 flex justify-between items-end">
<div>
<h1 className="text-4xl font-bold tracking-tight text-slate-900 mb-2">Environment Report</h1>
<p className="text-slate-500 font-medium">Veridian Grow Operations</p>
</div>
<div className="text-right">
<p className="text-sm text-slate-500">Report Period</p>
<p className="font-semibold text-slate-900">{reportData?.period.start} {reportData?.period.end}</p>
<p className="text-xs text-slate-400 mt-1">Generated: {new Date().toLocaleDateString()}</p>
</div>
</div>
{/* Executive Summary Table */}
<div className="mb-10">
<h2 className="text-lg font-bold uppercase tracking-wider text-slate-700 mb-4 border-l-4 border-slate-900 pl-3">
Executive Summary
</h2>
<div className="grid grid-cols-4 gap-6 mb-6">
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Avg Temperature</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.devices[0]?.stats.temperature.avg.toFixed(1)}°F</span>
<span className="text-xs text-slate-500">Min: {reportData?.devices[0]?.stats.temperature.min.toFixed(1)}° / Max: {reportData?.devices[0]?.stats.temperature.max.toFixed(1)}°</span>
</div>
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Avg Humidity</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.devices[0]?.stats.humidity.avg.toFixed(0)}%</span>
<span className="text-xs text-slate-500">Min: {reportData?.devices[0]?.stats.humidity.min.toFixed(0)}% / Max: {reportData?.devices[0]?.stats.humidity.max.toFixed(0)}%</span>
</div>
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Avg VPD</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.devices[0]?.stats.vpd.avg.toFixed(2)}</span>
<span className="text-xs text-slate-500">Min: {reportData?.devices[0]?.stats.vpd.min.toFixed(2)} / Max: {reportData?.devices[0]?.stats.vpd.max.toFixed(2)}</span>
</div>
<div className="p-4 bg-slate-50 border border-slate-200 rounded">
<span className="block text-xs font-semibold uppercase text-slate-500 mb-1">Total Alerts</span>
<span className="block text-3xl font-bold text-slate-900">{reportData?.alertAnalytics?.summary.total || 0}</span>
<span className="text-xs text-slate-500">Resolution Rate: {reportData?.alertAnalytics?.summary.resolutionRate}%</span>
</div>
</div>
</div>
{/* Historical Charts (Clean Style) */}
<div className="mb-10 page-break-inside-avoid">
<h2 className="text-lg font-bold uppercase tracking-wider text-slate-700 mb-4 border-l-4 border-slate-900 pl-3">
Environmental Trends
</h2>
<div className="space-y-8">
<div className="h-64 w-full border border-slate-200 p-4 rounded bg-white">
<h3 className="text-sm font-semibold text-slate-600 mb-2 text-center">Temperature History (°F)</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={reportData?.history || []}>
<CartesianGrid strokeDasharray="3 3" stroke={reportTheme.grid} vertical={false} />
<XAxis dataKey="timestamp" stroke={reportTheme.text} fontSize={10} tickLine={false} tickFormatter={(val) => val.split(',')[1]} />
<YAxis stroke={reportTheme.text} fontSize={10} tickLine={false} domain={['auto', 'auto']} />
<Area type="monotone" dataKey="temperature" stroke={reportTheme.temp} fill={reportTheme.temp} fillOpacity={0.1} strokeWidth={2} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="h-48 w-full border border-slate-200 p-4 rounded bg-white">
<h3 className="text-sm font-semibold text-slate-600 mb-2 text-center">Humidity History (%)</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={reportData?.history || []}>
<CartesianGrid strokeDasharray="3 3" stroke={reportTheme.grid} vertical={false} />
<XAxis dataKey="timestamp" stroke={reportTheme.text} fontSize={9} tick={false} />
<YAxis stroke={reportTheme.text} fontSize={9} tickLine={false} domain={[0, 100]} />
<Area type="monotone" dataKey="humidity" stroke={reportTheme.humidity} fill={reportTheme.humidity} fillOpacity={0.1} strokeWidth={2} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="h-48 w-full border border-slate-200 p-4 rounded bg-white">
<h3 className="text-sm font-semibold text-slate-600 mb-2 text-center">VPD History (kPa)</h3>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={reportData?.history || []}>
<CartesianGrid strokeDasharray="3 3" stroke={reportTheme.grid} vertical={false} />
<XAxis dataKey="timestamp" stroke={reportTheme.text} fontSize={9} tick={false} />
<YAxis stroke={reportTheme.text} fontSize={9} tickLine={false} domain={[0, 2.5]} />
<Area type="monotone" dataKey="vpd" stroke={reportTheme.vpd} fill={reportTheme.vpd} fillOpacity={0.1} strokeWidth={2} isAnimationActive={false} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
{/* Data Tables Section */}
<div className="mb-8">
<h2 className="text-lg font-bold uppercase tracking-wider text-slate-700 mb-4 border-l-4 border-slate-900 pl-3">
Alert & Intervention Analysis
</h2>
{/* KPI Table */}
<table className="w-full text-sm text-left border border-slate-200 mb-6">
<thead className="bg-slate-100 text-slate-700 font-semibold uppercase text-xs">
<tr>
<th className="px-4 py-3 border-b border-slate-200">Metric</th>
<th className="px-4 py-3 border-b border-slate-200">Value</th>
<th className="px-4 py-3 border-b border-slate-200">Notes</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
<tr>
<td className="px-4 py-2 font-medium">Avg Resolution Time</td>
<td className="px-4 py-2">{reportData?.alertAnalytics?.resolutionTimes.avgMinutes || '--'} min</td>
<td className="px-4 py-2 text-slate-500">Average time to return to normal range</td>
</tr>
<tr>
<td className="px-4 py-2 font-medium">Resolution Rate</td>
<td className="px-4 py-2">{reportData?.alertAnalytics?.summary.resolutionRate}%</td>
<td className="px-4 py-2 text-slate-500">{reportData?.alertAnalytics?.summary.resolved} resolved out of {reportData?.alertAnalytics?.summary.total} total</td>
</tr>
<tr>
<td className="px-4 py-2 font-medium">Slowest Response</td>
<td className="px-4 py-2">{reportData?.alertAnalytics?.resolutionTimes.maxMinutes || '--'} min</td>
<td className="px-4 py-2 text-slate-500">Max deviation duration</td>
</tr>
</tbody>
</table>
{/* Recent Alerts List */}
{reportData?.alertAnalytics?.recentAlerts && reportData.alertAnalytics.recentAlerts.length > 0 ? (
<table className="w-full text-xs text-left border border-slate-200">
<thead className="bg-slate-800 text-white font-semibold uppercase">
<tr>
<th className="px-4 py-2">Time</th>
<th className="px-4 py-2">Severity</th>
<th className="px-4 py-2">Alert Type</th>
<th className="px-4 py-2">Message</th>
<th className="px-4 py-2 text-right">Duration</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{reportData.alertAnalytics.recentAlerts.slice(0, 15).map((alert, idx) => (
<tr key={alert.id} className={idx % 2 === 0 ? 'bg-white' : 'bg-slate-50'}>
<td className="px-4 py-2 whitespace-nowrap text-slate-600">
{new Date(alert.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' })}
</td>
<td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full font-bold text-[10px] ${alert.severity === 'CRITICAL' ? 'bg-red-100 text-red-800' :
alert.severity === 'WARNING' ? 'bg-amber-100 text-amber-800' :
'bg-blue-100 text-blue-800'
}`}>
{alert.severity}
</span>
</td>
<td className="px-4 py-2 font-medium text-slate-900">{alert.type.replace('_', ' ')}</td>
<td className="px-4 py-2 text-slate-600 truncate max-w-xs">{alert.message}</td>
<td className="px-4 py-2 text-right font-mono text-slate-700">
{alert.resolutionTimeMin ? `${alert.resolutionTimeMin.toFixed(1)}m` : '-'}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-slate-500 italic text-sm p-4 border border-slate-200 rounded">No recent alerts found for this period.</p>
)}
</div>
{/* Footer */}
<div className="mt-12 pt-6 border-t border-slate-200 text-center text-xs text-slate-400">
<p>© {new Date().getFullYear()} Veridian Systems Generated via Environment Operations Manager</p>
</div>
</div>
</div>
</div>
</>