linechart extract db update

This commit is contained in:
Syasya 2025-07-30 17:00:43 +08:00
parent c6dd83f4db
commit efdad6dfcc
4 changed files with 243 additions and 100 deletions

View File

@ -9,6 +9,8 @@ import DashboardLayout from './dashlayout';
import html2canvas from 'html2canvas'; import html2canvas from 'html2canvas';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { fetchPowerTimeseries } from '@/app/utils/api';
const EnergyLineChart = dynamic(() => import('@/components/dashboards/EnergyLineChart'), { const EnergyLineChart = dynamic(() => import('@/components/dashboards/EnergyLineChart'), {
ssr: false, ssr: false,
}); });
@ -45,6 +47,53 @@ const AdminDashboard = () => {
} }
}, [siteParam, selectedSite]); }, [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<SiteName, string> = {
'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 // Update query string when site is changed manually
const handleSiteChange = (newSite: SiteName) => { const handleSiteChange = (newSite: SiteName) => {
setSelectedSite(newSite); setSelectedSite(newSite);
@ -142,9 +191,11 @@ const AdminDashboard = () => {
<div className="grid md:grid-cols-2 gap-6 lg:flex-col justify-center"> <div className="grid md:grid-cols-2 gap-6 lg:flex-col justify-center">
<div ref={energyChartRef} className="pb-5"> <div ref={energyChartRef} className="pb-5">
<EnergyLineChart <EnergyLineChart
consumptionData={currentSiteDetails.consumptionData} consumption={timeSeriesData.consumption}
generationData={currentSiteDetails.generationData} generation={timeSeriesData.generation}
/> />
</div> </div>
<div ref={monthlyChartRef} className="pb-5"> <div ref={monthlyChartRef} className="pb-5">
<MonthlyBarChart siteData={currentSiteDetails} /> <MonthlyBarChart siteData={currentSiteDetails} />

28
app/utils/api.ts Normal file
View File

@ -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<TimeSeriesResponse> { // <-- 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
}

View File

@ -5,23 +5,31 @@ import zoomPlugin from 'chartjs-plugin-zoom';
ChartJS.register(zoomPlugin); ChartJS.register(zoomPlugin);
interface EnergyLineChartProps { interface TimeSeriesEntry {
consumptionData: number[]; time: string;
generationData: number[]; value: number;
} }
const labels = [ interface EnergyLineChartProps {
'08:00', '08:30', '09:00', '09:30', '10:00', consumption: TimeSeriesEntry[];
'10:30', '11:00', '11:30', '12:00', '12:30', generation: TimeSeriesEntry[];
'13:00', '13:30', '14:00', '14:30', '15:00', }
'15:30', '16:00', '16:30', '17:00',
];
const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartProps) => { const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
const chartRef = useRef<any>(null); const chartRef = useRef<any>(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 [startIndex, setStartIndex] = useState(0);
const [endIndex, setEndIndex] = useState(labels.length - 1); const [endIndex, setEndIndex] = useState(allTimes.length - 1);
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@ -29,14 +37,13 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro
} }
}, []); }, []);
// Filter data arrays based on selected range const filteredLabels = allTimes.slice(startIndex, endIndex + 1);
const filteredConsumption = consumptionData.slice(startIndex, endIndex + 1); const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? null);
const filteredGeneration = generationData.slice(startIndex, endIndex + 1); const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? null);
const filteredLabels = labels.slice(startIndex, endIndex + 1);
const allDataPoints = [...filteredConsumption, ...filteredGeneration]; const allValues = [...filteredConsumption, ...filteredGeneration].filter(v => v !== null) as number[];
const maxDataValue = allDataPoints.length > 0 ? Math.max(...allDataPoints) : 0; const maxValue = allValues.length > 0 ? Math.max(...allValues) : 0;
const yAxisSuggestedMax = maxDataValue * 1.15; const yAxisSuggestedMax = maxValue * 1.15;
const data = { const data = {
labels: filteredLabels, labels: filteredLabels,
@ -59,24 +66,44 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro
}; };
const options = { const options = {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { position: 'top' as const }, legend: { position: 'top' as const },
zoom: {
zoom: { zoom: {
zoom: { wheel: { enabled: true },
wheel: { enabled: true }, pinch: { enabled: true },
pinch: { enabled: true }, mode: 'x' as const,
mode: 'x' as const,
},
pan: { enabled: true, mode: 'x' as const },
}, },
tooltip: { enabled: true, mode: 'index' as const, intersect: false }, pan: { enabled: true, mode: 'x' as const },
}, },
scales: { tooltip: { enabled: true, mode: 'index' as const, intersect: false },
y: { beginAtZero: true, suggestedMax: yAxisSuggestedMax }, },
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 = () => { const handleResetZoom = () => {
chartRef.current?.resetZoom(); chartRef.current?.resetZoom();
@ -104,10 +131,8 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro
}} }}
className="border rounded p-1" className="border rounded p-1"
> >
{labels.map((label, idx) => ( {allTimes.map((label, idx) => (
<option key={idx} value={idx}> <option key={idx} value={idx}>{label}</option>
{label}
</option>
))} ))}
</select> </select>
</label> </label>
@ -121,10 +146,8 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro
}} }}
className="border rounded p-1" className="border rounded p-1"
> >
{labels.map((label, idx) => ( {allTimes.map((label, idx) => (
<option key={idx} value={idx}> <option key={idx} value={idx}>{label}</option>
{label}
</option>
))} ))}
</select> </select>
</label> </label>

View File

@ -1,4 +1,31 @@
// types/siteData.ts // 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 type SiteName = 'Site A' | 'Site B' | 'Site C';
export interface SiteDetails { export interface SiteDetails {
@ -16,13 +43,10 @@ export interface SiteDetails {
realTimePower: number; // Real-time power used (kW) realTimePower: number; // Real-time power used (kW)
installedPower: number; // Installed capacity (kWp) installedPower: number; // Installed capacity (kWp)
// New properties for KPI calculations (assuming these are base values for the month) dailyTimeSeriesData: {
// If consumptionData/generationData are daily, we will sum them up. consumption: { time: string; value: number }[];
// If they were monthly totals, you'd name them appropriately. generation: { time: string; value: number }[];
// 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
// For savings calculation: // For savings calculation:
gridImportPrice_RM_per_kWh: number; // Price paid for electricity from the grid 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) 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); .map(() => Math.floor(Math.random() * (max - min + 1)) + min);
export const mockSiteData: Record<SiteName, SiteDetails> = { export const mockSiteData: Record<SiteName, SiteDetails & {
'Site A': { dailyTimeSeriesData: {
location: 'Petaling Jaya, Selangor', consumption: { time: string; value: number }[];
inverterProvider: 'SolarEdge', generation: { time: string; value: number }[];
emergencyContact: '+60 12-345 6789', };
lastSyncTimestamp: '2025-06-03 15:30:00', }> = {
consumptionData: generateYearlyDataInRange(80, 250), 'Site A': {
generationData: generateYearlyDataInRange(80, 250), location: 'Petaling Jaya, Selangor',
systemStatus: 'Normal', inverterProvider: 'SolarEdge',
temperature: '35°C', emergencyContact: '+60 12-345 6789',
solarPower: 108.4, lastSyncTimestamp: '2025-06-03 15:30:00',
realTimePower: 108.4, consumptionData: generateYearlyDataInRange(80, 250),
installedPower: 174.9, // kWp generationData: generateYearlyDataInRange(80, 250),
gridImportPrice_RM_per_kWh: 0.50, // Example: RM 0.50 per kWh systemStatus: 'Normal',
solarExportTariff_RM_per_kWh: 0.30, // Example: RM 0.30 per kWh temperature: '35°C',
theoreticalMaxGeneration_kWh: 80000, // Example: Theoretical max for 174.9 kWp over a month in Malaysia solarPower: 108.4,
connectedDevices : [], 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', 'Site B': {
inverterProvider: 'Huawei', location: 'Kuala Lumpur, Wilayah Persekutuan',
emergencyContact: '+60 19-876 5432', inverterProvider: 'Huawei',
lastSyncTimestamp: '2025-06-02 10:15:00', emergencyContact: '+60 19-876 5432',
consumptionData: generateYearlyDataInRange(200, 450), lastSyncTimestamp: '2025-06-02 10:15:00',
generationData: generateYearlyDataInRange(200, 450), consumptionData: generateYearlyDataInRange(200, 450),
systemStatus: 'Normal', generationData: generateYearlyDataInRange(200, 450),
temperature: '32°C', systemStatus: 'Normal',
solarPower: 95.2, temperature: '32°C',
realTimePower: 95.2, solarPower: 95.2,
installedPower: 150.0, realTimePower: 95.2,
gridImportPrice_RM_per_kWh: 0.52, installedPower: 150.0,
solarExportTariff_RM_per_kWh: 0.32, gridImportPrice_RM_per_kWh: 0.52,
theoreticalMaxGeneration_kWh: 190000, solarExportTariff_RM_per_kWh: 0.32,
connectedDevices: [], theoreticalMaxGeneration_kWh: 190000,
connectedDevices: [],
dailyTimeSeriesData: {
consumption: generateDailyData('consumption', 150, 300),
generation: generateDailyData('generation', 0, 120),
}, },
'Site C': { },
location: 'Johor Bahru, Johor', 'Site C': {
inverterProvider: 'Enphase', location: 'Johor Bahru, Johor',
emergencyContact: '+60 13-555 1234', inverterProvider: 'Enphase',
lastSyncTimestamp: '2025-06-03 08:00:00', emergencyContact: '+60 13-555 1234',
consumptionData: generateYearlyDataInRange(400, 550), lastSyncTimestamp: '2025-06-03 08:00:00',
generationData: generateYearlyDataInRange(400, 550), consumptionData: generateYearlyDataInRange(400, 550),
systemStatus: 'Faulty', generationData: generateYearlyDataInRange(400, 550),
temperature: '30°C', systemStatus: 'Faulty',
solarPower: 25.0, temperature: '30°C',
realTimePower: 70.0, solarPower: 25.0,
installedPower: 120.0, realTimePower: 70.0,
gridImportPrice_RM_per_kWh: 0.48, installedPower: 120.0,
solarExportTariff_RM_per_kWh: 0.28, gridImportPrice_RM_per_kWh: 0.48,
theoreticalMaxGeneration_kWh: 180000, // Lower theoretical max due to fault or smaller system solarExportTariff_RM_per_kWh: 0.28,
connectedDevices: [], theoreticalMaxGeneration_kWh: 180000,
connectedDevices: [],
dailyTimeSeriesData: {
consumption: generateDailyData('consumption', 100, 200),
generation: generateDailyData('generation', 0, 90),
}, },
},
}; };