feature/syasya/testlayout #6
@ -10,6 +10,8 @@ import html2canvas from 'html2canvas';
 | 
			
		||||
import jsPDF from 'jspdf';
 | 
			
		||||
import dynamic from 'next/dynamic';
 | 
			
		||||
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'), {
 | 
			
		||||
  ssr: false,
 | 
			
		||||
@ -19,6 +21,16 @@ const MonthlyBarChart = dynamic(() => import('@/components/dashboards/MonthlyBar
 | 
			
		||||
  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';
 | 
			
		||||
 | 
			
		||||
const AdminDashboard = () => {
 | 
			
		||||
@ -34,6 +46,8 @@ const AdminDashboard = () => {
 | 
			
		||||
  const siteParam = searchParams?.get('site');
 | 
			
		||||
  const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
 | 
			
		||||
 | 
			
		||||
  const [kpi, setKpi] = useState<MonthlyKPI | null>(null);
 | 
			
		||||
 | 
			
		||||
  const [selectedSite, setSelectedSite] = useState<SiteName>(() => {
 | 
			
		||||
    if (siteParam && validSiteNames.includes(siteParam as SiteName)) {
 | 
			
		||||
      return siteParam as SiteName;
 | 
			
		||||
@ -94,6 +108,23 @@ const AdminDashboard = () => {
 | 
			
		||||
  fetchData();
 | 
			
		||||
}, [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
 | 
			
		||||
  const handleSiteChange = (newSite: SiteName) => {
 | 
			
		||||
    setSelectedSite(newSite);
 | 
			
		||||
@ -169,7 +200,7 @@ const AdminDashboard = () => {
 | 
			
		||||
      <div className="px-6 space-y-6">
 | 
			
		||||
        <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">
 | 
			
		||||
            <SiteSelector
 | 
			
		||||
              selectedSite={selectedSite}
 | 
			
		||||
@ -183,22 +214,37 @@ const AdminDashboard = () => {
 | 
			
		||||
              lastSyncTimestamp={currentSiteDetails.lastSyncTimestamp}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div>
 | 
			
		||||
          <KPI_Table siteId={siteIdMap[selectedSite]} month={currentMonth} />
 | 
			
		||||
        </div>
 | 
			
		||||
         {/* TOP 3 CARDS */}
 | 
			
		||||
          <div className="space-y-4">
 | 
			
		||||
            <KpiTop
 | 
			
		||||
              yieldKwh={yieldKwh}
 | 
			
		||||
              consumptionKwh={consumptionKwh}
 | 
			
		||||
              gridDrawKwh={gridDrawKwh}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 
 | 
			
		||||
        <div className="grid md:grid-cols-2 gap-6 lg:flex-col justify-center">
 | 
			
		||||
        <div ref={energyChartRef} className="pb-5">
 | 
			
		||||
            <EnergyLineChart siteId={siteIdMap[selectedSite]} />
 | 
			
		||||
 | 
			
		||||
        </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]} />
 | 
			
		||||
          </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">
 | 
			
		||||
          <button onClick={handlePDFExport} className="text-sm lg:text-lg btn-primary">
 | 
			
		||||
            Export Chart Images to PDF
 | 
			
		||||
 | 
			
		||||
@ -48,3 +48,29 @@ export async function fetchForecast(
 | 
			
		||||
  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';
 | 
			
		||||
 | 
			
		||||
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 = {
 | 
			
		||||
    labels: filteredLabels.map(formatLabel),
 | 
			
		||||
@ -231,26 +245,29 @@ const axisColor = isDark ? '#fff' : '#222';
 | 
			
		||||
      {
 | 
			
		||||
        label: 'Consumption',
 | 
			
		||||
        data: filteredConsumption,
 | 
			
		||||
        borderColor: '#8884d8',
 | 
			
		||||
        borderColor: consumptionColor,
 | 
			
		||||
        backgroundColor: (ctx: any) => areaGradient(ctx, consumptionColor),
 | 
			
		||||
        fill: true,                                   // <-- fill under line
 | 
			
		||||
        tension: 0.4,
 | 
			
		||||
        fill: false,
 | 
			
		||||
        spanGaps: true,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: 'Generation',
 | 
			
		||||
        data: filteredGeneration,
 | 
			
		||||
        borderColor: '#82ca9d',
 | 
			
		||||
        borderColor: generationColor,
 | 
			
		||||
        backgroundColor: (ctx: any) => areaGradient(ctx, generationColor),
 | 
			
		||||
        fill: true,                                   // <-- fill under line
 | 
			
		||||
        tension: 0.4,
 | 
			
		||||
        fill: false,
 | 
			
		||||
        spanGaps: true,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
      label: 'Forecasted Solar',
 | 
			
		||||
      data: filteredForecast,
 | 
			
		||||
      borderColor: '#ffa500', // orange
 | 
			
		||||
      borderColor: '#fcd913', // orange
 | 
			
		||||
      backgroundColor: (ctx: any) => areaGradient(ctx, '#fcd913', 0.18, 0.03),
 | 
			
		||||
      tension: 0.4,
 | 
			
		||||
      borderDash: [5, 5], // dashed line to distinguish forecast
 | 
			
		||||
      fill: false,
 | 
			
		||||
      fill: true,
 | 
			
		||||
      spanGaps: true,
 | 
			
		||||
    }
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import React, { useEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface KPI_TableProps {
 | 
			
		||||
  siteId: string;
 | 
			
		||||
@ -12,8 +12,8 @@ interface MonthlyKPI {
 | 
			
		||||
  consumption_kwh: number | null;
 | 
			
		||||
  grid_draw_kwh: number | null;
 | 
			
		||||
  efficiency: number | null;
 | 
			
		||||
  peak_demand_kw: number | null; // ✅ new
 | 
			
		||||
  avg_power_factor: number | null; // ✅ new
 | 
			
		||||
  peak_demand_kw: number | null;
 | 
			
		||||
  avg_power_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);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!siteId || !month) return;
 | 
			
		||||
 | 
			
		||||
    const fetchKPI = async () => {
 | 
			
		||||
      setLoading(true);
 | 
			
		||||
      try {
 | 
			
		||||
        const res = await fetch(`http://localhost:8000/kpi/monthly?site=${siteId}&month=${month}`);
 | 
			
		||||
        const data = await res.json();
 | 
			
		||||
        setKpiData(data);
 | 
			
		||||
        const res = await fetch(
 | 
			
		||||
          `http://localhost:8000/kpi/monthly?site=${siteId}&month=${month}`
 | 
			
		||||
        );
 | 
			
		||||
        setKpiData(await res.json());
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        console.error('Failed to fetch KPI:', err);
 | 
			
		||||
        setKpiData(null); // fallback
 | 
			
		||||
        console.error("Failed to fetch KPI:", err);
 | 
			
		||||
        setKpiData(null);
 | 
			
		||||
      } finally {
 | 
			
		||||
        setLoading(false);
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (siteId && month) fetchKPI();
 | 
			
		||||
    fetchKPI();
 | 
			
		||||
  }, [siteId, month]);
 | 
			
		||||
 | 
			
		||||
  if (!siteId) {
 | 
			
		||||
    return (
 | 
			
		||||
      <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>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  const formatValue = (value: number | null, unit = "", decimals = 2) =>
 | 
			
		||||
    value != null ? `${value.toFixed(decimals)}${unit}` : "—";
 | 
			
		||||
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return (
 | 
			
		||||
      <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>Loading...</p>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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
 | 
			
		||||
];
 | 
			
		||||
  const rows = [
 | 
			
		||||
    { label: "Monthly Yield", value: formatValue(kpiData?.yield_kwh ?? null, " kWh", 0) },
 | 
			
		||||
    { label: "Monthly Consumption", value: formatValue(kpiData?.consumption_kwh ?? null, " kWh", 0) },
 | 
			
		||||
    { label: "Monthly Grid Draw", value: formatValue(kpiData?.grid_draw_kwh ?? null, " kWh", 0) },
 | 
			
		||||
    { label: "Efficiency", value: formatValue(kpiData?.efficiency ?? null, "%", 1) },
 | 
			
		||||
    { label: "Peak Demand", value: formatValue(kpiData?.peak_demand_kw ?? null, " kW") },
 | 
			
		||||
    { label: "Power Factor", value: formatValue(kpiData?.avg_power_factor ?? null) },
 | 
			
		||||
    { label: "Load Factor", value: formatValue(kpiData?.load_factor ?? null) },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <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>
 | 
			
		||||
          <tr className="bg-rtgray-100 dark:bg-rtgray-800 text-black dark:text-white">
 | 
			
		||||
            <th className="border border-rtgray-300 dark:border-rtgray-700 p-3 text-left dark:bg-rtgray-900">KPI</th>
 | 
			
		||||
            <th className="border border-rtgray-300 dark:border-rtgray-700 p-3 text-left dark:bg-rtgray-900">Value</th>
 | 
			
		||||
              <tr className="bg-gray-100 dark:bg-rtgray-800">
 | 
			
		||||
                <th className="border p-3 text-left dark:text-white">KPI</th>
 | 
			
		||||
                <th className="border p-3 text-left dark:text-white">Value</th>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
          {data.map((row) => (
 | 
			
		||||
            <tr key={row.kpi} className="even:bg-rtgray-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 border-rtgray-300 dark:border-rtgray-700 p-2.5">{row.value}</td>
 | 
			
		||||
              {rows.map((row) => (
 | 
			
		||||
                <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>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -106,3 +89,4 @@ const data = [
 | 
			
		||||
export default KPI_Table;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -126,9 +126,9 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
 | 
			
		||||
 | 
			
		||||
  if (loading || !siteId || chartData.length === 0) {
 | 
			
		||||
    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">
 | 
			
		||||
          <h2 className="text-lg font-bold pb-3">Monthly Energy Yield</h2>
 | 
			
		||||
          <h2 className="text-lg font-bold">Monthly Energy Yield</h2>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="h-96 w-full flex items-center justify-center">
 | 
			
		||||
          <p className="text-white/70">
 | 
			
		||||
@ -140,14 +140,10 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="bg-white p-4 rounded-lg shadow-md dark:bg-rtgray-800 dark:text-white-light">
 | 
			
		||||
      <div className="flex justify-between items-center mb-2">
 | 
			
		||||
        <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">
 | 
			
		||||
    <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}>
 | 
			
		||||
          <BarChart data={chartData} >
 | 
			
		||||
            <XAxis
 | 
			
		||||
              dataKey="month"
 | 
			
		||||
              tick={{ fontSize: 10, fill: isDark ? '#fff' : '#222' }}
 | 
			
		||||
@ -158,6 +154,16 @@ const generationColor = isDark ? '#fcd913' : '#669bbc';
 | 
			
		||||
              tick={{ fontSize: 10, fill: isDark ? '#fff' : '#222' }}
 | 
			
		||||
              axisLine={{ 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
 | 
			
		||||
              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