feat: Dedicated professional print layout for environment reports
This commit is contained in:
parent
add6c6d305
commit
2998b90fe0
1 changed files with 195 additions and 4 deletions
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue