feature/syasya/testlayout #7
@ -26,3 +26,25 @@ export async function fetchPowerTimeseries(
 | 
				
			|||||||
  console.log(`🔍 API response from /power-timeseries?${params.toString()}:`, json); // ✅ log here
 | 
					  console.log(`🔍 API response from /power-timeseries?${params.toString()}:`, json); // ✅ log here
 | 
				
			||||||
  return json; // <-- This is a single object, not an array
 | 
					  return json; // <-- This is a single object, not an array
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function fetchForecast(
 | 
				
			||||||
 | 
					  lat: number,
 | 
				
			||||||
 | 
					  lon: number,
 | 
				
			||||||
 | 
					  dec: number,
 | 
				
			||||||
 | 
					  az: number,
 | 
				
			||||||
 | 
					  kwp: number
 | 
				
			||||||
 | 
					): Promise<{ time: string; forecast: number }[]> {
 | 
				
			||||||
 | 
					  const query = new URLSearchParams({
 | 
				
			||||||
 | 
					    lat: lat.toString(),
 | 
				
			||||||
 | 
					    lon: lon.toString(),
 | 
				
			||||||
 | 
					    dec: dec.toString(),
 | 
				
			||||||
 | 
					    az: az.toString(),
 | 
				
			||||||
 | 
					    kwp: kwp.toString(),
 | 
				
			||||||
 | 
					  }).toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const res = await fetch(`http://localhost:8000/forecast?${query}`);
 | 
				
			||||||
 | 
					  if (!res.ok) throw new Error("Failed to fetch forecast");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return res.json();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ import {
 | 
				
			|||||||
  startOfYear,
 | 
					  startOfYear,
 | 
				
			||||||
  endOfYear,
 | 
					  endOfYear,
 | 
				
			||||||
} from 'date-fns';
 | 
					} from 'date-fns';
 | 
				
			||||||
import { fetchPowerTimeseries } from '@/app/utils/api';
 | 
					import { fetchPowerTimeseries, fetchForecast } from '@/app/utils/api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ChartJS.register(zoomPlugin);
 | 
					ChartJS.register(zoomPlugin);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -41,7 +41,8 @@ function groupTimeSeries(
 | 
				
			|||||||
        const local = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' }));
 | 
					        const local = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' }));
 | 
				
			||||||
        const hour = local.getHours();
 | 
					        const hour = local.getHours();
 | 
				
			||||||
        const minute = local.getMinutes() < 30 ? '00' : '30';
 | 
					        const minute = local.getMinutes() < 30 ? '00' : '30';
 | 
				
			||||||
        key = `${hour.toString().padStart(2, '0')}:${minute}`;
 | 
					        const adjusted = new Date(local.setMinutes(minute === '00' ? 0 : 30, 0)); // zero seconds
 | 
				
			||||||
 | 
					        key = adjusted.toISOString();  // ✅ full timestamp key
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case 'daily':
 | 
					      case 'daily':
 | 
				
			||||||
        key = date.toLocaleDateString('en-MY', {
 | 
					        key = date.toLocaleDateString('en-MY', {
 | 
				
			||||||
@ -78,6 +79,8 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
				
			|||||||
  const [consumption, setConsumption] = useState<TimeSeriesEntry[]>([]);
 | 
					  const [consumption, setConsumption] = useState<TimeSeriesEntry[]>([]);
 | 
				
			||||||
  const [generation, setGeneration] = useState<TimeSeriesEntry[]>([]);
 | 
					  const [generation, setGeneration] = useState<TimeSeriesEntry[]>([]);
 | 
				
			||||||
  const [selectedDate, setSelectedDate] = useState(new Date());
 | 
					  const [selectedDate, setSelectedDate] = useState(new Date());
 | 
				
			||||||
 | 
					  const [forecast, setForecast] = useState<TimeSeriesEntry[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const now = new Date();
 | 
					    const now = new Date();
 | 
				
			||||||
@ -115,6 +118,20 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
				
			|||||||
        const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd);
 | 
					        const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd);
 | 
				
			||||||
        setConsumption(res.consumption);
 | 
					        setConsumption(res.consumption);
 | 
				
			||||||
        setGeneration(res.generation);
 | 
					        setGeneration(res.generation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ⬇️ ADD THIS here — fetch forecast
 | 
				
			||||||
 | 
					      const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 5.67);
 | 
				
			||||||
 | 
					      const selectedDateStr = selectedDate.toISOString().split('T')[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      setForecast(
 | 
				
			||||||
 | 
					        forecastData
 | 
				
			||||||
 | 
					          .filter(({ time }) => time.startsWith(selectedDateStr))  // ✅ filter only selected date
 | 
				
			||||||
 | 
					          .map(({ time, forecast }) => ({
 | 
				
			||||||
 | 
					            time,
 | 
				
			||||||
 | 
					            value: forecast
 | 
				
			||||||
 | 
					          }))
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Failed to fetch energy timeseries:', error);
 | 
					        console.error('Failed to fetch energy timeseries:', error);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -125,19 +142,25 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const groupedConsumption = groupTimeSeries(consumption, viewMode);
 | 
					  const groupedConsumption = groupTimeSeries(consumption, viewMode);
 | 
				
			||||||
  const groupedGeneration = groupTimeSeries(generation, viewMode);
 | 
					  const groupedGeneration = groupTimeSeries(generation, viewMode);
 | 
				
			||||||
 | 
					  const groupedForecast = groupTimeSeries(forecast, viewMode);
 | 
				
			||||||
 | 
					  const forecastMap = Object.fromEntries(groupedForecast.map(d => [d.time, d.value]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const allTimes = Array.from(new Set([
 | 
					  const allTimes = Array.from(new Set([
 | 
				
			||||||
  ...groupedConsumption.map(d => d.time),
 | 
					  ...groupedConsumption.map(d => d.time),
 | 
				
			||||||
  ...groupedGeneration.map(d => d.time),
 | 
					  ...groupedGeneration.map(d => d.time),
 | 
				
			||||||
 | 
					  ...groupedForecast.map(d => d.time),
 | 
				
			||||||
])).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
 | 
					])).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value]));
 | 
					  const consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value]));
 | 
				
			||||||
  const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value]));
 | 
					  const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [startIndex, setStartIndex] = useState(0);
 | 
					  const [startIndex, setStartIndex] = useState(0);
 | 
				
			||||||
  const [endIndex, setEndIndex] = useState(allTimes.length - 1);
 | 
					  const [endIndex, setEndIndex] = useState(allTimes.length - 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (typeof window !== 'undefined') {
 | 
					    if (typeof window !== 'undefined') {
 | 
				
			||||||
      import('hammerjs');
 | 
					      import('hammerjs');
 | 
				
			||||||
@ -151,6 +174,13 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const formatLabel = (key: string) => {
 | 
					  const formatLabel = (key: string) => {
 | 
				
			||||||
    switch (viewMode) {
 | 
					    switch (viewMode) {
 | 
				
			||||||
 | 
					      case 'day':
 | 
				
			||||||
 | 
					        return new Date(key).toLocaleTimeString('en-GB', {
 | 
				
			||||||
 | 
					          hour: '2-digit',
 | 
				
			||||||
 | 
					          minute: '2-digit',
 | 
				
			||||||
 | 
					          hour12: false,
 | 
				
			||||||
 | 
					          timeZone: 'Asia/Kuala_Lumpur',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      case 'monthly':
 | 
					      case 'monthly':
 | 
				
			||||||
        return new Date(`${key}-01`).toLocaleString('en-GB', { month: 'short', year: 'numeric' });
 | 
					        return new Date(`${key}-01`).toLocaleString('en-GB', { month: 'short', year: 'numeric' });
 | 
				
			||||||
      case 'weekly':
 | 
					      case 'weekly':
 | 
				
			||||||
@ -161,8 +191,10 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const filteredLabels = allTimes.slice(startIndex, endIndex + 1);
 | 
					  const filteredLabels = allTimes.slice(startIndex, endIndex + 1);
 | 
				
			||||||
  const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? null);
 | 
					  const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? 0);
 | 
				
			||||||
  const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? null);
 | 
					  const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? 0);
 | 
				
			||||||
 | 
					  const filteredForecast = filteredLabels.map(t => forecastMap[t] ?? null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const allValues = [...filteredConsumption, ...filteredGeneration].filter(v => v !== null) as number[];
 | 
					  const allValues = [...filteredConsumption, ...filteredGeneration].filter(v => v !== null) as number[];
 | 
				
			||||||
  const maxValue = allValues.length > 0 ? Math.max(...allValues) : 0;
 | 
					  const maxValue = allValues.length > 0 ? Math.max(...allValues) : 0;
 | 
				
			||||||
@ -177,6 +209,7 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
				
			|||||||
        borderColor: '#8884d8',
 | 
					        borderColor: '#8884d8',
 | 
				
			||||||
        tension: 0.4,
 | 
					        tension: 0.4,
 | 
				
			||||||
        fill: false,
 | 
					        fill: false,
 | 
				
			||||||
 | 
					        spanGaps: true,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        label: 'Generation',
 | 
					        label: 'Generation',
 | 
				
			||||||
@ -184,7 +217,17 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
				
			|||||||
        borderColor: '#82ca9d',
 | 
					        borderColor: '#82ca9d',
 | 
				
			||||||
        tension: 0.4,
 | 
					        tension: 0.4,
 | 
				
			||||||
        fill: false,
 | 
					        fill: false,
 | 
				
			||||||
 | 
					        spanGaps: true,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					      label: 'Forecasted Solar',
 | 
				
			||||||
 | 
					      data: filteredForecast,
 | 
				
			||||||
 | 
					      borderColor: '#ffa500', // orange
 | 
				
			||||||
 | 
					      tension: 0.4,
 | 
				
			||||||
 | 
					      borderDash: [5, 5], // dashed line to distinguish forecast
 | 
				
			||||||
 | 
					      fill: false,
 | 
				
			||||||
 | 
					      spanGaps: true,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user