new layout test
This commit is contained in:
parent
81a00d72e4
commit
9ab01d2655
@ -10,6 +10,8 @@ import html2canvas from 'html2canvas';
|
|||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { fetchPowerTimeseries } from '@/app/utils/api';
|
import { fetchPowerTimeseries } from '@/app/utils/api';
|
||||||
|
import KpiTop from '@/components/dashboards/kpitop';
|
||||||
|
import KpiBottom from '@/components/dashboards/kpibottom';
|
||||||
|
|
||||||
const EnergyLineChart = dynamic(() => import('@/components/dashboards/EnergyLineChart'), {
|
const EnergyLineChart = dynamic(() => import('@/components/dashboards/EnergyLineChart'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -19,6 +21,16 @@ const MonthlyBarChart = dynamic(() => import('@/components/dashboards/MonthlyBar
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type MonthlyKPI = {
|
||||||
|
site: string; month: string;
|
||||||
|
yield_kwh: number | null; consumption_kwh: number | null; grid_draw_kwh: number | null;
|
||||||
|
efficiency: number | null; peak_demand_kw: number | null;
|
||||||
|
avg_power_factor: number | null; load_factor: number | null;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000';
|
||||||
|
|
||||||
import { SiteName, SiteDetails, mockSiteData } from '@/types/SiteData';
|
import { SiteName, SiteDetails, mockSiteData } from '@/types/SiteData';
|
||||||
|
|
||||||
const AdminDashboard = () => {
|
const AdminDashboard = () => {
|
||||||
@ -34,6 +46,8 @@ const AdminDashboard = () => {
|
|||||||
const siteParam = searchParams?.get('site');
|
const siteParam = searchParams?.get('site');
|
||||||
const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
|
const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
|
||||||
|
|
||||||
|
const [kpi, setKpi] = useState<MonthlyKPI | null>(null);
|
||||||
|
|
||||||
const [selectedSite, setSelectedSite] = useState<SiteName>(() => {
|
const [selectedSite, setSelectedSite] = useState<SiteName>(() => {
|
||||||
if (siteParam && validSiteNames.includes(siteParam as SiteName)) {
|
if (siteParam && validSiteNames.includes(siteParam as SiteName)) {
|
||||||
return siteParam as SiteName;
|
return siteParam as SiteName;
|
||||||
@ -94,6 +108,23 @@ const AdminDashboard = () => {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [selectedSite]);
|
}, [selectedSite]);
|
||||||
|
|
||||||
|
// fetch KPI monthly (uses your FastAPI)
|
||||||
|
useEffect(() => {
|
||||||
|
const siteId = siteIdMap[selectedSite];
|
||||||
|
const url = `${API}/kpi/monthly?site=${encodeURIComponent(siteId)}&month=${currentMonth}`;
|
||||||
|
fetch(url).then(r => r.json()).then(setKpi).catch(console.error);
|
||||||
|
}, [selectedSite]);
|
||||||
|
|
||||||
|
// derived values with safe fallbacks
|
||||||
|
const yieldKwh = kpi?.yield_kwh ?? 0;
|
||||||
|
const consumptionKwh = kpi?.consumption_kwh ?? 0;
|
||||||
|
const gridDrawKwh = kpi?.grid_draw_kwh ?? Math.max(0, consumptionKwh - yieldKwh);
|
||||||
|
|
||||||
|
const efficiencyPct = (kpi?.efficiency ?? 0) * 100;
|
||||||
|
const powerFactor = kpi?.avg_power_factor ?? 0;
|
||||||
|
const loadFactor = (kpi?.load_factor ?? 0);
|
||||||
|
|
||||||
|
// ...your existing code above return()
|
||||||
// Update query string when site is changed manually
|
// Update query string when site is changed manually
|
||||||
const handleSiteChange = (newSite: SiteName) => {
|
const handleSiteChange = (newSite: SiteName) => {
|
||||||
setSelectedSite(newSite);
|
setSelectedSite(newSite);
|
||||||
@ -169,7 +200,7 @@ const AdminDashboard = () => {
|
|||||||
<div className="px-6 space-y-6">
|
<div className="px-6 space-y-6">
|
||||||
<h1 className="text-lg font-semibold dark:text-white">Admin Dashboard</h1>
|
<h1 className="text-lg font-semibold dark:text-white">Admin Dashboard</h1>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid gap-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SiteSelector
|
<SiteSelector
|
||||||
selectedSite={selectedSite}
|
selectedSite={selectedSite}
|
||||||
@ -183,22 +214,37 @@ const AdminDashboard = () => {
|
|||||||
lastSyncTimestamp={currentSiteDetails.lastSyncTimestamp}
|
lastSyncTimestamp={currentSiteDetails.lastSyncTimestamp}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<KPI_Table siteId={siteIdMap[selectedSite]} month={currentMonth} />
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* TOP 3 CARDS */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<KpiTop
|
||||||
|
yieldKwh={yieldKwh}
|
||||||
|
consumptionKwh={consumptionKwh}
|
||||||
|
gridDrawKwh={gridDrawKwh}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6 lg:flex-col justify-center">
|
|
||||||
<div ref={energyChartRef} className="pb-5">
|
<div ref={energyChartRef} className="pb-5">
|
||||||
<EnergyLineChart siteId={siteIdMap[selectedSite]} />
|
<EnergyLineChart siteId={siteIdMap[selectedSite]} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div ref={monthlyChartRef} className="pb-5">
|
{/* BOTTOM 3 PANELS */}
|
||||||
|
<KpiBottom
|
||||||
|
efficiencyPct={efficiencyPct}
|
||||||
|
powerFactor={powerFactor}
|
||||||
|
loadFactor={loadFactor}
|
||||||
|
middle={
|
||||||
|
<div ref={monthlyChartRef} className="transform scale-90 origin-top">
|
||||||
<MonthlyBarChart siteId={siteIdMap[selectedSite]} />
|
<MonthlyBarChart siteId={siteIdMap[selectedSite]} />
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<div className="flex items-center justify-center w-full px-3 text-center">
|
||||||
|
<div className="text-3xl font-semibold">
|
||||||
|
{(kpi?.peak_demand_kw ?? 0).toFixed(2)} kW
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div className="flex flex-col md:flex-row gap-4 justify-center">
|
<div className="flex flex-col md:flex-row gap-4 justify-center">
|
||||||
<button onClick={handlePDFExport} className="text-sm lg:text-lg btn-primary">
|
<button onClick={handlePDFExport} className="text-sm lg:text-lg btn-primary">
|
||||||
Export Chart Images to PDF
|
Export Chart Images to PDF
|
||||||
|
@ -48,3 +48,29 @@ export async function fetchForecast(
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MonthlyKPI = {
|
||||||
|
site: string;
|
||||||
|
month: string; // "YYYY-MM"
|
||||||
|
yield_kwh: number | null;
|
||||||
|
consumption_kwh: number | null;
|
||||||
|
grid_draw_kwh: number | null;
|
||||||
|
efficiency: number | null; // 0..1 (fraction)
|
||||||
|
peak_demand_kw: number | null;
|
||||||
|
avg_power_factor: number | null; // 0..1
|
||||||
|
load_factor: number | null; // 0..1
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const API = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
|
||||||
|
|
||||||
|
export async function fetchMonthlyKpi(params: {
|
||||||
|
site: string;
|
||||||
|
month: string; // "YYYY-MM"
|
||||||
|
consumption_topic?: string;
|
||||||
|
generation_topic?: string;
|
||||||
|
}): Promise<MonthlyKPI> {
|
||||||
|
const qs = new URLSearchParams(params as Record<string, string>);
|
||||||
|
const res = await fetch(`${API}/kpi/monthly?${qs.toString()}`, { cache: "no-store" });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
@ -224,6 +224,20 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
|
|
||||||
const axisColor = isDark ? '#fff' : '#222';
|
const axisColor = isDark ? '#fff' : '#222';
|
||||||
|
|
||||||
|
function areaGradient(ctx: any, hex: string, alphaTop = 0.22, alphaBottom = 0.02) {
|
||||||
|
const { ctx: g, chartArea } = ctx.chart;
|
||||||
|
if (!chartArea) return hex; // initial render fallback
|
||||||
|
const gradient = g.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
|
||||||
|
// top more opaque → bottom fades out
|
||||||
|
gradient.addColorStop(0, hex + Math.floor(alphaTop * 255).toString(16).padStart(2, '0'));
|
||||||
|
gradient.addColorStop(1, hex + Math.floor(alphaBottom * 255).toString(16).padStart(2, '0'));
|
||||||
|
return gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define colors for both light and dark modes
|
||||||
|
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 data = {
|
const data = {
|
||||||
labels: filteredLabels.map(formatLabel),
|
labels: filteredLabels.map(formatLabel),
|
||||||
@ -231,26 +245,29 @@ const axisColor = isDark ? '#fff' : '#222';
|
|||||||
{
|
{
|
||||||
label: 'Consumption',
|
label: 'Consumption',
|
||||||
data: filteredConsumption,
|
data: filteredConsumption,
|
||||||
borderColor: '#8884d8',
|
borderColor: consumptionColor,
|
||||||
|
backgroundColor: (ctx: any) => areaGradient(ctx, consumptionColor),
|
||||||
|
fill: true, // <-- fill under line
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
fill: false,
|
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Generation',
|
label: 'Generation',
|
||||||
data: filteredGeneration,
|
data: filteredGeneration,
|
||||||
borderColor: '#82ca9d',
|
borderColor: generationColor,
|
||||||
|
backgroundColor: (ctx: any) => areaGradient(ctx, generationColor),
|
||||||
|
fill: true, // <-- fill under line
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
fill: false,
|
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Forecasted Solar',
|
label: 'Forecasted Solar',
|
||||||
data: filteredForecast,
|
data: filteredForecast,
|
||||||
borderColor: '#ffa500', // orange
|
borderColor: '#fcd913', // orange
|
||||||
|
backgroundColor: (ctx: any) => areaGradient(ctx, '#fcd913', 0.18, 0.03),
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
borderDash: [5, 5], // dashed line to distinguish forecast
|
borderDash: [5, 5], // dashed line to distinguish forecast
|
||||||
fill: false,
|
fill: true,
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface KPI_TableProps {
|
interface KPI_TableProps {
|
||||||
siteId: string;
|
siteId: string;
|
||||||
@ -12,8 +12,8 @@ interface MonthlyKPI {
|
|||||||
consumption_kwh: number | null;
|
consumption_kwh: number | null;
|
||||||
grid_draw_kwh: number | null;
|
grid_draw_kwh: number | null;
|
||||||
efficiency: number | null;
|
efficiency: number | null;
|
||||||
peak_demand_kw: number | null; // ✅ new
|
peak_demand_kw: number | null;
|
||||||
avg_power_factor: number | null; // ✅ new
|
avg_power_factor: number | null;
|
||||||
load_factor: number | null;
|
load_factor: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,83 +22,66 @@ const KPI_Table: React.FC<KPI_TableProps> = ({ siteId, month }) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!siteId || !month) return;
|
||||||
|
|
||||||
const fetchKPI = async () => {
|
const fetchKPI = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://localhost:8000/kpi/monthly?site=${siteId}&month=${month}`);
|
const res = await fetch(
|
||||||
const data = await res.json();
|
`http://localhost:8000/kpi/monthly?site=${siteId}&month=${month}`
|
||||||
setKpiData(data);
|
);
|
||||||
|
setKpiData(await res.json());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch KPI:', err);
|
console.error("Failed to fetch KPI:", err);
|
||||||
setKpiData(null); // fallback
|
setKpiData(null);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (siteId && month) fetchKPI();
|
fetchKPI();
|
||||||
}, [siteId, month]);
|
}, [siteId, month]);
|
||||||
|
|
||||||
if (!siteId) {
|
const formatValue = (value: number | null, unit = "", decimals = 2) =>
|
||||||
return (
|
value != null ? `${value.toFixed(decimals)}${unit}` : "—";
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-bold mb-2">Monthly KPI</h2>
|
|
||||||
<div className="min-h-[275px] w-full flex items-center justify-center border">
|
|
||||||
<p>No site selected</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
const rows = [
|
||||||
return (
|
{ label: "Monthly Yield", value: formatValue(kpiData?.yield_kwh ?? null, " kWh", 0) },
|
||||||
<div>
|
{ label: "Monthly Consumption", value: formatValue(kpiData?.consumption_kwh ?? null, " kWh", 0) },
|
||||||
<h2 className="text-lg font-bold mb-2">Monthly KPI</h2>
|
{ label: "Monthly Grid Draw", value: formatValue(kpiData?.grid_draw_kwh ?? null, " kWh", 0) },
|
||||||
<div className="min-h-[275px] w-full flex items-center justify-center border">
|
{ label: "Efficiency", value: formatValue(kpiData?.efficiency ?? null, "%", 1) },
|
||||||
<p>Loading...</p>
|
{ label: "Peak Demand", value: formatValue(kpiData?.peak_demand_kw ?? null, " kW") },
|
||||||
</div>
|
{ label: "Power Factor", value: formatValue(kpiData?.avg_power_factor ?? null) },
|
||||||
</div>
|
{ label: "Load Factor", value: formatValue(kpiData?.load_factor ?? null) },
|
||||||
);
|
];
|
||||||
}
|
|
||||||
|
|
||||||
// Use optional chaining and nullish coalescing to safely default values to 0
|
|
||||||
const yield_kwh = kpiData?.yield_kwh ?? 0;
|
|
||||||
const consumption_kwh = kpiData?.consumption_kwh ?? 0;
|
|
||||||
const grid_draw_kwh = kpiData?.grid_draw_kwh ?? 0;
|
|
||||||
const efficiency = kpiData?.efficiency ?? 0;
|
|
||||||
const peak_demand_kw = kpiData?.peak_demand_kw ?? 0;
|
|
||||||
const power_factor = kpiData?.avg_power_factor ?? 0;
|
|
||||||
const load_factor = kpiData?.load_factor ?? 0;
|
|
||||||
|
|
||||||
const data = [
|
|
||||||
{ kpi: 'Monthly Yield', value: `${yield_kwh.toFixed(0)} kWh` },
|
|
||||||
{ kpi: 'Monthly Consumption', value: `${consumption_kwh.toFixed(0)} kWh` },
|
|
||||||
{ kpi: 'Monthly Grid Draw', value: `${grid_draw_kwh.toFixed(0)} kWh` },
|
|
||||||
{ kpi: 'Efficiency', value: `${efficiency.toFixed(1)}%` },
|
|
||||||
{ kpi: 'Peak Demand', value: `${peak_demand_kw.toFixed(2)} kW` }, // ✅ added
|
|
||||||
{ kpi: 'Power Factor', value: `${power_factor.toFixed(2)} kW` }, // ✅ added
|
|
||||||
{ kpi: 'Load Factor', value: `${load_factor.toFixed(2)} kW` }, // ✅ added
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-bold mb-2 dark:text-white">Monthly KPI</h2>
|
<h2 className="text-lg font-bold mb-2 dark:text-white">Monthly KPI</h2>
|
||||||
<table className="min-h-[275px] w-full border-collapse border border-gray-300 dark:border-rtgray-700 text-black dark:text-white bg-white dark:bg-rtgray-700">
|
<div className="min-h-[275px] border rounded">
|
||||||
|
{!siteId ? (
|
||||||
|
<p className="text-center py-10">No site selected</p>
|
||||||
|
) : loading ? (
|
||||||
|
<p className="text-center py-10">Loading...</p>
|
||||||
|
) : (
|
||||||
|
<table className="w-full border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-rtgray-100 dark:bg-rtgray-800 text-black dark:text-white">
|
<tr className="bg-gray-100 dark:bg-rtgray-800">
|
||||||
<th className="border border-rtgray-300 dark:border-rtgray-700 p-3 text-left dark:bg-rtgray-900">KPI</th>
|
<th className="border p-3 text-left dark:text-white">KPI</th>
|
||||||
<th className="border border-rtgray-300 dark:border-rtgray-700 p-3 text-left dark:bg-rtgray-900">Value</th>
|
<th className="border p-3 text-left dark:text-white">Value</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.map((row) => (
|
{rows.map((row) => (
|
||||||
<tr key={row.kpi} className="even:bg-rtgray-50 dark:even:bg-rtgray-800">
|
<tr key={row.label} className="even:bg-gray-50 dark:even:bg-rtgray-800">
|
||||||
<td className="border border-rtgray-300 dark:border-rtgray-700 p-2.5">{row.kpi}</td>
|
<td className="border p-2.5 dark:text-white">{row.label}</td>
|
||||||
<td className="border border-rtgray-300 dark:border-rtgray-700 p-2.5">{row.value}</td>
|
<td className="border p-2.5 dark:text-white">{row.value}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -106,3 +89,4 @@ const data = [
|
|||||||
export default KPI_Table;
|
export default KPI_Table;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -126,9 +126,9 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
|
|||||||
|
|
||||||
if (loading || !siteId || chartData.length === 0) {
|
if (loading || !siteId || chartData.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-4 rounded-lg shadow-md dark:bg-rtgray-800 dark:text-white-light">
|
<div className="bg-white p-3 rounded-lg shadow-md dark:bg-rtgray-800 dark:text-white-light">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<h2 className="text-lg font-bold pb-3">Monthly Energy Yield</h2>
|
<h2 className="text-lg font-bold">Monthly Energy Yield</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-96 w-full flex items-center justify-center">
|
<div className="h-96 w-full flex items-center justify-center">
|
||||||
<p className="text-white/70">
|
<p className="text-white/70">
|
||||||
@ -140,14 +140,10 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-4 rounded-lg shadow-md dark:bg-rtgray-800 dark:text-white-light">
|
<div className="bg-white p-3 rounded-lg dark:bg-rtgray-800 dark:text-white-light">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="h-[200px] w-full">
|
||||||
<h2 className="text-lg font-bold pb-3">Monthly Energy Yield</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="lg:h-[22.6vw] h-[290px] w-full pt-10">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={chartData}>
|
<BarChart data={chartData} >
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="month"
|
dataKey="month"
|
||||||
tick={{ fontSize: 10, fill: isDark ? '#fff' : '#222' }}
|
tick={{ fontSize: 10, fill: isDark ? '#fff' : '#222' }}
|
||||||
@ -158,6 +154,16 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
|
|||||||
tick={{ fontSize: 10, fill: isDark ? '#fff' : '#222' }}
|
tick={{ fontSize: 10, fill: isDark ? '#fff' : '#222' }}
|
||||||
axisLine={{ stroke: isDark ? '#fff' : '#222' }}
|
axisLine={{ stroke: isDark ? '#fff' : '#222' }}
|
||||||
tickLine={{ 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
|
||||||
|
style: {
|
||||||
|
textAnchor: 'middle',
|
||||||
|
fill: isDark ? '#fff' : '#222',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={(value: number) => [`${value.toFixed(2)} kWh`]}
|
formatter={(value: number) => [`${value.toFixed(2)} kWh`]}
|
||||||
|
54
components/dashboards/kpibottom.tsx
Normal file
54
components/dashboards/kpibottom.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// components/dashboards/KpiBottom.tsx
|
||||||
|
'use client';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
efficiencyPct: number; // % value (0..100)
|
||||||
|
powerFactor: number; // 0..1
|
||||||
|
loadFactor: number; // ratio, not %
|
||||||
|
middle?: ReactNode;
|
||||||
|
right?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Panel = ({ title, children }: { title: string; children: ReactNode }) => (
|
||||||
|
<div className="rounded-2xl p-5 shadow-md bg-white dark:bg-rtgray-800 text-white min-h-[260px] flex flex-col">
|
||||||
|
<div className="text-lg font-bold opacity-80 mb-3">{title}</div>
|
||||||
|
<div className="flex-1 grid place-items-center">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Stat = ({ value, label, accent = false }: { value: ReactNode; label: string; accent?: boolean }) => (
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<div className={`text-3xl font-semibold ${accent ? 'text-[#fcd913]' : 'text-white'}`}>{value}</div>
|
||||||
|
<div className="text-xs text-[#9aa4b2]">{label}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function KpiBottom({
|
||||||
|
efficiencyPct, powerFactor, loadFactor, middle, right,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
|
<Panel title="Measurements">
|
||||||
|
<div className="grid grid-cols-3 gap-3 w-full">
|
||||||
|
<Stat value={`${efficiencyPct.toFixed(1)}%`} label="Efficiency" />
|
||||||
|
<Stat value={powerFactor.toFixed(2)} label="Power Factor" />
|
||||||
|
<Stat value={loadFactor.toFixed(2)} label="Load Factor" />
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel title="Monthly Yield">
|
||||||
|
<div className="w-full h-48">
|
||||||
|
{middle}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel title="Peak Power Demand">
|
||||||
|
<div className="text-3xl font-semibold">
|
||||||
|
{right}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
74
components/dashboards/kpitop.tsx
Normal file
74
components/dashboards/kpitop.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// components/KpiTop.tsx
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
month?: string;
|
||||||
|
yieldKwh: number;
|
||||||
|
consumptionKwh: number;
|
||||||
|
gridDrawKwh: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Card: React.FC<{ title: string; value: string; accent?: boolean; icon?: React.ReactNode }> = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
accent,
|
||||||
|
icon,
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={`rounded-xl p-4 md:p-5 shadow-sm
|
||||||
|
${accent
|
||||||
|
? "bg-[#fcd913] text-black"
|
||||||
|
: "bg-white text-gray-900 dark:bg-rtgray-800 dark:text-white"}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="shrink-0 text-black dark:text-white">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-lg font-bold opacity-80">{title}</div>
|
||||||
|
<div className="text-2xl font-semibold">{value}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export default function KpiTop({ month, yieldKwh, consumptionKwh, gridDrawKwh }: Props) {
|
||||||
|
return (
|
||||||
|
<section aria-label="Top KPIs" className="space-y-3">
|
||||||
|
{month && <div className="text-xs dark:text-[#9aa4b2]">{month}</div>}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
<Card
|
||||||
|
title="Monthly Yield"
|
||||||
|
value={`${yieldKwh.toLocaleString()} kWh`}
|
||||||
|
icon={
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="12" r="4" stroke="#fcd913" strokeWidth="2" />
|
||||||
|
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1" stroke="#fcd913" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
title="Monthly Consumption"
|
||||||
|
value={`${consumptionKwh.toLocaleString()} kWh`}
|
||||||
|
icon={
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect x="8" y="3" width="8" height="12" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<path d="M12 15v6" stroke="#fcd913" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
title="Monthly Grid Draw"
|
||||||
|
value={`${gridDrawKwh.toLocaleString()} kWh`}
|
||||||
|
icon={
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M5 21h14M7 21l5-18 5 18" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<path d="M14 8l2 2" stroke="#fcd913" strokeWidth="2" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user