feature/syasya/testlayout #6
@ -19,6 +19,7 @@ import DatePicker from 'react-datepicker';
 | 
			
		||||
import 'react-datepicker/dist/react-datepicker.css';
 | 
			
		||||
import './datepicker-dark.css'; // custom dark mode styles
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ChartJS.register(zoomPlugin);
 | 
			
		||||
 | 
			
		||||
interface TimeSeriesEntry {
 | 
			
		||||
@ -30,9 +31,48 @@ interface EnergyLineChartProps {
 | 
			
		||||
  siteId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function powerSeriesToEnergySeries(
 | 
			
		||||
  data: TimeSeriesEntry[],
 | 
			
		||||
  guessMinutes = 30
 | 
			
		||||
): TimeSeriesEntry[] {
 | 
			
		||||
  if (!data?.length) return [];
 | 
			
		||||
 | 
			
		||||
  // Ensure ascending by time
 | 
			
		||||
  const sorted = [...data].sort(
 | 
			
		||||
    (a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const out: TimeSeriesEntry[] = [];
 | 
			
		||||
  let lastDeltaMs: number | null = null;
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < sorted.length; i++) {
 | 
			
		||||
    const t0 = new Date(sorted[i].time).getTime();
 | 
			
		||||
    const p0 = sorted[i].value; // kW
 | 
			
		||||
 | 
			
		||||
    let deltaMs: number;
 | 
			
		||||
    if (i < sorted.length - 1) {
 | 
			
		||||
      const t1 = new Date(sorted[i + 1].time).getTime();
 | 
			
		||||
      deltaMs = Math.max(0, t1 - t0);
 | 
			
		||||
      if (deltaMs > 0) lastDeltaMs = deltaMs;
 | 
			
		||||
    } else {
 | 
			
		||||
      // For the last point, assume previous cadence or a guess
 | 
			
		||||
      deltaMs = lastDeltaMs ?? guessMinutes * 60 * 1000;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const hours = deltaMs / (1000 * 60 * 60);
 | 
			
		||||
    const kwh = p0 * hours; // kW * h = kWh
 | 
			
		||||
 | 
			
		||||
    out.push({ time: sorted[i].time, value: kwh });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function groupTimeSeries(
 | 
			
		||||
  data: TimeSeriesEntry[],
 | 
			
		||||
  mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly'
 | 
			
		||||
  mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly',
 | 
			
		||||
  agg: 'mean' | 'max' | 'sum' = 'mean'
 | 
			
		||||
): TimeSeriesEntry[] {
 | 
			
		||||
  const groupMap = new Map<string, number[]>();
 | 
			
		||||
 | 
			
		||||
@ -41,19 +81,22 @@ function groupTimeSeries(
 | 
			
		||||
    let key = '';
 | 
			
		||||
 | 
			
		||||
    switch (mode) {
 | 
			
		||||
      case 'day':
 | 
			
		||||
        const local = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' }));
 | 
			
		||||
        const hour = local.getHours();
 | 
			
		||||
        const minute = local.getMinutes() < 30 ? '00' : '30';
 | 
			
		||||
        const adjusted = new Date(local.setMinutes(minute === '00' ? 0 : 30, 0)); // zero seconds
 | 
			
		||||
        key = adjusted.toISOString();  // ✅ full timestamp key
 | 
			
		||||
      case 'day': {
 | 
			
		||||
        const local = new Date(
 | 
			
		||||
          date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' })
 | 
			
		||||
        );
 | 
			
		||||
        const minute = local.getMinutes() < 30 ? 0 : 30;
 | 
			
		||||
        local.setMinutes(minute, 0, 0);
 | 
			
		||||
        key = local.toISOString();
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case 'daily':
 | 
			
		||||
        key = date.toLocaleDateString('en-MY', {
 | 
			
		||||
          timeZone: 'Asia/Kuala_Lumpur',
 | 
			
		||||
          weekday: 'short',
 | 
			
		||||
          day: '2-digit',
 | 
			
		||||
          month: 'short',
 | 
			
		||||
          year: 'numeric',
 | 
			
		||||
        });
 | 
			
		||||
        break;
 | 
			
		||||
      case 'weekly':
 | 
			
		||||
@ -71,11 +114,18 @@ function groupTimeSeries(
 | 
			
		||||
    groupMap.get(key)!.push(entry.value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return Array.from(groupMap.entries()).map(([time, values]) => ({
 | 
			
		||||
    time,
 | 
			
		||||
    value: values.reduce((sum, v) => sum + v, 0),
 | 
			
		||||
  }));
 | 
			
		||||
  return Array.from(groupMap.entries()).map(([time, values]) => {
 | 
			
		||||
    if (agg === 'sum') {
 | 
			
		||||
      const sum = values.reduce((a, b) => a + b, 0);
 | 
			
		||||
      return { time, value: sum };
 | 
			
		||||
    }
 | 
			
		||||
    const mean = values.reduce((a, b) => a + b, 0) / values.length;
 | 
			
		||||
    const max = values.reduce((a, b) => (b > a ? b : a), -Infinity);
 | 
			
		||||
    return { time, value: agg === 'max' ? max : mean };
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
			
		||||
  const chartRef = useRef<any>(null);
 | 
			
		||||
@ -85,6 +135,94 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
			
		||||
  const [selectedDate, setSelectedDate] = useState(new Date());
 | 
			
		||||
  const [forecast, setForecast] = useState<TimeSeriesEntry[]>([]);
 | 
			
		||||
 | 
			
		||||
  const LIVE_REFRESH_MS = 300000;       // 5min when viewing a single day
 | 
			
		||||
  const SLOW_REFRESH_MS = 600000;      // 10min for weekly/monthly/yearly
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const fetchAndSet = React.useCallback(async () => {
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    let start: Date;
 | 
			
		||||
    let end: Date;
 | 
			
		||||
 | 
			
		||||
    switch (viewMode) {
 | 
			
		||||
      case 'day':
 | 
			
		||||
        start = startOfDay(selectedDate);
 | 
			
		||||
        end = endOfDay(selectedDate);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'daily':
 | 
			
		||||
        start = startOfWeek(now, { weekStartsOn: 1 });
 | 
			
		||||
        end = endOfWeek(now, { weekStartsOn: 1 });
 | 
			
		||||
        break;
 | 
			
		||||
      case 'weekly':
 | 
			
		||||
        start = startOfMonth(now);
 | 
			
		||||
        end = endOfMonth(now);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'monthly':
 | 
			
		||||
        start = startOfYear(now);
 | 
			
		||||
        end = endOfYear(now);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'yearly':
 | 
			
		||||
        start = new Date('2020-01-01');
 | 
			
		||||
        end = now;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const isoStart = start.toISOString();
 | 
			
		||||
    const isoEnd = end.toISOString();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd);
 | 
			
		||||
      setConsumption(res.consumption);
 | 
			
		||||
      setGeneration(res.generation);
 | 
			
		||||
 | 
			
		||||
      // Forecast only needs updating for the selected day
 | 
			
		||||
      const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 25.67);
 | 
			
		||||
      const selectedDateStr = selectedDate.toISOString().split('T')[0];
 | 
			
		||||
      setForecast(
 | 
			
		||||
        forecastData
 | 
			
		||||
          .filter(({ time }: any) => time.startsWith(selectedDateStr))
 | 
			
		||||
          .map(({ time, forecast }: any) => ({ time, value: forecast }))
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Failed to fetch energy timeseries:', error);
 | 
			
		||||
    }
 | 
			
		||||
  }, [siteId, viewMode, selectedDate]);
 | 
			
		||||
 | 
			
		||||
  // 3) Auto-refresh effect: initial load + interval (pauses when tab hidden)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let timer: number | undefined;
 | 
			
		||||
 | 
			
		||||
    const tick = async () => {
 | 
			
		||||
      // Avoid wasted calls when the tab is in the background
 | 
			
		||||
      if (!document.hidden) {
 | 
			
		||||
        await fetchAndSet();
 | 
			
		||||
      }
 | 
			
		||||
      const ms = viewMode === 'day' ? LIVE_REFRESH_MS : SLOW_REFRESH_MS;
 | 
			
		||||
      timer = window.setTimeout(tick, ms);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // initial load
 | 
			
		||||
    fetchAndSet();
 | 
			
		||||
 | 
			
		||||
    // schedule next cycles
 | 
			
		||||
    timer = window.setTimeout(tick, viewMode === 'day' ? LIVE_REFRESH_MS : SLOW_REFRESH_MS);
 | 
			
		||||
 | 
			
		||||
    const onVis = () => {
 | 
			
		||||
      if (!document.hidden) {
 | 
			
		||||
        // kick immediately when user returns
 | 
			
		||||
        clearTimeout(timer);
 | 
			
		||||
        tick();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    document.addEventListener('visibilitychange', onVis);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      clearTimeout(timer);
 | 
			
		||||
      document.removeEventListener('visibilitychange', onVis);
 | 
			
		||||
    };
 | 
			
		||||
  }, [fetchAndSet, viewMode]);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  function useIsDarkMode() {
 | 
			
		||||
  const [isDark, setIsDark] = useState(() =>
 | 
			
		||||
    typeof document !== 'undefined'
 | 
			
		||||
@ -140,7 +278,7 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
			
		||||
        setGeneration(res.generation);
 | 
			
		||||
 | 
			
		||||
        // ⬇️ ADD THIS here — fetch forecast
 | 
			
		||||
      const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 20.67);
 | 
			
		||||
      const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 30.67);
 | 
			
		||||
      const selectedDateStr = selectedDate.toISOString().split('T')[0];
 | 
			
		||||
 | 
			
		||||
      setForecast(
 | 
			
		||||
@ -160,9 +298,40 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
			
		||||
    fetchData();
 | 
			
		||||
  }, [siteId, viewMode, selectedDate]);
 | 
			
		||||
 | 
			
		||||
  const groupedConsumption = groupTimeSeries(consumption, viewMode);
 | 
			
		||||
  const groupedGeneration = groupTimeSeries(generation, viewMode);
 | 
			
		||||
  const groupedForecast = groupTimeSeries(forecast, viewMode);
 | 
			
		||||
  const isEnergyView = viewMode !== 'day';
 | 
			
		||||
 | 
			
		||||
// Convert to energy series for aggregated views
 | 
			
		||||
const consumptionForGrouping = isEnergyView
 | 
			
		||||
  ? powerSeriesToEnergySeries(consumption, 30)
 | 
			
		||||
  : consumption;
 | 
			
		||||
 | 
			
		||||
const generationForGrouping = isEnergyView
 | 
			
		||||
  ? powerSeriesToEnergySeries(generation, 30)
 | 
			
		||||
  : generation;
 | 
			
		||||
 | 
			
		||||
const forecastForGrouping = isEnergyView
 | 
			
		||||
  ? powerSeriesToEnergySeries(forecast, 60) // if forecast is hourly, guess 60
 | 
			
		||||
  : forecast;
 | 
			
		||||
 | 
			
		||||
// Group: sum for energy views, mean for day view
 | 
			
		||||
const groupedConsumption = groupTimeSeries(
 | 
			
		||||
  consumptionForGrouping,
 | 
			
		||||
  viewMode,
 | 
			
		||||
  isEnergyView ? 'sum' : 'mean'
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const groupedGeneration = groupTimeSeries(
 | 
			
		||||
  generationForGrouping,
 | 
			
		||||
  viewMode,
 | 
			
		||||
  isEnergyView ? 'sum' : 'mean'
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const groupedForecast = groupTimeSeries(
 | 
			
		||||
  forecastForGrouping,
 | 
			
		||||
  viewMode,
 | 
			
		||||
  isEnergyView ? 'sum' : 'mean'
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
  const forecastMap = Object.fromEntries(groupedForecast.map(d => [d.time, d.value]));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -238,6 +407,8 @@ function areaGradient(ctx: any, hex: string, alphaTop = 0.22, alphaBottom = 0.02
 | 
			
		||||
const consumptionColor = isDark ? '#B80F0A' : '#EF4444'; // Example: Brighter red for dark mode
 | 
			
		||||
const generationColor = isDark ? '#48A860' : '#22C55E'; // Example: Brighter green for dark mode
 | 
			
		||||
const forecastColor = '#fcd913'; // A golden yellow that works well in both modes
 | 
			
		||||
const yUnit = isEnergyView ? 'kWh' : 'kW';
 | 
			
		||||
const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
 | 
			
		||||
 | 
			
		||||
  const data = {
 | 
			
		||||
    labels: filteredLabels.map(formatLabel),
 | 
			
		||||
@ -300,6 +471,13 @@ const forecastColor = '#fcd913'; // A golden yellow that works well in both mode
 | 
			
		||||
      bodyColor: axisColor,
 | 
			
		||||
      borderColor: isDark ? '#444' : '#ccc',
 | 
			
		||||
      borderWidth: 1,
 | 
			
		||||
      callbacks: {
 | 
			
		||||
      label: (ctx: any) => {
 | 
			
		||||
        const dsLabel = ctx.dataset.label || '';
 | 
			
		||||
        const val = ctx.parsed.y;
 | 
			
		||||
        return `${dsLabel}: ${val?.toFixed(2)} ${yUnit}`;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    },
 | 
			
		||||
    },
 | 
			
		||||
    scales: {
 | 
			
		||||
@ -326,12 +504,7 @@ const forecastColor = '#fcd913'; // A golden yellow that works well in both mode
 | 
			
		||||
      y: {
 | 
			
		||||
        beginAtZero: true,
 | 
			
		||||
        suggestedMax: yAxisSuggestedMax,
 | 
			
		||||
        title: {
 | 
			
		||||
          display: true,
 | 
			
		||||
          text: 'Power (kW)',
 | 
			
		||||
          color: axisColor,
 | 
			
		||||
          font: { weight: 'normal' as const },
 | 
			
		||||
        },
 | 
			
		||||
        title: { display: true, text: yTitle, color: axisColor, font: { weight: 'normal' as const } },
 | 
			
		||||
        ticks: {
 | 
			
		||||
        color: axisColor,
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import React, { useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
  BarChart,
 | 
			
		||||
  Bar,
 | 
			
		||||
@ -9,45 +9,24 @@ import {
 | 
			
		||||
  Legend,
 | 
			
		||||
} from 'recharts';
 | 
			
		||||
import { format } from 'date-fns';
 | 
			
		||||
import { fetchPowerTimeseries } from '@/app/utils/api';
 | 
			
		||||
 | 
			
		||||
import { fetchMonthlyKpi, type MonthlyKPI } from '@/app/utils/api';
 | 
			
		||||
 | 
			
		||||
interface MonthlyBarChartProps {
 | 
			
		||||
  siteId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TimeSeriesEntry {
 | 
			
		||||
  time: string;
 | 
			
		||||
  value: number;
 | 
			
		||||
const getLastNMonthKeys = (n: number): string[] => {
 | 
			
		||||
  const out: string[] = [];
 | 
			
		||||
  const now = new Date();
 | 
			
		||||
  // include current month, go back n-1 months
 | 
			
		||||
  for (let i = 0; i < n; i++) {
 | 
			
		||||
    const d = new Date(now.getFullYear(), now.getMonth() - (n - 1 - i), 1);
 | 
			
		||||
    const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; // YYYY-MM
 | 
			
		||||
    out.push(key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
const groupTimeSeries = (
 | 
			
		||||
  data: TimeSeriesEntry[],
 | 
			
		||||
  mode: 'monthly'
 | 
			
		||||
): TimeSeriesEntry[] => {
 | 
			
		||||
  const groupMap = new Map<string, number[]>();
 | 
			
		||||
 | 
			
		||||
  for (const entry of data) {
 | 
			
		||||
    const date = new Date(entry.time);
 | 
			
		||||
    const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
 | 
			
		||||
    if (!groupMap.has(key)) groupMap.set(key, []);
 | 
			
		||||
    groupMap.get(key)!.push(entry.value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return Array.from(groupMap.entries()).map(([time, values]) => ({
 | 
			
		||||
    time,
 | 
			
		||||
    value: values.reduce((sum, v) => sum + v, 0),
 | 
			
		||||
  }));
 | 
			
		||||
  return out;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const MonthlyBarChart: React.FC<MonthlyBarChartProps> = ({ siteId }) => {
 | 
			
		||||
  const [chartData, setChartData] = useState<
 | 
			
		||||
    { month: string; consumption: number; generation: number }[]
 | 
			
		||||
  >([]);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
 | 
			
		||||
function useIsDarkMode() {
 | 
			
		||||
  const [isDark, setIsDark] = useState(() =>
 | 
			
		||||
    typeof document !== 'undefined'
 | 
			
		||||
@ -58,71 +37,77 @@ const MonthlyBarChart: React.FC<MonthlyBarChartProps> = ({ siteId }) => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const check = () => setIsDark(document.body.classList.contains('dark'));
 | 
			
		||||
    check();
 | 
			
		||||
 | 
			
		||||
    // Listen for class changes on <body>
 | 
			
		||||
    const observer = new MutationObserver(check);
 | 
			
		||||
    observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
 | 
			
		||||
 | 
			
		||||
    return () => observer.disconnect();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return isDark;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const isDark = useIsDarkMode();
 | 
			
		||||
const MonthlyBarChart: React.FC<MonthlyBarChartProps> = ({ siteId }) => {
 | 
			
		||||
  const [chartData, setChartData] = useState<
 | 
			
		||||
    { month: string; consumption: number; generation: number }[]
 | 
			
		||||
  >([]);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
 | 
			
		||||
  const isDark = useIsDarkMode();
 | 
			
		||||
  const consumptionColor = isDark ? '#ba8e23' : '#003049';
 | 
			
		||||
  const generationColor = isDark ? '#fcd913' : '#669bbc';
 | 
			
		||||
 | 
			
		||||
  const monthKeys = useMemo(() => getLastNMonthKeys(6), []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!siteId) return;
 | 
			
		||||
 | 
			
		||||
    const fetchMonthlyData = async () => {
 | 
			
		||||
    const load = async () => {
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
      const start = '2025-01-01T00:00:00+08:00';
 | 
			
		||||
      const end = '2025-12-31T23:59:59+08:00';
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const res = await fetchPowerTimeseries(siteId, start, end);
 | 
			
		||||
        // Fetch all 6 months in parallel
 | 
			
		||||
        const results: MonthlyKPI[] = await Promise.all(
 | 
			
		||||
          monthKeys.map((month) =>
 | 
			
		||||
            fetchMonthlyKpi({
 | 
			
		||||
              site: siteId,
 | 
			
		||||
              month,
 | 
			
		||||
              // consumption_topic: '...', // optional if your API needs it
 | 
			
		||||
              // generation_topic: '...',  // optional if your API needs it
 | 
			
		||||
            }).catch((e) => {
 | 
			
		||||
              // normalize failures to an error-shaped record so the chart can still render other months
 | 
			
		||||
              return {
 | 
			
		||||
                site: siteId,
 | 
			
		||||
                month,
 | 
			
		||||
                yield_kwh: null,
 | 
			
		||||
                consumption_kwh: null,
 | 
			
		||||
                grid_draw_kwh: null,
 | 
			
		||||
                efficiency: null,
 | 
			
		||||
                peak_demand_kw: null,
 | 
			
		||||
                avg_power_factor: null,
 | 
			
		||||
                load_factor: null,
 | 
			
		||||
                error: String(e),
 | 
			
		||||
              } as MonthlyKPI;
 | 
			
		||||
            })
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const groupedConsumption = groupTimeSeries(res.consumption, 'monthly');
 | 
			
		||||
        const groupedGeneration = groupTimeSeries(res.generation, 'monthly');
 | 
			
		||||
        // Map to chart rows; default nulls to 0 for stacking/tooltip friendliness
 | 
			
		||||
        const rows = results.map((kpi) => {
 | 
			
		||||
          const monthLabel = format(new Date(`${kpi.month}-01`), 'MMM');
 | 
			
		||||
          return {
 | 
			
		||||
            month: monthLabel,
 | 
			
		||||
            consumption: kpi.consumption_kwh ?? 0,
 | 
			
		||||
            generation: kpi.yield_kwh ?? 0,
 | 
			
		||||
          };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const monthMap = new Map<string, { consumption: number; generation: number }>();
 | 
			
		||||
 | 
			
		||||
        for (const entry of groupedConsumption) {
 | 
			
		||||
          if (!monthMap.has(entry.time)) {
 | 
			
		||||
            monthMap.set(entry.time, { consumption: 0, generation: 0 });
 | 
			
		||||
          }
 | 
			
		||||
          monthMap.get(entry.time)!.consumption = entry.value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const entry of groupedGeneration) {
 | 
			
		||||
          if (!monthMap.has(entry.time)) {
 | 
			
		||||
            monthMap.set(entry.time, { consumption: 0, generation: 0 });
 | 
			
		||||
          }
 | 
			
		||||
          monthMap.get(entry.time)!.generation = entry.value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const formatted = Array.from(monthMap.entries())
 | 
			
		||||
          .sort(([a], [b]) => a.localeCompare(b))
 | 
			
		||||
          .map(([key, val]) => ({
 | 
			
		||||
            month: format(new Date(`${key}-01`), 'MMM'),
 | 
			
		||||
            consumption: val.consumption,
 | 
			
		||||
            generation: val.generation,
 | 
			
		||||
          }));
 | 
			
		||||
 | 
			
		||||
        setChartData(formatted.slice(-6)); // last 6 months
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('Failed to fetch monthly power data:', error);
 | 
			
		||||
        setChartData([]);
 | 
			
		||||
        setChartData(rows);
 | 
			
		||||
      } finally {
 | 
			
		||||
        setLoading(false);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    fetchMonthlyData();
 | 
			
		||||
  }, [siteId]);
 | 
			
		||||
    load();
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [siteId]); // monthKeys are stable via useMemo
 | 
			
		||||
 | 
			
		||||
  if (loading || !siteId || chartData.length === 0) {
 | 
			
		||||
    return (
 | 
			
		||||
@ -152,9 +137,9 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
 | 
			
		||||
              axisLine={{ stroke: isDark ? '#fff' : '#222' }}
 | 
			
		||||
              tickLine={{ stroke: isDark ? '#fff' : '#222' }}
 | 
			
		||||
              label={{
 | 
			
		||||
              value: 'Power (kW)',   // <-- Y-axis label
 | 
			
		||||
              angle: -90,            // Vertical text
 | 
			
		||||
              position: 'insideLeft', // Position inside the chart area
 | 
			
		||||
                value: 'Energy (kWh)', // fixed: units are kWh
 | 
			
		||||
                angle: -90,
 | 
			
		||||
                position: 'insideLeft',
 | 
			
		||||
                style: {
 | 
			
		||||
                  textAnchor: 'middle',
 | 
			
		||||
                  fill: isDark ? '#fff' : '#222',
 | 
			
		||||
@ -174,15 +159,11 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
 | 
			
		||||
                color: isDark ? '#fff' : '#222',
 | 
			
		||||
              }}
 | 
			
		||||
              cursor={{
 | 
			
		||||
                fill: isDark ? '#808080' : '#e0e7ef', // dark mode bg, light mode bg
 | 
			
		||||
                fillOpacity: isDark ? 0.6 : 0.3,      // adjust opacity as you like
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <Legend
 | 
			
		||||
              wrapperStyle={{
 | 
			
		||||
                color: isDark ? '#fff' : '#222',
 | 
			
		||||
                fill: isDark ? '#808080' : '#e0e7ef',
 | 
			
		||||
                fillOpacity: isDark ? 0.6 : 0.3,
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <Legend wrapperStyle={{ color: isDark ? '#fff' : '#222' }} />
 | 
			
		||||
            <Bar dataKey="consumption" fill={consumptionColor} name="Consumption (kWh)" />
 | 
			
		||||
            <Bar dataKey="generation" fill={generationColor} name="Generation (kWh)" />
 | 
			
		||||
          </BarChart>
 | 
			
		||||
@ -194,3 +175,4 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
 | 
			
		||||
 | 
			
		||||
export default MonthlyBarChart;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user