From efdad6dfccbf5c7e93c183e35c38ce626323a1aa Mon Sep 17 00:00:00 2001 From: Syasya Date: Wed, 30 Jul 2025 17:00:43 +0800 Subject: [PATCH] linechart extract db update --- app/(admin)/adminDashboard/page.tsx | 57 +++++++- app/utils/api.ts | 28 ++++ components/dashboards/EnergyLineChart.tsx | 103 ++++++++------ types/SiteData.ts | 155 ++++++++++++++-------- 4 files changed, 243 insertions(+), 100 deletions(-) create mode 100644 app/utils/api.ts diff --git a/app/(admin)/adminDashboard/page.tsx b/app/(admin)/adminDashboard/page.tsx index 5eeafd4..fd2f2bb 100644 --- a/app/(admin)/adminDashboard/page.tsx +++ b/app/(admin)/adminDashboard/page.tsx @@ -9,6 +9,8 @@ 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, }); @@ -45,6 +47,53 @@ const AdminDashboard = () => { } }, [siteParam, selectedSite]); + const [timeSeriesData, setTimeSeriesData] = useState<{ + consumption: { time: string; value: number }[]; + generation: { time: string; value: number }[]; + }>({ consumption: [], generation: [] }); + + useEffect(() => { + const fetchData = async () => { + const siteIdMap: Record = { + 'Site A': 'site_01', + 'Site B': 'site_02', + 'Site C': 'site_03', +}; + + 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); @@ -142,9 +191,11 @@ const AdminDashboard = () => {
+ consumption={timeSeriesData.consumption} + generation={timeSeriesData.generation} +/> + +
diff --git a/app/utils/api.ts b/app/utils/api.ts new file mode 100644 index 0000000..e5a79da --- /dev/null +++ b/app/utils/api.ts @@ -0,0 +1,28 @@ +// app/utils/api.ts +export interface TimeSeriesEntry { + time: string; + value: number; +} + +export interface TimeSeriesResponse { + consumption: TimeSeriesEntry[]; + generation: TimeSeriesEntry[]; +} + +export async function fetchPowerTimeseries( + site: string, + start: string, + end: string +): Promise { // <-- Change here + const params = new URLSearchParams({ site, start, end }); + + const res = await fetch(`http://localhost:8000/power-timeseries?${params.toString()}`); + + if (!res.ok) { + throw new Error(`Failed to fetch data: ${res.status}`); + } + + const json = await res.json(); + console.log(`🔍 API response from /power-timeseries?${params.toString()}:`, json); // ✅ log here + return json; // <-- This is a single object, not an array +} diff --git a/components/dashboards/EnergyLineChart.tsx b/components/dashboards/EnergyLineChart.tsx index 223a4e9..4611ff3 100644 --- a/components/dashboards/EnergyLineChart.tsx +++ b/components/dashboards/EnergyLineChart.tsx @@ -5,23 +5,31 @@ import zoomPlugin from 'chartjs-plugin-zoom'; ChartJS.register(zoomPlugin); -interface EnergyLineChartProps { - consumptionData: number[]; - generationData: number[]; +interface TimeSeriesEntry { + time: string; + value: number; } -const labels = [ - '08:00', '08:30', '09:00', '09:30', '10:00', - '10:30', '11:00', '11:30', '12:00', '12:30', - '13:00', '13:30', '14:00', '14:30', '15:00', - '15:30', '16:00', '16:30', '17:00', -]; +interface EnergyLineChartProps { + consumption: TimeSeriesEntry[]; + generation: TimeSeriesEntry[]; +} -const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartProps) => { +const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => { const chartRef = useRef(null); + // Generate sorted unique time labels from both series + const allTimes = Array.from(new Set([ + ...consumption.map(d => d.time), + ...generation.map(d => d.time), + ])).sort(); // e.g., ["00:00", "00:30", "01:00", ...] + + // Map times to values + const consumptionMap = Object.fromEntries(consumption.map(d => [d.time, d.value])); + const generationMap = Object.fromEntries(generation.map(d => [d.time, d.value])); + const [startIndex, setStartIndex] = useState(0); - const [endIndex, setEndIndex] = useState(labels.length - 1); + const [endIndex, setEndIndex] = useState(allTimes.length - 1); useEffect(() => { if (typeof window !== 'undefined') { @@ -29,14 +37,13 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro } }, []); - // Filter data arrays based on selected range - const filteredConsumption = consumptionData.slice(startIndex, endIndex + 1); - const filteredGeneration = generationData.slice(startIndex, endIndex + 1); - const filteredLabels = labels.slice(startIndex, endIndex + 1); + const filteredLabels = allTimes.slice(startIndex, endIndex + 1); + const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? null); + const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? null); - const allDataPoints = [...filteredConsumption, ...filteredGeneration]; - const maxDataValue = allDataPoints.length > 0 ? Math.max(...allDataPoints) : 0; - const yAxisSuggestedMax = maxDataValue * 1.15; + const allValues = [...filteredConsumption, ...filteredGeneration].filter(v => v !== null) as number[]; + const maxValue = allValues.length > 0 ? Math.max(...allValues) : 0; + const yAxisSuggestedMax = maxValue * 1.15; const data = { labels: filteredLabels, @@ -59,24 +66,44 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro }; const options = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { position: 'top' as const }, + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { position: 'top' as const }, + zoom: { zoom: { - zoom: { - wheel: { enabled: true }, - pinch: { enabled: true }, - mode: 'x' as const, - }, - pan: { enabled: true, mode: 'x' as const }, + wheel: { enabled: true }, + pinch: { enabled: true }, + mode: 'x' as const, }, - tooltip: { enabled: true, mode: 'index' as const, intersect: false }, + pan: { enabled: true, mode: 'x' as const }, }, - scales: { - y: { beginAtZero: true, suggestedMax: yAxisSuggestedMax }, + tooltip: { enabled: true, mode: 'index' as const, intersect: false }, + }, + scales: { + x: { + title: { + display: true, + text: 'Time (HH:MM)', + font: { + weight: 'bold' as const, // ✅ FIX: cast as 'const' + }, + }, }, - }; + y: { + beginAtZero: true, + suggestedMax: yAxisSuggestedMax, + title: { + display: true, + text: 'Power (kW)', + font: { + weight: 'bold' as const, // ✅ FIX: cast as 'const' + }, + }, + }, + }, +} as const; // ✅ Ensures compatibility with chart.js types + const handleResetZoom = () => { chartRef.current?.resetZoom(); @@ -104,10 +131,8 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro }} className="border rounded p-1" > - {labels.map((label, idx) => ( - + {allTimes.map((label, idx) => ( + ))} @@ -121,10 +146,8 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro }} className="border rounded p-1" > - {labels.map((label, idx) => ( - + {allTimes.map((label, idx) => ( + ))} diff --git a/types/SiteData.ts b/types/SiteData.ts index 11bcdce..b424f9e 100644 --- a/types/SiteData.ts +++ b/types/SiteData.ts @@ -1,4 +1,31 @@ // types/siteData.ts + +const generateDailyData = ( + type: 'consumption' | 'generation', + min: number, + max: number +): { time: string; value: number }[] => { + return Array.from({ length: 48 }, (_, i) => { + const hour = Math.floor(i / 2); + const minute = i % 2 === 0 ? '00' : '30'; + const time = `${hour.toString().padStart(2, '0')}:${minute}`; + let value = Math.random() * (max - min) + min; + + if (type === 'generation') { + if (hour < 8 || hour >= 18) { + value = 0; + } else { + const peakHour = 13; + const offset = Math.abs(hour + (i % 2 === 0 ? 0 : 0.5) - peakHour); + value = Math.max(0, max - offset * 5 + Math.random() * 5); + } + } + + return { time, value: parseFloat(value.toFixed(2)) }; + }); +}; + + export type SiteName = 'Site A' | 'Site B' | 'Site C'; export interface SiteDetails { @@ -15,14 +42,11 @@ export interface SiteDetails { solarPower: number; // Power generated by solar (kW) - Real-time realTimePower: number; // Real-time power used (kW) installedPower: number; // Installed capacity (kWp) - - // New properties for KPI calculations (assuming these are base values for the month) - // If consumptionData/generationData are daily, we will sum them up. - // If they were monthly totals, you'd name them appropriately. - // Let's assume these are the *raw data points* for the month. - // monthlyConsumptionkWh?: number; // Optional, if you want to store a pre-calculated total - // monthlyGenerationkWh?: number; // Optional, if you want to store a pre-calculated total + dailyTimeSeriesData: { + consumption: { time: string; value: number }[]; + generation: { time: string; value: number }[]; + }; // For savings calculation: gridImportPrice_RM_per_kWh: number; // Price paid for electricity from the grid solarExportTariff_RM_per_kWh: number; // Price received for excess solar sent to grid (e.g., FiT) @@ -40,56 +64,73 @@ const generateYearlyDataInRange = (min: number, max: number) => .map(() => Math.floor(Math.random() * (max - min + 1)) + min); -export const mockSiteData: Record = { - 'Site A': { - location: 'Petaling Jaya, Selangor', - inverterProvider: 'SolarEdge', - emergencyContact: '+60 12-345 6789', - lastSyncTimestamp: '2025-06-03 15:30:00', - consumptionData: generateYearlyDataInRange(80, 250), - generationData: generateYearlyDataInRange(80, 250), - systemStatus: 'Normal', - temperature: '35°C', - solarPower: 108.4, - realTimePower: 108.4, - installedPower: 174.9, // kWp - gridImportPrice_RM_per_kWh: 0.50, // Example: RM 0.50 per kWh - solarExportTariff_RM_per_kWh: 0.30, // Example: RM 0.30 per kWh - theoreticalMaxGeneration_kWh: 80000, // Example: Theoretical max for 174.9 kWp over a month in Malaysia - connectedDevices : [], +export const mockSiteData: Record = { + 'Site A': { + location: 'Petaling Jaya, Selangor', + inverterProvider: 'SolarEdge', + emergencyContact: '+60 12-345 6789', + lastSyncTimestamp: '2025-06-03 15:30:00', + consumptionData: generateYearlyDataInRange(80, 250), + generationData: generateYearlyDataInRange(80, 250), + systemStatus: 'Normal', + temperature: '35°C', + solarPower: 108.4, + realTimePower: 108.4, + installedPower: 174.9, + gridImportPrice_RM_per_kWh: 0.50, + solarExportTariff_RM_per_kWh: 0.30, + theoreticalMaxGeneration_kWh: 80000, + connectedDevices: [], + dailyTimeSeriesData: { + consumption: generateDailyData('consumption', 80, 250), + generation: generateDailyData('generation', 80, 100), }, - 'Site B': { - location: 'Kuala Lumpur, Wilayah Persekutuan', - inverterProvider: 'Huawei', - emergencyContact: '+60 19-876 5432', - lastSyncTimestamp: '2025-06-02 10:15:00', - consumptionData: generateYearlyDataInRange(200, 450), - generationData: generateYearlyDataInRange(200, 450), - systemStatus: 'Normal', - temperature: '32°C', - solarPower: 95.2, - realTimePower: 95.2, - installedPower: 150.0, - gridImportPrice_RM_per_kWh: 0.52, - solarExportTariff_RM_per_kWh: 0.32, - theoreticalMaxGeneration_kWh: 190000, - connectedDevices: [], + }, + 'Site B': { + location: 'Kuala Lumpur, Wilayah Persekutuan', + inverterProvider: 'Huawei', + emergencyContact: '+60 19-876 5432', + lastSyncTimestamp: '2025-06-02 10:15:00', + consumptionData: generateYearlyDataInRange(200, 450), + generationData: generateYearlyDataInRange(200, 450), + systemStatus: 'Normal', + temperature: '32°C', + solarPower: 95.2, + realTimePower: 95.2, + installedPower: 150.0, + gridImportPrice_RM_per_kWh: 0.52, + solarExportTariff_RM_per_kWh: 0.32, + theoreticalMaxGeneration_kWh: 190000, + connectedDevices: [], + dailyTimeSeriesData: { + consumption: generateDailyData('consumption', 150, 300), + generation: generateDailyData('generation', 0, 120), }, - 'Site C': { - location: 'Johor Bahru, Johor', - inverterProvider: 'Enphase', - emergencyContact: '+60 13-555 1234', - lastSyncTimestamp: '2025-06-03 08:00:00', - consumptionData: generateYearlyDataInRange(400, 550), - generationData: generateYearlyDataInRange(400, 550), - systemStatus: 'Faulty', - temperature: '30°C', - solarPower: 25.0, - realTimePower: 70.0, - installedPower: 120.0, - gridImportPrice_RM_per_kWh: 0.48, - solarExportTariff_RM_per_kWh: 0.28, - theoreticalMaxGeneration_kWh: 180000, // Lower theoretical max due to fault or smaller system - connectedDevices: [], + }, + 'Site C': { + location: 'Johor Bahru, Johor', + inverterProvider: 'Enphase', + emergencyContact: '+60 13-555 1234', + lastSyncTimestamp: '2025-06-03 08:00:00', + consumptionData: generateYearlyDataInRange(400, 550), + generationData: generateYearlyDataInRange(400, 550), + systemStatus: 'Faulty', + temperature: '30°C', + solarPower: 25.0, + realTimePower: 70.0, + installedPower: 120.0, + gridImportPrice_RM_per_kWh: 0.48, + solarExportTariff_RM_per_kWh: 0.28, + theoreticalMaxGeneration_kWh: 180000, + connectedDevices: [], + dailyTimeSeriesData: { + consumption: generateDailyData('consumption', 100, 200), + generation: generateDailyData('generation', 0, 90), }, -}; \ No newline at end of file + }, +};