feature/syasya/testlayout #6
@ -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>
 | 
					        </div>
 | 
				
			||||||
 | 
					         {/* TOP 3 CARDS */}
 | 
				
			||||||
        <div className="grid md:grid-cols-2 gap-6 lg:flex-col justify-center">
 | 
					          <div className="space-y-4">
 | 
				
			||||||
          <div ref={energyChartRef} className="pb-5">
 | 
					            <KpiTop
 | 
				
			||||||
            <EnergyLineChart siteId={siteIdMap[selectedSite]} />
 | 
					              yieldKwh={yieldKwh}
 | 
				
			||||||
 | 
					              consumptionKwh={consumptionKwh}
 | 
				
			||||||
 | 
					              gridDrawKwh={gridDrawKwh}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <div ref={monthlyChartRef} className="pb-5">
 | 
					 
 | 
				
			||||||
 | 
					        <div ref={energyChartRef} className="pb-5">
 | 
				
			||||||
 | 
					            <EnergyLineChart siteId={siteIdMap[selectedSite]} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					         {/* 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>
 | 
				
			||||||
        </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 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">
 | 
				
			||||||
        <thead>
 | 
					        {!siteId ? (
 | 
				
			||||||
          <tr className="bg-rtgray-100 dark:bg-rtgray-800 text-black dark:text-white">
 | 
					          <p className="text-center py-10">No site selected</p>
 | 
				
			||||||
            <th className="border border-rtgray-300 dark:border-rtgray-700 p-3 text-left dark:bg-rtgray-900">KPI</th>
 | 
					        ) : loading ? (
 | 
				
			||||||
            <th className="border border-rtgray-300 dark:border-rtgray-700 p-3 text-left dark:bg-rtgray-900">Value</th>
 | 
					          <p className="text-center py-10">Loading...</p>
 | 
				
			||||||
          </tr>
 | 
					        ) : (
 | 
				
			||||||
        </thead>
 | 
					          <table className="w-full border-collapse">
 | 
				
			||||||
        <tbody>
 | 
					            <thead>
 | 
				
			||||||
          {data.map((row) => (
 | 
					              <tr className="bg-gray-100 dark:bg-rtgray-800">
 | 
				
			||||||
            <tr key={row.kpi} className="even:bg-rtgray-50 dark:even:bg-rtgray-800">
 | 
					                <th className="border p-3 text-left dark:text-white">KPI</th>
 | 
				
			||||||
              <td className="border border-rtgray-300 dark:border-rtgray-700 p-2.5">{row.kpi}</td>
 | 
					                <th className="border p-3 text-left dark:text-white">Value</th>
 | 
				
			||||||
              <td className="border border-rtgray-300 dark:border-rtgray-700 p-2.5">{row.value}</td>
 | 
					              </tr>
 | 
				
			||||||
            </tr>
 | 
					            </thead>
 | 
				
			||||||
          ))}
 | 
					            <tbody>
 | 
				
			||||||
        </tbody>
 | 
					              {rows.map((row) => (
 | 
				
			||||||
      </table>
 | 
					                <tr key={row.label} className="even:bg-gray-50 dark:even:bg-rtgray-800">
 | 
				
			||||||
 | 
					                  <td className="border p-2.5 dark:text-white">{row.label}</td>
 | 
				
			||||||
 | 
					                  <td className="border p-2.5 dark:text-white">{row.value}</td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </tbody>
 | 
				
			||||||
 | 
					          </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