215 lines
6.2 KiB
TypeScript
215 lines
6.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
|
import SiteSelector from '@/components/dashboards/SiteSelector';
|
|
import SiteStatus from '@/components/dashboards/SiteStatus';
|
|
import KPI_Table from '@/components/dashboards/KPIStatus';
|
|
import DashboardLayout from './dashlayout';
|
|
import html2canvas from 'html2canvas';
|
|
import jsPDF from 'jspdf';
|
|
import dynamic from 'next/dynamic';
|
|
import { fetchPowerTimeseries } from '@/app/utils/api';
|
|
|
|
const EnergyLineChart = dynamic(() => import('@/components/dashboards/EnergyLineChart'), {
|
|
ssr: false,
|
|
});
|
|
|
|
const MonthlyBarChart = dynamic(() => import('@/components/dashboards/MonthlyBarChart'), {
|
|
ssr: false,
|
|
});
|
|
|
|
import { SiteName, SiteDetails, mockSiteData } from '@/types/SiteData';
|
|
|
|
const AdminDashboard = () => {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const siteIdMap: Record<SiteName, string> = {
|
|
'Site A': 'site_01',
|
|
'Site B': 'site_02',
|
|
'Site C': 'site_03',
|
|
};
|
|
|
|
const siteParam = searchParams?.get('site');
|
|
const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
|
|
|
|
const [selectedSite, setSelectedSite] = useState<SiteName>(() => {
|
|
if (siteParam && validSiteNames.includes(siteParam as SiteName)) {
|
|
return siteParam as SiteName;
|
|
}
|
|
return 'Site A';
|
|
});
|
|
|
|
// Keep siteParam and selectedSite in sync
|
|
useEffect(() => {
|
|
if (
|
|
siteParam &&
|
|
validSiteNames.includes(siteParam as SiteName) &&
|
|
siteParam !== selectedSite
|
|
) {
|
|
setSelectedSite(siteParam as SiteName);
|
|
}
|
|
}, [siteParam, selectedSite]);
|
|
|
|
const [timeSeriesData, setTimeSeriesData] = useState<{
|
|
consumption: { time: string; value: number }[];
|
|
generation: { time: string; value: number }[];
|
|
}>({ consumption: [], generation: [] });
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
|
|
const siteId = siteIdMap[selectedSite];
|
|
const today = new Date();
|
|
|
|
// Format to YYYY-MM-DD
|
|
const yyyyMMdd = today.toISOString().split('T')[0];
|
|
|
|
// Append Malaysia's +08:00 time zone manually
|
|
const start = `${yyyyMMdd}T00:00:00+08:00`;
|
|
const end = `${yyyyMMdd}T23:59:59+08:00`;
|
|
|
|
|
|
try {
|
|
const raw = await fetchPowerTimeseries(siteId, start, end);
|
|
|
|
const consumption = raw.consumption.map(d => ({
|
|
time: d.time,
|
|
value: d.value,
|
|
}));
|
|
|
|
const generation = raw.generation.map(d => ({
|
|
time: d.time,
|
|
value: d.value,
|
|
}));
|
|
|
|
setTimeSeriesData({ consumption, generation });
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch power time series:', error);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, [selectedSite]);
|
|
|
|
// Update query string when site is changed manually
|
|
const handleSiteChange = (newSite: SiteName) => {
|
|
setSelectedSite(newSite);
|
|
const newUrl = `${pathname}?site=${encodeURIComponent(newSite)}`;
|
|
router.push(newUrl);
|
|
};
|
|
|
|
const currentSiteDetails: SiteDetails = mockSiteData[selectedSite] || {
|
|
location: 'N/A',
|
|
inverterProvider: 'N/A',
|
|
emergencyContact: 'N/A',
|
|
lastSyncTimestamp: 'N/A',
|
|
consumptionData: [],
|
|
generationData: [],
|
|
systemStatus: 'N/A',
|
|
temperature: 'N/A',
|
|
solarPower: 0,
|
|
realTimePower: 0,
|
|
installedPower: 0,
|
|
};
|
|
|
|
const handleCSVExport = () => {
|
|
alert('Exported raw data to CSV (mock)');
|
|
};
|
|
|
|
const energyChartRef = useRef(null);
|
|
const monthlyChartRef = useRef(null);
|
|
|
|
const handlePDFExport = async () => {
|
|
const doc = new jsPDF('p', 'mm', 'a4'); // portrait, millimeters, A4
|
|
const chartRefs = [
|
|
{ ref: energyChartRef, title: 'Energy Line Chart' },
|
|
{ ref: monthlyChartRef, title: 'Monthly Energy Yield' }
|
|
];
|
|
|
|
let yOffset = 10;
|
|
|
|
for (const chart of chartRefs) {
|
|
if (!chart.ref.current) continue;
|
|
|
|
// Capture chart as image
|
|
const canvas = await html2canvas(chart.ref.current, {
|
|
scale: 2, // Higher scale for better resolution
|
|
});
|
|
|
|
const imgData = canvas.toDataURL('image/png');
|
|
const imgProps = doc.getImageProperties(imgData);
|
|
|
|
const pdfWidth = doc.internal.pageSize.getWidth() - 20; // 10 margin each side
|
|
const imgHeight = (imgProps.height * pdfWidth) / imgProps.width;
|
|
|
|
// Add title and image
|
|
doc.setFontSize(14);
|
|
doc.text(chart.title, 10, yOffset);
|
|
yOffset += 6; // Space between title and chart
|
|
|
|
// If content will overflow page, add a new page
|
|
if (yOffset + imgHeight > doc.internal.pageSize.getHeight()) {
|
|
doc.addPage();
|
|
yOffset = 10;
|
|
}
|
|
|
|
doc.addImage(imgData, 'PNG', 10, yOffset, pdfWidth, imgHeight);
|
|
yOffset += imgHeight + 10; // Update offset for next chart
|
|
}
|
|
|
|
doc.save('dashboard_charts.pdf');
|
|
};
|
|
const currentMonth = new Date().toISOString().slice(0, 7); // "YYYY-MM"
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="px-6 space-y-6">
|
|
<h1 className="text-lg font-semibold">Admin Dashboard</h1>
|
|
|
|
<div className="grid md:grid-cols-2 gap-6">
|
|
<div className="space-y-4">
|
|
<SiteSelector
|
|
selectedSite={selectedSite}
|
|
setSelectedSite={handleSiteChange}
|
|
/>
|
|
<SiteStatus
|
|
selectedSite={selectedSite}
|
|
location={currentSiteDetails.location}
|
|
inverterProvider={currentSiteDetails.inverterProvider}
|
|
emergencyContact={currentSiteDetails.emergencyContact}
|
|
lastSyncTimestamp={currentSiteDetails.lastSyncTimestamp}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<KPI_Table siteId={siteIdMap[selectedSite]} month={currentMonth} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-6 lg:flex-col justify-center">
|
|
<div ref={energyChartRef} className="pb-5">
|
|
<EnergyLineChart siteId={siteIdMap[selectedSite]} />
|
|
|
|
</div>
|
|
<div ref={monthlyChartRef} className="pb-5">
|
|
<MonthlyBarChart siteId={siteIdMap[selectedSite]} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col md:flex-row gap-4 justify-center">
|
|
<button onClick={handlePDFExport} className="text-sm lg:text-lg btn-primary">
|
|
Export Chart Images to PDF
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
};
|
|
|
|
export default AdminDashboard;
|
|
|
|
|