179 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			179 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import React, { useEffect, useMemo, useState } from 'react';
 | |
| import {
 | |
|   BarChart,
 | |
|   Bar,
 | |
|   XAxis,
 | |
|   YAxis,
 | |
|   Tooltip,
 | |
|   ResponsiveContainer,
 | |
|   Legend,
 | |
| } from 'recharts';
 | |
| import { format } from 'date-fns';
 | |
| import { fetchMonthlyKpi, type MonthlyKPI } from '@/app/utils/api';
 | |
| 
 | |
| interface MonthlyBarChartProps {
 | |
|   siteId: string;
 | |
| }
 | |
| 
 | |
| 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);
 | |
|   }
 | |
|   return out;
 | |
| };
 | |
| 
 | |
| function useIsDarkMode() {
 | |
|   const [isDark, setIsDark] = useState(() =>
 | |
|     typeof document !== 'undefined'
 | |
|       ? document.body.classList.contains('dark')
 | |
|       : false
 | |
|   );
 | |
| 
 | |
|   useEffect(() => {
 | |
|     const check = () => setIsDark(document.body.classList.contains('dark'));
 | |
|     check();
 | |
|     const observer = new MutationObserver(check);
 | |
|     observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
 | |
|     return () => observer.disconnect();
 | |
|   }, []);
 | |
| 
 | |
|   return isDark;
 | |
| }
 | |
| 
 | |
| 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 load = async () => {
 | |
|       setLoading(true);
 | |
|       try {
 | |
|         // 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;
 | |
|             })
 | |
|           )
 | |
|         );
 | |
| 
 | |
|         // 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,
 | |
|           };
 | |
|         });
 | |
| 
 | |
|         setChartData(rows);
 | |
|       } finally {
 | |
|         setLoading(false);
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     load();
 | |
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | |
|   }, [siteId]); // monthKeys are stable via useMemo
 | |
| 
 | |
|   if (loading || !siteId || chartData.length === 0) {
 | |
|     return (
 | |
|       <div className="bg-white p-3 rounded-lg shadow-md dark:bg-rtgray-800 dark:text-white-light">
 | |
|         <div className="h-[200px] w-full flex items-center justify-center">
 | |
|           <p className="text-white/70">
 | |
|             {loading ? 'Loading data...' : 'No data available for chart.'}
 | |
|           </p>
 | |
|         </div>
 | |
|       </div>
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     <div className="bg-white p-3 rounded-lg dark:bg-rtgray-800 dark:text-white-light">
 | |
|       <div className="h-[200px] w-full">
 | |
|         <ResponsiveContainer width="100%" height="100%">
 | |
|           <BarChart data={chartData}>
 | |
|             <XAxis
 | |
|               dataKey="month"
 | |
|               tick={{ fontSize: 10, fill: isDark ? '#fff' : '#222' }}
 | |
|               axisLine={{ stroke: isDark ? '#fff' : '#222' }}
 | |
|               tickLine={{ stroke: isDark ? '#fff' : '#222' }}
 | |
|             />
 | |
|             <YAxis
 | |
|               tick={{ fontSize: 10, fill: isDark ? '#fff' : '#222' }}
 | |
|               axisLine={{ stroke: isDark ? '#fff' : '#222' }}
 | |
|               tickLine={{ stroke: isDark ? '#fff' : '#222' }}
 | |
|               label={{
 | |
|                 value: 'Energy (kWh)', // fixed: units are kWh
 | |
|                 angle: -90,
 | |
|                 position: 'insideLeft',
 | |
|                 style: {
 | |
|                   textAnchor: 'middle',
 | |
|                   fill: isDark ? '#fff' : '#222',
 | |
|                   fontSize: 12,
 | |
|                 },
 | |
|               }}
 | |
|             />
 | |
|             <Tooltip
 | |
|               formatter={(value: number) => [`${value.toFixed(2)} kWh`]}
 | |
|               labelFormatter={(label) => `${label}`}
 | |
|               contentStyle={{
 | |
|                 background: isDark ? '#232b3e' : '#fff',
 | |
|                 color: isDark ? '#fff' : '#222',
 | |
|                 border: isDark ? '1px solid #444' : '1px solid #ccc',
 | |
|               }}
 | |
|               labelStyle={{
 | |
|                 color: isDark ? '#fff' : '#222',
 | |
|               }}
 | |
|               cursor={{
 | |
|                 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>
 | |
|         </ResponsiveContainer>
 | |
|       </div>
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export default MonthlyBarChart;
 | |
| 
 | |
| 
 |