feature/syasya/testlayout #7
@ -25,6 +25,11 @@ const AdminDashboard = () => {
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const pathname = usePathname();
 | 
			
		||||
  const searchParams = useSearchParams();
 | 
			
		||||
  const siteIdMap: Record<SiteName, string> = {
 | 
			
		||||
  'Site A': 'site_01',
 | 
			
		||||
  'Site B': 'site_02',
 | 
			
		||||
  'Site C': 'site_03',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
  const siteParam = searchParams?.get('site');
 | 
			
		||||
  const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
 | 
			
		||||
@ -54,11 +59,6 @@ const AdminDashboard = () => {
 | 
			
		||||
 | 
			
		||||
  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();
 | 
			
		||||
@ -190,11 +190,7 @@ const AdminDashboard = () => {
 | 
			
		||||
 | 
			
		||||
        <div className="grid md:grid-cols-2 gap-6 lg:flex-col justify-center">
 | 
			
		||||
          <div ref={energyChartRef} className="pb-5">
 | 
			
		||||
            <EnergyLineChart
 | 
			
		||||
  consumption={timeSeriesData.consumption}
 | 
			
		||||
  generation={timeSeriesData.generation}
 | 
			
		||||
/>
 | 
			
		||||
 | 
			
		||||
            <EnergyLineChart siteId={siteIdMap[selectedSite]} />
 | 
			
		||||
 | 
			
		||||
          </div>
 | 
			
		||||
          <div ref={monthlyChartRef} className="pb-5">
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,18 @@ import React, { useRef, useEffect, useState } from 'react';
 | 
			
		||||
import { Line } from 'react-chartjs-2';
 | 
			
		||||
import ChartJS from 'chart.js/auto';
 | 
			
		||||
import zoomPlugin from 'chartjs-plugin-zoom';
 | 
			
		||||
import {
 | 
			
		||||
  getISOWeek,
 | 
			
		||||
  startOfDay,
 | 
			
		||||
  endOfDay,
 | 
			
		||||
  startOfWeek,
 | 
			
		||||
  endOfWeek,
 | 
			
		||||
  startOfMonth,
 | 
			
		||||
  endOfMonth,
 | 
			
		||||
  startOfYear,
 | 
			
		||||
  endOfYear,
 | 
			
		||||
} from 'date-fns';
 | 
			
		||||
import { fetchPowerTimeseries } from '@/app/utils/api';
 | 
			
		||||
 | 
			
		||||
ChartJS.register(zoomPlugin);
 | 
			
		||||
 | 
			
		||||
@ -11,22 +23,116 @@ interface TimeSeriesEntry {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface EnergyLineChartProps {
 | 
			
		||||
  consumption: TimeSeriesEntry[];
 | 
			
		||||
  generation: TimeSeriesEntry[];
 | 
			
		||||
  siteId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
 | 
			
		||||
function groupTimeSeries(
 | 
			
		||||
  data: TimeSeriesEntry[],
 | 
			
		||||
  mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly'
 | 
			
		||||
): TimeSeriesEntry[] {
 | 
			
		||||
  const groupMap = new Map<string, number[]>();
 | 
			
		||||
 | 
			
		||||
  for (const entry of data) {
 | 
			
		||||
    const date = new Date(entry.time);
 | 
			
		||||
    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';
 | 
			
		||||
        key = `${hour.toString().padStart(2, '0')}:${minute}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'daily':
 | 
			
		||||
        key = date.toLocaleDateString('en-MY', {
 | 
			
		||||
          timeZone: 'Asia/Kuala_Lumpur',
 | 
			
		||||
          weekday: 'short',
 | 
			
		||||
          day: '2-digit',
 | 
			
		||||
          month: 'short',
 | 
			
		||||
        });
 | 
			
		||||
        break;
 | 
			
		||||
      case 'weekly':
 | 
			
		||||
        key = `${date.getFullYear()}-W${String(getISOWeek(date)).padStart(2, '0')}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'monthly':
 | 
			
		||||
        key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'yearly':
 | 
			
		||||
        key = date.getFullYear().toString();
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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),
 | 
			
		||||
  }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
 | 
			
		||||
  const chartRef = useRef<any>(null);
 | 
			
		||||
  const [viewMode, setViewMode] = useState<'day' | 'daily' | 'weekly' | 'monthly' | 'yearly'>('day');
 | 
			
		||||
  const [consumption, setConsumption] = useState<TimeSeriesEntry[]>([]);
 | 
			
		||||
  const [generation, setGeneration] = useState<TimeSeriesEntry[]>([]);
 | 
			
		||||
  const [selectedDate, setSelectedDate] = useState(new Date());
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    const fetchData = async () => {
 | 
			
		||||
      try {
 | 
			
		||||
        const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd);
 | 
			
		||||
        setConsumption(res.consumption);
 | 
			
		||||
        setGeneration(res.generation);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('Failed to fetch energy timeseries:', error);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    fetchData();
 | 
			
		||||
  }, [siteId, viewMode, selectedDate]);
 | 
			
		||||
 | 
			
		||||
  const groupedConsumption = groupTimeSeries(consumption, viewMode);
 | 
			
		||||
  const groupedGeneration = groupTimeSeries(generation, viewMode);
 | 
			
		||||
 | 
			
		||||
  // 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", ...]
 | 
			
		||||
    ...groupedConsumption.map(d => d.time),
 | 
			
		||||
    ...groupedGeneration.map(d => d.time),
 | 
			
		||||
  ])).sort();
 | 
			
		||||
 | 
			
		||||
  // 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 consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value]));
 | 
			
		||||
  const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value]));
 | 
			
		||||
 | 
			
		||||
  const [startIndex, setStartIndex] = useState(0);
 | 
			
		||||
  const [endIndex, setEndIndex] = useState(allTimes.length - 1);
 | 
			
		||||
@ -37,6 +143,22 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setStartIndex(0);
 | 
			
		||||
    setEndIndex(allTimes.length - 1);
 | 
			
		||||
  }, [viewMode, allTimes.length]);
 | 
			
		||||
 | 
			
		||||
  const formatLabel = (key: string) => {
 | 
			
		||||
    switch (viewMode) {
 | 
			
		||||
      case 'monthly':
 | 
			
		||||
        return new Date(`${key}-01`).toLocaleString('en-GB', { month: 'short', year: 'numeric' });
 | 
			
		||||
      case 'weekly':
 | 
			
		||||
        return key.replace('-', ' ');
 | 
			
		||||
      default:
 | 
			
		||||
        return key;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const filteredLabels = allTimes.slice(startIndex, endIndex + 1);
 | 
			
		||||
  const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? null);
 | 
			
		||||
  const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? null);
 | 
			
		||||
@ -46,7 +168,7 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
 | 
			
		||||
  const yAxisSuggestedMax = maxValue * 1.15;
 | 
			
		||||
 | 
			
		||||
  const data = {
 | 
			
		||||
    labels: filteredLabels,
 | 
			
		||||
    labels: filteredLabels.map(formatLabel),
 | 
			
		||||
    datasets: [
 | 
			
		||||
      {
 | 
			
		||||
        label: 'Consumption',
 | 
			
		||||
@ -84,10 +206,17 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
 | 
			
		||||
      x: {
 | 
			
		||||
        title: {
 | 
			
		||||
          display: true,
 | 
			
		||||
        text: 'Time (HH:MM)',
 | 
			
		||||
        font: {
 | 
			
		||||
          weight: 'bold' as const, // ✅ FIX: cast as 'const'
 | 
			
		||||
        },
 | 
			
		||||
          text:
 | 
			
		||||
            viewMode === 'day'
 | 
			
		||||
              ? 'Time (HH:MM)'
 | 
			
		||||
              : viewMode === 'daily'
 | 
			
		||||
              ? 'Day'
 | 
			
		||||
              : viewMode === 'weekly'
 | 
			
		||||
              ? 'Week'
 | 
			
		||||
              : viewMode === 'monthly'
 | 
			
		||||
              ? 'Month'
 | 
			
		||||
              : 'Year',
 | 
			
		||||
          font: { weight: 'bold' as const },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      y: {
 | 
			
		||||
@ -96,14 +225,11 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
 | 
			
		||||
        title: {
 | 
			
		||||
          display: true,
 | 
			
		||||
          text: 'Power (kW)',
 | 
			
		||||
        font: {
 | 
			
		||||
          weight: 'bold' as const, // ✅ FIX: cast as 'const'
 | 
			
		||||
          font: { weight: 'bold' as const },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
} as const; // ✅ Ensures compatibility with chart.js types
 | 
			
		||||
 | 
			
		||||
  } as const;
 | 
			
		||||
 | 
			
		||||
  const handleResetZoom = () => {
 | 
			
		||||
    chartRef.current?.resetZoom();
 | 
			
		||||
@ -119,9 +245,20 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {/* Time range selectors */}
 | 
			
		||||
        <div className="mb-4 flex gap-4 items-center">
 | 
			
		||||
          <label className='font-medium'>
 | 
			
		||||
          {viewMode === 'day' && (
 | 
			
		||||
            <label className="font-medium">
 | 
			
		||||
              Date:{' '}
 | 
			
		||||
              <input
 | 
			
		||||
                type="date"
 | 
			
		||||
                value={selectedDate.toISOString().split('T')[0]}
 | 
			
		||||
                onChange={(e) => setSelectedDate(new Date(e.target.value))}
 | 
			
		||||
                className="border rounded p-1"
 | 
			
		||||
              />
 | 
			
		||||
            </label>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <label className="font-medium">
 | 
			
		||||
            From:{' '}
 | 
			
		||||
            <select
 | 
			
		||||
              value={startIndex}
 | 
			
		||||
@ -132,11 +269,11 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
 | 
			
		||||
              className="border rounded p-1"
 | 
			
		||||
            >
 | 
			
		||||
              {allTimes.map((label, idx) => (
 | 
			
		||||
                <option key={idx} value={idx}>{label}</option>
 | 
			
		||||
                <option key={idx} value={idx}>{formatLabel(label)}</option>
 | 
			
		||||
              ))}
 | 
			
		||||
            </select>
 | 
			
		||||
          </label>
 | 
			
		||||
          <label className='font-medium'>
 | 
			
		||||
          <label className="font-medium">
 | 
			
		||||
            To:{' '}
 | 
			
		||||
            <select
 | 
			
		||||
              value={endIndex}
 | 
			
		||||
@ -147,10 +284,24 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
 | 
			
		||||
              className="border rounded p-1"
 | 
			
		||||
            >
 | 
			
		||||
              {allTimes.map((label, idx) => (
 | 
			
		||||
                <option key={idx} value={idx}>{label}</option>
 | 
			
		||||
                <option key={idx} value={idx}>{formatLabel(label)}</option>
 | 
			
		||||
              ))}
 | 
			
		||||
            </select>
 | 
			
		||||
          </label>
 | 
			
		||||
          <label className="font-medium">
 | 
			
		||||
            View:{' '}
 | 
			
		||||
            <select
 | 
			
		||||
              value={viewMode}
 | 
			
		||||
              onChange={(e) => setViewMode(e.target.value as typeof viewMode)}
 | 
			
		||||
              className="border rounded p-1"
 | 
			
		||||
            >
 | 
			
		||||
              <option value="day">Day</option>
 | 
			
		||||
              <option value="daily">Daily</option>
 | 
			
		||||
              <option value="weekly">Weekly</option>
 | 
			
		||||
              <option value="monthly">Monthly</option>
 | 
			
		||||
              <option value="yearly">Yearly</option>
 | 
			
		||||
            </select>
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className="h-96 w-full">
 | 
			
		||||
@ -164,3 +315,8 @@ const EnergyLineChart = ({ consumption, generation }: EnergyLineChartProps) => {
 | 
			
		||||
export default EnergyLineChart;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -23,6 +23,7 @@
 | 
			
		||||
                "chart.js": "^4.4.9",
 | 
			
		||||
                "chartjs-plugin-zoom": "^2.2.0",
 | 
			
		||||
                "cookie": "^1.0.2",
 | 
			
		||||
                "date-fns": "^4.1.0",
 | 
			
		||||
                "eslint": "8.32.0",
 | 
			
		||||
                "eslint-config-next": "13.1.2",
 | 
			
		||||
                "html2canvas": "^1.4.1",
 | 
			
		||||
@ -1974,6 +1975,16 @@
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
 | 
			
		||||
            "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/date-fns": {
 | 
			
		||||
            "version": "4.1.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
 | 
			
		||||
            "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "funding": {
 | 
			
		||||
                "type": "github",
 | 
			
		||||
                "url": "https://github.com/sponsors/kossnocorp"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/debug": {
 | 
			
		||||
            "version": "4.3.4",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
 | 
			
		||||
@ -7633,6 +7644,11 @@
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
 | 
			
		||||
            "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="
 | 
			
		||||
        },
 | 
			
		||||
        "date-fns": {
 | 
			
		||||
            "version": "4.1.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
 | 
			
		||||
            "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
 | 
			
		||||
        },
 | 
			
		||||
        "debug": {
 | 
			
		||||
            "version": "4.3.4",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@
 | 
			
		||||
        "chart.js": "^4.4.9",
 | 
			
		||||
        "chartjs-plugin-zoom": "^2.2.0",
 | 
			
		||||
        "cookie": "^1.0.2",
 | 
			
		||||
        "date-fns": "^4.1.0",
 | 
			
		||||
        "eslint": "8.32.0",
 | 
			
		||||
        "eslint-config-next": "13.1.2",
 | 
			
		||||
        "html2canvas": "^1.4.1",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user