UserDashboard/components/dashboards/MonthlyBarChart.tsx
2025-08-20 09:07:32 +08:00

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;