'use client'; import { useState, useEffect, useMemo, useRef } from 'react'; import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import SiteSelector from '@/components/dashboards/SiteSelector'; import SiteStatus from '@/components/dashboards/SiteStatus'; import DashboardLayout from './dashlayout'; import html2canvas from 'html2canvas'; import jsPDF from 'jspdf'; import dynamic from 'next/dynamic'; import { fetchPowerTimeseries } from '@/app/utils/api'; import KpiTop from '@/components/dashboards/kpitop'; import KpiBottom from '@/components/dashboards/kpibottom'; import { formatAddress } from '@/app/utils/formatAddress'; import { formatCrmTimestamp } from '@/app/utils/datetime'; import LoggingControlCard from '@/components/dashboards/LoggingControl'; const EnergyLineChart = dynamic(() => import('@/components/dashboards/EnergyLineChart'), { ssr: false }); const MonthlyBarChart = dynamic(() => import('@/components/dashboards/MonthlyBarChart'), { ssr: false }); type MonthlyKPI = { site: string; month: string; yield_kwh: number | null; consumption_kwh: number | null; grid_draw_kwh: number | null; efficiency: number | null; peak_demand_kw: number | null; avg_power_factor: number | null; load_factor: number | null; error?: string; }; type CrmProject = { name: string; // e.g. PROJ-0008 <-- use as siteId project_name: string; status?: string; percent_complete?: number | null; owner?: string | null; modified?: string | null; customer?: string | null; project_type?: string | null; custom_address?: string | null; custom_email?: string | null; custom_mobile_phone_no?: string | null; }; const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'; // Adjust this to your FastAPI route const START_LOGGING_ENDPOINT = (siteId: string) => `${API}/logging/start?site=${encodeURIComponent(siteId)}`; // helper to build ISO strings with +08:00 const withTZ = (d: Date) => { const yyyyMMdd = d.toISOString().split('T')[0]; return { start: `${yyyyMMdd}T00:00:00+08:00`, end: `${yyyyMMdd}T23:59:59+08:00`, }; }; const AdminDashboard = () => { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); // --- load CRM projects dynamically --- const [sites, setSites] = useState([]); const [sitesLoading, setSitesLoading] = useState(true); const [sitesError, setSitesError] = useState(null); // near other refs const loggingRef = useRef(null); useEffect(() => { setSitesLoading(true); fetch(`${API}/crm/projects?limit=0`) .then(r => r.json()) .then(json => setSites(json?.data ?? [])) .catch(setSitesError) .finally(() => setSitesLoading(false)); }, []); // The canonical siteId is the CRM Project "name" (e.g., PROJ-0008) const siteParam = searchParams?.get('site') || null; const [selectedSiteId, setSelectedSiteId] = useState(siteParam); // Keep query param <-> state in sync useEffect(() => { if ((siteParam || null) !== selectedSiteId) { setSelectedSiteId(siteParam); } }, [siteParam]); // eslint-disable-line // Default to the first site when loaded useEffect(() => { if (!selectedSiteId && sites.length) { setSelectedSiteId(sites[0].name); router.replace(`${pathname}?site=${encodeURIComponent(sites[0].name)}`); } }, [sites, selectedSiteId, pathname, router]); // Current selected CRM project const selectedProject: CrmProject | null = useMemo( () => sites.find(s => s.name === selectedSiteId) ?? null, [sites, selectedSiteId] ); // declare currentMonth BEFORE it’s used const currentMonth = useMemo(() => new Date().toISOString().slice(0, 7), []); // --- Time-series state --- const [timeSeriesData, setTimeSeriesData] = useState<{ consumption: { time: string; value: number }[]; generation: { time: string; value: number }[]; }>({ consumption: [], generation: [] }); // data-availability flags const [hasAnyData, setHasAnyData] = useState(false); // historical window const [hasTodayData, setHasTodayData] = useState(false); const [isLogging, setIsLogging] = useState(false); const [startError, setStartError] = useState(null); // Fetch today’s timeseries for selected siteId useEffect(() => { if (!selectedSiteId) return; const fetchToday = async () => { const { start, end } = withTZ(new Date()); try { const raw = await fetchPowerTimeseries(selectedSiteId, start, end); const consumption = raw.consumption.map((d: any) => ({ time: d.time, value: d.value })); const generation = raw.generation.map((d: any) => ({ time: d.time, value: d.value })); setTimeSeriesData({ consumption, generation }); const anyToday = (consumption?.length ?? 0) > 0 || (generation?.length ?? 0) > 0; setHasTodayData(anyToday); } catch (error) { console.error('Failed to fetch power time series:', error); setHasTodayData(false); } }; fetchToday(); }, [selectedSiteId]); // Check historical data (last 30 days) → controls empty state useEffect(() => { if (!selectedSiteId) return; const fetchHistorical = async () => { try { const endDate = new Date(); const startDate = new Date(); startDate.setDate(endDate.getDate() - 30); const startISO = `${startDate.toISOString().split('T')[0]}T00:00:00+08:00`; const endISO = `${endDate.toISOString().split('T')[0]}T23:59:59+08:00`; const raw = await fetchPowerTimeseries(selectedSiteId, startISO, endISO); const anyHistorical = (raw?.consumption?.length ?? 0) > 0 || (raw?.generation?.length ?? 0) > 0; setHasAnyData(anyHistorical); } catch (e) { console.error('Failed to check historical data:', e); setHasAnyData(false); } }; fetchHistorical(); }, [selectedSiteId]); // --- KPI monthly --- const [kpi, setKpi] = useState(null); useEffect(() => { if (!selectedSiteId) return; const url = `${API}/kpi/monthly?site=${encodeURIComponent(selectedSiteId)}&month=${currentMonth}`; fetch(url).then(r => r.json()).then(setKpi).catch(console.error); }, [selectedSiteId, currentMonth]); // derived values with safe fallbacks const yieldKwh = kpi?.yield_kwh ?? 0; const consumptionKwh = kpi?.consumption_kwh ?? 0; const gridDrawKwh = kpi?.grid_draw_kwh ?? Math.max(0, consumptionKwh - yieldKwh); const efficiencyPct = (kpi?.efficiency ?? 0) * 100; const powerFactor = kpi?.avg_power_factor ?? 0; const loadFactor = (kpi?.load_factor ?? 0); // Update URL when site is changed manually (expects a siteId/Project.name) const handleSiteChange = (newSiteId: string) => { setSelectedSiteId(newSiteId); const newUrl = `${pathname}?site=${encodeURIComponent(newSiteId)}`; router.push(newUrl); // reset flags when switching setHasAnyData(false); setHasTodayData(false); setIsLogging(false); setStartError(null); }; const locationFormatted = useMemo(() => { const raw = selectedProject?.custom_address ?? ''; if (!raw) return 'N/A'; return formatAddress(raw).multiLine; }, [selectedProject?.custom_address]); const lastSyncFormatted = useMemo( () => formatCrmTimestamp(selectedProject?.modified, { includeSeconds: true }), [selectedProject?.modified] ); // Adapt CRM project -> SiteStatus props const currentSiteDetails = { location: locationFormatted, inverterProvider: selectedProject?.project_type || 'N/A', emergencyContact: selectedProject?.custom_mobile_phone_no || selectedProject?.custom_email || selectedProject?.customer || 'N/A', lastSyncTimestamp: lastSyncFormatted || 'N/A', }; const energyChartRef = useRef(null); const monthlyChartRef = useRef(null); const handlePDFExport = async () => { const doc = new jsPDF('p', 'mm', '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; const canvas = await html2canvas(chart.ref.current, { scale: 2 }); const imgData = canvas.toDataURL('image/png'); const imgProps = doc.getImageProperties(imgData); const pdfWidth = doc.internal.pageSize.getWidth() - 20; const imgHeight = (imgProps.height * pdfWidth) / imgProps.width; doc.setFontSize(14); doc.text(chart.title, 10, yOffset); yOffset += 6; if (yOffset + imgHeight > doc.internal.pageSize.getHeight()) { doc.addPage(); yOffset = 10; } doc.addImage(imgData, 'PNG', 10, yOffset, pdfWidth, imgHeight); yOffset += imgHeight + 10; } doc.save('dashboard_charts.pdf'); }; // Start logging then poll for data until it shows up const startLogging = async () => { if (!selectedSiteId) return; setIsLogging(true); setStartError(null); try { const resp = await fetch(START_LOGGING_ENDPOINT(selectedSiteId), { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); if (!resp.ok) { const text = await resp.text(); throw new Error(text || `Failed with status ${resp.status}`); } // Poll for data for up to ~45s (15 tries x 3s) for (let i = 0; i < 15; i++) { const today = new Date(); const { start, end } = withTZ(today); try { const raw = await fetchPowerTimeseries(selectedSiteId, start, end); const consumption = raw.consumption ?? []; const generation = raw.generation ?? []; if ((consumption.length ?? 0) > 0 || (generation.length ?? 0) > 0) { setHasAnyData(true); // site now has data setHasTodayData(true); // and today has data too break; } } catch { // ignore and keep polling } await new Promise(r => setTimeout(r, 3000)); } } catch (e: any) { setStartError(e?.message ?? 'Failed to start logging'); setIsLogging(false); } }; // ---------- RENDER ---------- if (sitesLoading) { return (
Loading sites…
); } if (sitesError) { return (
Failed to load sites from CRM.
); } if (!selectedProject) { return (
No site selected.
); } // Build selector options from CRM const siteOptions = sites.map(s => ({ label: s.project_name || s.name, value: s.name, })); return (

Admin Dashboard

{/* Selector + status */}
{/* Small dark yellow banner when there is ZERO historical data */} {!hasAnyData && (
No data yet. Enter the meter number and click Start to begin streaming. {startError &&
{startError}
}
)}
{/* Render the rest only if there is *any* data */} {hasAnyData && ( <> {/* Tiny banner if today is empty but historical exists */} {!hasTodayData && (
No data yet today — charts may be blank until new points arrive.
)} {/* TOP 3 CARDS */}
{/* BOTTOM 3 PANELS */}
} right={
{(kpi?.peak_demand_kw ?? 0).toFixed(2)} kW
} />
)}
); }; export default AdminDashboard;