linechart extract db update
This commit is contained in:
parent
c6dd83f4db
commit
efdad6dfcc
@ -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
28
app/utils/api.ts
Normal 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
|
||||||
|
}
|
@ -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,
|
||||||
@ -74,9 +81,29 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro
|
|||||||
tooltip: { enabled: true, mode: 'index' as const, intersect: false },
|
tooltip: { enabled: true, mode: 'index' as const, intersect: false },
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
y: { beginAtZero: true, suggestedMax: yAxisSuggestedMax },
|
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>
|
||||||
|
@ -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,7 +64,12 @@ 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 & {
|
||||||
|
dailyTimeSeriesData: {
|
||||||
|
consumption: { time: string; value: number }[];
|
||||||
|
generation: { time: string; value: number }[];
|
||||||
|
};
|
||||||
|
}> = {
|
||||||
'Site A': {
|
'Site A': {
|
||||||
location: 'Petaling Jaya, Selangor',
|
location: 'Petaling Jaya, Selangor',
|
||||||
inverterProvider: 'SolarEdge',
|
inverterProvider: 'SolarEdge',
|
||||||
@ -52,11 +81,15 @@ export const mockSiteData: Record<SiteName, SiteDetails> = {
|
|||||||
temperature: '35°C',
|
temperature: '35°C',
|
||||||
solarPower: 108.4,
|
solarPower: 108.4,
|
||||||
realTimePower: 108.4,
|
realTimePower: 108.4,
|
||||||
installedPower: 174.9, // kWp
|
installedPower: 174.9,
|
||||||
gridImportPrice_RM_per_kWh: 0.50, // Example: RM 0.50 per kWh
|
gridImportPrice_RM_per_kWh: 0.50,
|
||||||
solarExportTariff_RM_per_kWh: 0.30, // Example: RM 0.30 per kWh
|
solarExportTariff_RM_per_kWh: 0.30,
|
||||||
theoreticalMaxGeneration_kWh: 80000, // Example: Theoretical max for 174.9 kWp over a month in Malaysia
|
theoreticalMaxGeneration_kWh: 80000,
|
||||||
connectedDevices: [],
|
connectedDevices: [],
|
||||||
|
dailyTimeSeriesData: {
|
||||||
|
consumption: generateDailyData('consumption', 80, 250),
|
||||||
|
generation: generateDailyData('generation', 80, 100),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'Site B': {
|
'Site B': {
|
||||||
location: 'Kuala Lumpur, Wilayah Persekutuan',
|
location: 'Kuala Lumpur, Wilayah Persekutuan',
|
||||||
@ -74,6 +107,10 @@ export const mockSiteData: Record<SiteName, SiteDetails> = {
|
|||||||
solarExportTariff_RM_per_kWh: 0.32,
|
solarExportTariff_RM_per_kWh: 0.32,
|
||||||
theoreticalMaxGeneration_kWh: 190000,
|
theoreticalMaxGeneration_kWh: 190000,
|
||||||
connectedDevices: [],
|
connectedDevices: [],
|
||||||
|
dailyTimeSeriesData: {
|
||||||
|
consumption: generateDailyData('consumption', 150, 300),
|
||||||
|
generation: generateDailyData('generation', 0, 120),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'Site C': {
|
'Site C': {
|
||||||
location: 'Johor Bahru, Johor',
|
location: 'Johor Bahru, Johor',
|
||||||
@ -89,7 +126,11 @@ export const mockSiteData: Record<SiteName, SiteDetails> = {
|
|||||||
installedPower: 120.0,
|
installedPower: 120.0,
|
||||||
gridImportPrice_RM_per_kWh: 0.48,
|
gridImportPrice_RM_per_kWh: 0.48,
|
||||||
solarExportTariff_RM_per_kWh: 0.28,
|
solarExportTariff_RM_per_kWh: 0.28,
|
||||||
theoreticalMaxGeneration_kWh: 180000, // Lower theoretical max due to fault or smaller system
|
theoreticalMaxGeneration_kWh: 180000,
|
||||||
connectedDevices: [],
|
connectedDevices: [],
|
||||||
|
dailyTimeSeriesData: {
|
||||||
|
consumption: generateDailyData('consumption', 100, 200),
|
||||||
|
generation: generateDailyData('generation', 0, 90),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
Loading…
x
Reference in New Issue
Block a user