admin dashboard progress 2
This commit is contained in:
parent
2a6807cc8f
commit
29509f5bd1
@ -5,6 +5,7 @@ import { useSelector } from 'react-redux';
|
||||
import { IRootState } from '@/store';
|
||||
import Sidebar from '@/components/layouts/sidebar';
|
||||
import Header from '@/components/layouts/header'; // Correctly import the consolidated Header
|
||||
import Footer from '@/components/layouts/footer';
|
||||
|
||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const themeConfig = useSelector((state: IRootState) => state.themeConfig);
|
||||
@ -18,11 +19,13 @@ const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
<div className="main-content">
|
||||
{/* This is where your page content will be injected */}
|
||||
<div className="p-6 space-y-6 min-h-screen">
|
||||
<div className="p-6 space-y-4 min-h-screen">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<Footer/>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -6,7 +6,6 @@ import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import SiteSelector from '@/components/dashboards/SiteSelector';
|
||||
import SiteStatus from '@/components/dashboards/SiteStatus';
|
||||
import SystemOverview from '@/components/dashboards/SystemOverview'; // Import the new component
|
||||
import EnergyLineChart from '@/components/dashboards/EnergyLineChart';
|
||||
import KPI_Table from '@/components/dashboards/KPIStatus';
|
||||
import MonthlyBarChart from '@/components/dashboards/MonthlyBarChart';
|
||||
@ -75,38 +74,30 @@ const AdminDashboard = () => {
|
||||
</div>
|
||||
{/* Second Column: KPI Table */}
|
||||
<div> {/* This div will now be the second column */}
|
||||
<KPI_Table />
|
||||
<KPI_Table siteData={currentSiteDetails} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Overview - Now covers the whole width */}
|
||||
<SystemOverview
|
||||
status={currentSiteDetails.systemStatus}
|
||||
temperature={currentSiteDetails.temperature}
|
||||
solarPower={currentSiteDetails.solarPower}
|
||||
realTimePower={currentSiteDetails.realTimePower}
|
||||
installedPower={currentSiteDetails.installedPower}
|
||||
/>
|
||||
|
||||
|
||||
{/* Charts Section */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="pb-16">
|
||||
<div className="pb-5">
|
||||
<EnergyLineChart
|
||||
consumptionData={currentSiteDetails.consumptionData}
|
||||
generationData={currentSiteDetails.generationData}
|
||||
/>
|
||||
</div>
|
||||
<div className="pb-16">
|
||||
<MonthlyBarChart />
|
||||
<div className="pb-5">
|
||||
<MonthlyBarChart siteData={currentSiteDetails} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button onClick={handleCSVExport} className="btn-primary">
|
||||
<div className="flex flex-col md:flex-row gap-4 justify-center"> {/* Added a div wrapper */}
|
||||
<button onClick={handleCSVExport} className="text-sm lg:text-lg btn-primary">
|
||||
Export Raw Data to CSV
|
||||
</button>
|
||||
<button onClick={handlePDFExport} className="btn-primary">
|
||||
<button onClick={handlePDFExport} className="text-sm lg:text-lg btn-primary">
|
||||
Export Chart Images to PDF
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
// components/dashboards/EnergyLineChart.tsx
|
||||
'use client';
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
@ -40,6 +40,11 @@ const labels = [
|
||||
|
||||
const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartProps) => {
|
||||
const chartRef = useRef<any>(null);
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
import('hammerjs');
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Calculate suggestedMax dynamically based on the current data
|
||||
const allDataPoints = [...consumptionData, ...generationData];
|
||||
@ -108,17 +113,17 @@ const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartPro
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow-md dark:bg-gray-800 dark:text-white-light">
|
||||
<div className="h-96 w-full">
|
||||
<div className="h-98 w-full">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-lg font-bold dark:text-white-light">Power/Energy Consumption & Generation</h2>
|
||||
<h2 className="text-lg font-bold dark:text-white-light">Energy Consumption & Generation</h2>
|
||||
<button
|
||||
onClick={handleResetZoom}
|
||||
className="btn-primary text-sm"
|
||||
className="btn-primary px-8 py-2 text-sm"
|
||||
>
|
||||
Reset Zoom
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-full w-full">
|
||||
<div className="h-96 w-full">
|
||||
<Line ref={chartRef} data={data} options={options} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,33 +1,82 @@
|
||||
const KPI_Table = () => {
|
||||
const data = [
|
||||
{ kpi: 'Monthly Yield', value: '22000 kWh' },
|
||||
{ kpi: 'Monthly Consumption', value: '24000 kWh' },
|
||||
{ kpi: 'Monthly Grid Draw', value: '2000 kWh' },
|
||||
{ kpi: 'Efficiency', value: '87%' },
|
||||
{ kpi: 'Monthly Saved', value: 'RM 200' },
|
||||
];
|
||||
// components/KPI_Table.tsx
|
||||
import React from 'react';
|
||||
import { SiteDetails } from '@/types/SiteData'; // Adjust import path as necessary
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-2">Monthly KPI</h2>
|
||||
<table className="w-full border-collapse border">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border p-2 text-left">KPI</th>
|
||||
<th className="border p-2 text-left">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row) => (
|
||||
<tr key={row.kpi}>
|
||||
<td className="border p-2">{row.kpi}</td>
|
||||
<td className="border p-2">{row.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
interface KPI_TableProps {
|
||||
siteData: SiteDetails | null; // Pass the selected site's data as a prop
|
||||
}
|
||||
|
||||
const KPI_Table: React.FC<KPI_TableProps> = ({ siteData }) => {
|
||||
|
||||
if (!siteData) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-2">Monthly KPI</h2>
|
||||
<p className="text-white/70">Select a site to view KPIs.</p>
|
||||
<div className="min-h-[275px] w-full flex items-center justify-center border">
|
||||
<p>No data available</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- KPI Calculations ---
|
||||
const monthlyYield = siteData.generationData.reduce((sum, daily) => sum + daily, 0);
|
||||
const monthlyConsumption = siteData.consumptionData.reduce((sum, daily) => sum + daily, 0);
|
||||
|
||||
// Calculate Grid Draw/Export
|
||||
let monthlyGridDraw = 0;
|
||||
let monthlySolarExport = 0;
|
||||
|
||||
// A simplistic model for grid draw/export:
|
||||
// If generation > consumption, excess is exported.
|
||||
// If consumption > generation, deficit is drawn from grid.
|
||||
// Note: This is highly simplified. Real-world calculations involve time-of-use, battery storage, etc.
|
||||
const netEnergy = monthlyYield - monthlyConsumption;
|
||||
if (netEnergy > 0) {
|
||||
monthlySolarExport = netEnergy;
|
||||
} else {
|
||||
monthlyGridDraw = Math.abs(netEnergy);
|
||||
}
|
||||
|
||||
const selfConsumption = Math.min(monthlyYield, monthlyConsumption);
|
||||
const savingsFromSelfConsumption = selfConsumption * siteData.gridImportPrice_RM_per_kWh;
|
||||
const revenueFromExport = monthlySolarExport * siteData.solarExportTariff_RM_per_kWh;
|
||||
const monthlySaved = savingsFromSelfConsumption + revenueFromExport;
|
||||
const efficiency = siteData.theoreticalMaxGeneration_kWh && siteData.theoreticalMaxGeneration_kWh > 0
|
||||
? (monthlyYield / siteData.theoreticalMaxGeneration_kWh) * 100
|
||||
: 0; // Default to 0 or 'N/A' if theoretical max is not set or zero
|
||||
|
||||
const data = [
|
||||
{ kpi: 'Monthly Yield', value: `${monthlyYield.toFixed(0)} kWh` },
|
||||
{ kpi: 'Monthly Consumption', value: `${monthlyConsumption.toFixed(0)} kWh` },
|
||||
{ kpi: 'Monthly Grid Draw', value: `${monthlyGridDraw.toFixed(0)} kWh` },
|
||||
// { kpi: 'Monthly Solar Export', value: `${monthlySolarExport.toFixed(0)} kWh` }, // Can add this if desired
|
||||
{ kpi: 'Efficiency', value: `${efficiency.toFixed(1)}%` },
|
||||
{ kpi: 'Monthly Saved', value: `RM ${monthlySaved.toFixed(2)}` },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-2">Monthly KPI</h2>
|
||||
<table className="min-h-[275px] w-full border-collapse border border-gray-700 text-black bg-white">
|
||||
<thead>
|
||||
<tr className="bg-gray-800">
|
||||
<th className="border border-gray-700 p-3 text-left">KPI</th>
|
||||
<th className="border border-gray-700 p-3 text-left">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row) => (
|
||||
<tr key={row.kpi} className="">
|
||||
<td className="border border-gray-700 p-3">{row.kpi}</td>
|
||||
<td className="border border-gray-700 p-3">{row.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KPI_Table;
|
||||
|
@ -1,48 +1,88 @@
|
||||
// components/MonthlyBarChart.tsx
|
||||
import React from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Cell
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
Legend // Import Legend to distinguish between consumption and generation
|
||||
} from 'recharts';
|
||||
import { SiteDetails } from '@/types/SiteData'; // Adjust import path as necessary
|
||||
|
||||
const monthlyData = [
|
||||
{ month: 'January', usage: 3000 },
|
||||
{ month: 'February', usage: 3200 },
|
||||
{ month: 'March', usage: 2800 },
|
||||
{ month: 'April', usage: 3100 },
|
||||
{ month: 'May', usage: 3300 },
|
||||
{ month: 'June', usage: 3500 },
|
||||
{ month: 'July', usage: 3400 },
|
||||
{ month: 'August', usage: 3600 },
|
||||
{ month: 'September', usage: 3200 },
|
||||
{ month: 'October', usage: 3000 },
|
||||
{ month: 'November', usage: 2900 },
|
||||
{ month: 'December', usage: 3100 },
|
||||
];
|
||||
interface MonthlyBarChartProps {
|
||||
siteData: SiteDetails | null; // Pass the selected site's data as a prop
|
||||
}
|
||||
|
||||
const barColors = ['#003049', '#669bbc']; // Alternate between yellow tones
|
||||
// Define specific colors for consumption and generation
|
||||
const consumptionColor = '#003049'; // Darker blue/grey for consumption
|
||||
const generationColor = '#669bbc'; // Lighter blue for generation
|
||||
|
||||
const MonthlyBarChart = () => {
|
||||
return (
|
||||
<div className="h-80">
|
||||
<h2 className="text-lg font-bold mb-2">Monthly Energy Consumption</h2>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={monthlyData}>
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="usage">
|
||||
{monthlyData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={barColors[index % barColors.length]} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
const MonthlyBarChart: React.FC<MonthlyBarChartProps> = ({ siteData }) => {
|
||||
|
||||
// Prepare data for the chart
|
||||
const chartData = React.useMemo(() => {
|
||||
if (!siteData || siteData.consumptionData.length === 0 || siteData.generationData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Assuming consumptionData and generationData are arrays of daily values
|
||||
// We'll map them to an array suitable for Recharts
|
||||
const dataLength = Math.min(siteData.consumptionData.length, siteData.generationData.length);
|
||||
return Array.from({ length: dataLength }).map((_, index) => ({
|
||||
day: `Day ${index + 1}`, // Label for X-axis
|
||||
consumption: siteData.consumptionData[index],
|
||||
generation: siteData.generationData[index],
|
||||
}));
|
||||
}, [siteData]);
|
||||
|
||||
if (!siteData || chartData.length === 0) {
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow-md dark:bg-gray-800 dark:text-white-light">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-lg font-bold pb-3">Daily Energy Consumption & Generation</h2>
|
||||
</div>
|
||||
<div className="h-96 w-full flex items-center justify-center">
|
||||
<p className="text-white/70">No data available for chart. Please select a site.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow-md dark:bg-gray-800 dark:text-white-light">
|
||||
{/* Chart Title and any other header elements */}
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-lg font-bold pb-3">Daily Energy Consumption & Generation</h2>
|
||||
{/* You could add buttons or other controls here if needed */}
|
||||
</div>
|
||||
|
||||
{/* This div now acts as the direct container for the chart */}
|
||||
{/* It explicitly sets the height and width for the ResponsiveContainer */}
|
||||
<div className="h-96 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData}>
|
||||
<XAxis dataKey="day" interval={chartData.length > 10 ? 'preserveStartEnd' : 0} /> {/* Adjust interval for readability */}
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend /> {/* Add Legend for consumption and generation bars */}
|
||||
|
||||
{/* Bar for Consumption */}
|
||||
<Bar dataKey="consumption" fill={consumptionColor} name="Consumption (kWh)">
|
||||
{/* You can still apply individual cell colors if needed, but a single fill is often clearer for categories */}
|
||||
</Bar>
|
||||
|
||||
{/* Bar for Generation */}
|
||||
<Bar dataKey="generation" fill={generationColor} name="Generation (kWh)">
|
||||
{/* You can still apply individual cell colors if needed */}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonthlyBarChart;
|
||||
export default MonthlyBarChart;
|
@ -1,137 +0,0 @@
|
||||
// components/dashboards/SystemOverview.tsx
|
||||
'use client'; // This component will contain client-side interactivity and use browser APIs
|
||||
import React from 'react';
|
||||
import Image from 'next/image'; // Import Next.js Image component for optimized images
|
||||
|
||||
// You'll need actual SVG/PNG images for these.
|
||||
// For demonstration, I'll use placeholders. You'll replace these paths.
|
||||
import solarPanelIcon from '@/public/images/solarpanel.png'; // Example path
|
||||
import houseIcon from '@/public/images/building.png'; // Example path
|
||||
import gridTowerIcon from '@/public/images/gridtower.png'; // Example path
|
||||
interface SystemOverviewProps {
|
||||
status: string; // e.g., "Normal", "Faulty"
|
||||
temperature: string; // e.g., "35°C"
|
||||
solarPower: number; // Power generated by solar (kW)
|
||||
realTimePower: number; // Real-time power used (kW)
|
||||
installedPower: number; // Installed capacity (kWp)
|
||||
}
|
||||
|
||||
const SystemOverview: React.FC<SystemOverviewProps> = ({
|
||||
status,
|
||||
temperature,
|
||||
solarPower,
|
||||
realTimePower,
|
||||
installedPower,
|
||||
}) => {
|
||||
const statusColor = status === 'Normal' ? 'text-green-500' : 'text-red-500';
|
||||
|
||||
// Increased icon size for better visibility, adjust as needed
|
||||
const iconBaseSize = 100; // Original was 80, let's use 100-120 range.
|
||||
const solarIconSize = iconBaseSize * 1.2; // Solar panel slightly larger
|
||||
const otherIconSize = iconBaseSize * 1.1; // House and Grid slightly larger
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-lg shadow-md dark:bg-gray-800 dark:text-white-light col-span-full md:col-span-1 flex flex-col">
|
||||
{/* Top Status & Temperature Info */}
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<p className="text-gray-600 dark:text-gray-400 font-medium">Status:</p>
|
||||
<span className={`font-bold ${statusColor}`}>
|
||||
{/* Display checkmark only for 'Normal' status */}
|
||||
{status === 'Normal'}
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">{temperature}</p>
|
||||
|
||||
{/* Main Visual Section for Icons, SVG Lines, and Power Metrics */}
|
||||
{/* This container will control the overall aspect ratio and height of the visual elements */}
|
||||
<div className="relative flex-grow flex items-center justify-center min-h-[300px] sm:min-h-[350px] md:min-h-[400px] lg:min-h-[450px]">
|
||||
|
||||
{/* SVG for drawing the connecting lines */}
|
||||
{/* viewBox="0 0 1000 500" gives a 2:1 aspect ratio for internal coordinates. */}
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full"
|
||||
viewBox="0 0 1000 500"
|
||||
preserveAspectRatio="xMidYMid meet" // Scales proportionally within its container
|
||||
>
|
||||
<defs>
|
||||
{/* Define a reusable arrowhead marker */}
|
||||
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="0" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#60A5FA" /> {/* Fill with blue-400 color */}
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
{/* Path 1: Solar Panel (top-center) to House (top-right) */}
|
||||
{/* This path roughly follows the curved line from your image */}
|
||||
{/* M: moveto, C: cubic Bezier curve (x1 y1, x2 y2, x y) */}
|
||||
<path
|
||||
d="M 500 130 C 450 180, 200 250, 270 340"
|
||||
fill="none"
|
||||
stroke="#60A5FA" // Tailwind blue-400
|
||||
strokeWidth="4"
|
||||
// Creates a dashed line (8px dash, 4px gap)
|
||||
markerEnd="url(#arrowhead)" // Attach the arrowhead
|
||||
/>
|
||||
|
||||
{/* Path 2: House (mid-right) to Grid Tower (mid-left) */}
|
||||
{/* A relatively straight line between the house and grid tower */}
|
||||
<path
|
||||
d="M 290 395 L 700 395"
|
||||
fill="none"
|
||||
stroke="#60A5FA"
|
||||
strokeWidth="4"
|
||||
|
||||
markerEnd="url(#arrowhead)"
|
||||
/>
|
||||
|
||||
{/* Path 3: Grid Tower (top-left) to Solar Panel (top-left) */}
|
||||
{/* This path roughly connects the grid to the solar panel */}
|
||||
<path
|
||||
d="M 730 340 C 780 250, 550 150, 500 130"
|
||||
fill="none"
|
||||
stroke="#60A5FA"
|
||||
strokeWidth="4"
|
||||
|
||||
markerEnd="url(#arrowhead)"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* --- Icons (Images) --- */}
|
||||
{/* Positioned using absolute CSS with percentages and translate for centering */}
|
||||
{/* Solar Panel Icon */}
|
||||
<div className="absolute flex flex-col items-center z-10"
|
||||
style={{ top: '5%', left: '50%', transform: 'translateX(-50%)' }}>
|
||||
<Image src={solarPanelIcon} alt="Solar Panels" width={solarIconSize} height={solarIconSize} />
|
||||
<p className="mt-2 text-lg font-semibold dark:text-white-light">{solarPower} kW</p>
|
||||
</div>
|
||||
|
||||
{/* House Icon */}
|
||||
<div className="absolute flex flex-col items-center z-10"
|
||||
style={{ bottom: '5%', left: '25%', transform: 'translateX(-50%)' }}>
|
||||
<Image src={houseIcon} alt="House" width={otherIconSize} height={otherIconSize} />
|
||||
</div>
|
||||
|
||||
{/* Grid Tower Icon */}
|
||||
<div className="absolute flex flex-col items-center z-10"
|
||||
style={{ bottom: '5%', right: '20%', transform: 'translateX(50%)' }}>
|
||||
<Image src={gridTowerIcon} alt="Grid Tower" width={otherIconSize} height={otherIconSize} />
|
||||
</div>
|
||||
|
||||
{/* --- Power Metrics Texts --- */}
|
||||
{/* Positioned relative to the main visual container */}
|
||||
<div className="absolute text-right z-10"
|
||||
style={{ top: '25%', right: '5%' }}>
|
||||
<p className="text-gray-600 dark:text-gray-400">Real-time power</p>
|
||||
<p className="text-2xl font-bold text-primary-500">{realTimePower} kW</p>
|
||||
</div>
|
||||
<div className="absolute text-right z-10"
|
||||
style={{ top: '55%', right: '5%' }}>
|
||||
<p className="text-gray-600 dark:text-gray-400">Installed power</p>
|
||||
<p className="text-2xl font-bold text-primary-500">{installedPower} kWp</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemOverview;
|
@ -1,6 +1,54 @@
|
||||
import React from 'react';
|
||||
|
||||
const Footer = () => {
|
||||
const socialLinks = {
|
||||
instagram: 'https://www.instagram.com/rooftop.my/',
|
||||
linkedin: 'https://my.linkedin.com/company/rooftop-my?trk=public_jobs_topcard_logo',
|
||||
facebook: 'https://www.facebook.com/profile.php?id=61572728757164',
|
||||
whatsapp: 'https://wa.me/message/XFIYMAVF27EBE1',
|
||||
email: 'mailto:sales@rooftop.my',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 pt-0 mt-auto text-center dark:text-white-dark ltr:sm:text-left rtl:sm:text-right">© {new Date().getFullYear()}. Rooftop Energy All rights reserved.</div>
|
||||
<footer className="bg-rtyellow-500 text-black p-6 scale-100">
|
||||
{/* Social Links */}
|
||||
<div className="flex justify-center space-x-6 mb-6">
|
||||
{Object.entries(socialLinks).map(([platform, url]) => (
|
||||
<a
|
||||
key={platform}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 bg-rtyellow-600 flex items-center justify-center rounded-full transition-colors duration-300 hover:bg-white"
|
||||
>
|
||||
<img
|
||||
src={platform === 'email' ? '/mailicon.svg' : `/${platform}.svg`}
|
||||
alt={platform.charAt(0).toUpperCase() + platform.slice(1)}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="w-3/4 mx-auto border-t-2 border-rtyellow-600 my-6"></div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="text-left w-full max-w-md mx-auto mb-7">
|
||||
<p className="text-[17px] text-rtbrown-800">Rooftop Energy Tech Sdn Bhd</p>
|
||||
<p className="text-[17px] text-rtbrown-800">202501013544 (1613958-P)</p>
|
||||
<p className="text-[17px] text-rtbrown-800 md:whitespace-nowrap">
|
||||
3-5, Block D2, Dataran Prima,<br className="md:hidden" />
|
||||
47301 Petaling Jaya,<br className="md:hidden" />
|
||||
Selangor, Malaysia
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<p className="text-black text-s mt-6 text-center">Rooftop Energy © 2025</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
|
3
public/facebook.svg
Normal file
3
public/facebook.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.25 1.5H3.75C3.15326 1.5 2.58097 1.73705 2.15901 2.15901C1.73705 2.58097 1.5 3.15326 1.5 3.75L1.5 20.25C1.5 20.8467 1.73705 21.419 2.15901 21.841C2.58097 22.2629 3.15326 22.5 3.75 22.5H10.1836V15.3605H7.23047V12H10.1836V9.43875C10.1836 6.52547 11.918 4.91625 14.5744 4.91625C15.8466 4.91625 17.1769 5.14313 17.1769 5.14313V8.0025H15.7111C14.2669 8.0025 13.8164 8.89875 13.8164 9.81797V12H17.0405L16.5248 15.3605H13.8164V22.5H20.25C20.8467 22.5 21.419 22.2629 21.841 21.841C22.2629 21.419 22.5 20.8467 22.5 20.25V3.75C22.5 3.15326 22.2629 2.58097 21.841 2.15901C21.419 1.73705 20.8467 1.5 20.25 1.5Z" fill="#331E00"/>
|
||||
</svg>
|
After Width: | Height: | Size: 732 B |
4
public/instagram.svg
Normal file
4
public/instagram.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="28" height="30" viewBox="0 0 28 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24.4382 10.5347C24.4265 9.61957 24.2611 8.7135 23.9494 7.85706C23.6792 7.13466 23.2664 6.47859 22.7374 5.93077C22.2085 5.38296 21.5751 4.95543 20.8776 4.67552C20.0613 4.35816 19.199 4.18656 18.3272 4.16802C17.2049 4.11606 16.8491 4.10156 14.0001 4.10156C11.1511 4.10156 10.7859 4.10156 9.67175 4.16802C8.80043 4.18669 7.93849 4.35829 7.12258 4.67552C6.42498 4.95524 5.79144 5.38269 5.26249 5.93054C4.73353 6.47838 4.32082 7.13455 4.05075 7.85706C3.74372 8.70185 3.57839 9.59478 3.56192 10.4973C3.51175 11.6609 3.49658 12.0294 3.49658 14.9802C3.49658 17.9309 3.49658 18.3079 3.56192 19.4631C3.57942 20.3669 3.74392 21.2587 4.05075 22.1057C4.32127 22.828 4.73429 23.4839 5.26342 24.0315C5.79255 24.5791 6.42616 25.0064 7.12375 25.2861C7.93742 25.6162 8.79952 25.8001 9.67292 25.8298C10.7964 25.8818 11.1522 25.8975 14.0012 25.8975C16.8502 25.8975 17.2154 25.8975 18.3296 25.8298C19.2013 25.812 20.0637 25.6408 20.8799 25.3235C21.5772 25.0433 22.2105 24.6156 22.7394 24.0679C23.2683 23.5201 23.6812 22.8642 23.9517 22.142C24.2586 21.2961 24.4231 20.4044 24.4406 19.4994C24.4907 18.3369 24.5059 17.9684 24.5059 15.0164C24.5036 12.0657 24.5036 11.6911 24.4382 10.5347ZM13.9931 20.5603C11.0134 20.5603 8.59958 18.0602 8.59958 14.9741C8.59958 11.8881 11.0134 9.38802 13.9931 9.38802C15.4235 9.38802 16.7954 9.97656 17.8069 11.0242C18.8183 12.0718 19.3866 13.4926 19.3866 14.9741C19.3866 16.4557 18.8183 17.8765 17.8069 18.9241C16.7954 19.9717 15.4235 20.5603 13.9931 20.5603ZM19.6012 10.484C19.436 10.4841 19.2724 10.4506 19.1198 10.3851C18.9671 10.3197 18.8284 10.2238 18.7116 10.1028C18.5948 9.98183 18.5022 9.83817 18.439 9.68006C18.3759 9.52195 18.3434 9.3525 18.3436 9.1814C18.3436 9.01042 18.3761 8.84111 18.4393 8.68315C18.5024 8.52519 18.595 8.38166 18.7118 8.26076C18.8285 8.13986 18.9671 8.04395 19.1196 7.97852C19.2721 7.91309 19.4356 7.87942 19.6007 7.87942C19.7657 7.87942 19.9292 7.91309 20.0817 7.97852C20.2342 8.04395 20.3728 8.13986 20.4896 8.26076C20.6063 8.38166 20.6989 8.52519 20.7621 8.68315C20.8252 8.84111 20.8577 9.01042 20.8577 9.1814C20.8577 9.90156 20.2954 10.484 19.6012 10.484Z" fill="#331E00"/>
|
||||
<path d="M13.9928 18.603C15.9277 18.603 17.4963 16.9784 17.4963 14.9743C17.4963 12.9703 15.9277 11.3457 13.9928 11.3457C12.0578 11.3457 10.4893 12.9703 10.4893 14.9743C10.4893 16.9784 12.0578 18.603 13.9928 18.603Z" fill="#331E00"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
3
public/linkedin.svg
Normal file
3
public/linkedin.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 1.5H2.99531C2.17031 1.5 1.5 2.17969 1.5 3.01406V20.9859C1.5 21.8203 2.17031 22.5 2.99531 22.5H21C21.825 22.5 22.5 21.8203 22.5 20.9859V3.01406C22.5 2.17969 21.825 1.5 21 1.5ZM7.84687 19.5H4.73438V9.47812H7.85156V19.5H7.84687ZM6.29062 8.10938C5.29219 8.10938 4.48594 7.29844 4.48594 6.30469C4.48594 5.31094 5.29219 4.5 6.29062 4.5C7.28437 4.5 8.09531 5.31094 8.09531 6.30469C8.09531 7.30312 7.28906 8.10938 6.29062 8.10938ZM19.5141 19.5H16.4016V14.625C16.4016 13.4625 16.3781 11.9672 14.7844 11.9672C13.1625 11.9672 12.9141 13.2328 12.9141 14.5406V19.5H9.80156V9.47812H12.7875V10.8469H12.8297C13.2469 10.0594 14.2641 9.22969 15.7781 9.22969C18.9281 9.22969 19.5141 11.3062 19.5141 14.0062V19.5Z" fill="#331E00"/>
|
||||
</svg>
|
After Width: | Height: | Size: 828 B |
4
public/mailicon.svg
Normal file
4
public/mailicon.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4H20C21.1 4 22 4.9 22 6V18C22 19.1 21.1 20 20 20H4C2.9 20 2 19.1 2 18V6C2 4.9 2.9 4 4 4Z" stroke="#331E00" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M22 6L12 13L2 6" stroke="#331E00" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 401 B |
3
public/whatsapp.svg
Normal file
3
public/whatsapp.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.3547 4.55156C17.3906 2.58281 14.775 1.5 11.9953 1.5C6.25781 1.5 1.58906 6.16875 1.58906 11.9062C1.58906 13.7391 2.06719 15.5297 2.97656 17.1094L1.5 22.5L7.01719 21.0516C8.53594 21.8813 10.2469 22.3172 11.9906 22.3172H11.9953C17.7281 22.3172 22.5 17.6484 22.5 11.9109C22.5 9.13125 21.3188 6.52031 19.3547 4.55156ZM11.9953 20.5641C10.4391 20.5641 8.91562 20.1469 7.58906 19.3594L7.275 19.1719L4.00313 20.0297L4.875 16.8375L4.66875 16.5094C3.80156 15.1313 3.34688 13.5422 3.34688 11.9062C3.34688 7.13906 7.22812 3.25781 12 3.25781C14.3109 3.25781 16.4812 4.15781 18.1125 5.79375C19.7437 7.42969 20.7469 9.6 20.7422 11.9109C20.7422 16.6828 16.7625 20.5641 11.9953 20.5641ZM16.7391 14.0859C16.4813 13.9547 15.2016 13.3266 14.9625 13.2422C14.7234 13.1531 14.55 13.1109 14.3766 13.3734C14.2031 13.6359 13.7063 14.2172 13.5516 14.3953C13.4016 14.5688 13.2469 14.5922 12.9891 14.4609C11.4609 13.6969 10.4578 13.0969 9.45 11.3672C9.18281 10.9078 9.71719 10.9406 10.2141 9.94687C10.2984 9.77344 10.2562 9.62344 10.1906 9.49219C10.125 9.36094 9.60469 8.08125 9.38906 7.56094C9.17813 7.05469 8.9625 7.125 8.80313 7.11563C8.65313 7.10625 8.47969 7.10625 8.30625 7.10625C8.13281 7.10625 7.85156 7.17188 7.6125 7.42969C7.37344 7.69219 6.70312 8.32031 6.70312 9.6C6.70312 10.8797 7.63594 12.1172 7.7625 12.2906C7.89375 12.4641 9.59531 15.0891 12.2062 16.2188C13.8562 16.9312 14.5031 16.9922 15.3281 16.8703C15.8297 16.7953 16.8656 16.2422 17.0812 15.6328C17.2969 15.0234 17.2969 14.5031 17.2313 14.3953C17.1703 14.2781 16.9969 14.2125 16.7391 14.0859Z" fill="#331E00"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
@ -6,29 +6,51 @@ export interface SiteDetails {
|
||||
inverterProvider: string;
|
||||
emergencyContact: string;
|
||||
lastSyncTimestamp: string;
|
||||
consumptionData: number[];
|
||||
generationData: number[];
|
||||
// New properties for SystemOverview
|
||||
consumptionData: number[]; // e.g., Daily consumption in kWh
|
||||
generationData: number[]; // e.g., Daily generation in kWh
|
||||
|
||||
// Properties for SystemOverview
|
||||
systemStatus: string; // e.g., "Normal", "Faulty"
|
||||
temperature: string; // e.g., "35°C"
|
||||
solarPower: number; // Power generated by solar (kW)
|
||||
solarPower: number; // Power generated by solar (kW) - Real-time
|
||||
realTimePower: number; // Real-time power used (kW)
|
||||
installedPower: number; // Installed capacity (kWp)
|
||||
|
||||
// New properties for KPI calculations (assuming these are base values for the month)
|
||||
// If consumptionData/generationData are daily, we will sum them up.
|
||||
// If they were monthly totals, you'd name them appropriately.
|
||||
// Let's assume these are the *raw data points* for the month.
|
||||
// monthlyConsumptionkWh?: number; // Optional, if you want to store a pre-calculated total
|
||||
// monthlyGenerationkWh?: number; // Optional, if you want to store a pre-calculated total
|
||||
|
||||
// For savings calculation:
|
||||
gridImportPrice_RM_per_kWh: number; // Price paid for electricity from the grid
|
||||
solarExportTariff_RM_per_kWh: number; // Price received for excess solar sent to grid (e.g., FiT)
|
||||
theoreticalMaxGeneration_kWh?: number; // For efficiency calculation (e.g., based on installedPower * peak sun hours)
|
||||
}
|
||||
|
||||
// Helper function to sum data for monthly totals (if consumptionData/generationData are daily)
|
||||
const calculateMonthlyTotal = (dataArray: number[]): number => {
|
||||
return dataArray.reduce((sum, value) => sum + value, 0);
|
||||
};
|
||||
|
||||
|
||||
export const mockSiteData: Record<SiteName, SiteDetails> = {
|
||||
'Site A': {
|
||||
location: 'Petaling Jaya, Selangor',
|
||||
inverterProvider: 'SolarEdge',
|
||||
emergencyContact: '+60 12-345 6789',
|
||||
lastSyncTimestamp: '2025-06-03 15:30:00',
|
||||
consumptionData: [100, 110, 120, 130, 125, 140, 135, 120, 110, 180, 100, 130, 90, 150, 160, 170, 180, 175, 160],
|
||||
generationData: [80, 90, 95, 105, 110, 120, 115, 150, 130, 160, 120, 140, 120, 140, 130, 140, 150, 155, 140],
|
||||
systemStatus: 'Normal', // Added
|
||||
temperature: '35°C', // Added
|
||||
solarPower: 108.4, // Added
|
||||
realTimePower: 108.4, // Added
|
||||
installedPower: 174.9, // Added
|
||||
consumptionData: [100, 110, 120, 130, 125, 140, 135, 120, 110, 180, 100, 130, 90, 150, 160, 170, 180, 175, 160], // Example daily kWh for 19 days
|
||||
generationData: [80, 90, 95, 105, 110, 120, 115, 150, 130, 160, 120, 140, 120, 140, 130, 140, 150, 155, 140], // Example daily kWh for 19 days
|
||||
systemStatus: 'Normal',
|
||||
temperature: '35°C',
|
||||
solarPower: 108.4,
|
||||
realTimePower: 108.4,
|
||||
installedPower: 174.9, // kWp
|
||||
gridImportPrice_RM_per_kWh: 0.50, // Example: RM 0.50 per kWh
|
||||
solarExportTariff_RM_per_kWh: 0.30, // Example: RM 0.30 per kWh
|
||||
theoreticalMaxGeneration_kWh: 25000, // Example: Theoretical max for 174.9 kWp over a month in Malaysia
|
||||
},
|
||||
'Site B': {
|
||||
location: 'Kuala Lumpur, Wilayah Persekutuan',
|
||||
@ -37,11 +59,14 @@ export const mockSiteData: Record<SiteName, SiteDetails> = {
|
||||
lastSyncTimestamp: '2025-06-02 10:15:00',
|
||||
consumptionData: [90, 100, 110, 120, 115, 130, 125, 110, 100, 170, 90, 120, 80, 140, 150, 160, 170, 165, 150],
|
||||
generationData: [70, 80, 85, 95, 100, 110, 105, 140, 120, 150, 110, 130, 110, 130, 120, 130, 140, 145, 130],
|
||||
systemStatus: 'Normal', // Added
|
||||
temperature: '32°C', // Added
|
||||
solarPower: 95.2, // Added
|
||||
realTimePower: 95.2, // Added
|
||||
installedPower: 150.0, // Added
|
||||
systemStatus: 'Normal',
|
||||
temperature: '32°C',
|
||||
solarPower: 95.2,
|
||||
realTimePower: 95.2,
|
||||
installedPower: 150.0,
|
||||
gridImportPrice_RM_per_kWh: 0.52,
|
||||
solarExportTariff_RM_per_kWh: 0.32,
|
||||
theoreticalMaxGeneration_kWh: 20000,
|
||||
},
|
||||
'Site C': {
|
||||
location: 'Johor Bahru, Johor',
|
||||
@ -50,10 +75,13 @@ export const mockSiteData: Record<SiteName, SiteDetails> = {
|
||||
lastSyncTimestamp: '2025-06-03 08:00:00',
|
||||
consumptionData: [110, 120, 130, 140, 135, 150, 145, 130, 120, 190, 110, 140, 100, 160, 170, 180, 190, 185, 170],
|
||||
generationData: [50, 60, 65, 75, 80, 90, 85, 100, 90, 110, 80, 90, 80, 90, 80, 90, 100, 105, 90],
|
||||
systemStatus: 'Faulty', // Added
|
||||
temperature: '30°C', // Added
|
||||
solarPower: 25.0, // Added (lower due to fault)
|
||||
realTimePower: 70.0, // Added
|
||||
installedPower: 120.0, // Added
|
||||
systemStatus: 'Faulty',
|
||||
temperature: '30°C',
|
||||
solarPower: 25.0,
|
||||
realTimePower: 70.0,
|
||||
installedPower: 120.0,
|
||||
gridImportPrice_RM_per_kWh: 0.48,
|
||||
solarExportTariff_RM_per_kWh: 0.28,
|
||||
theoreticalMaxGeneration_kWh: 18000, // Lower theoretical max due to fault or smaller system
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user