Progress 1

This commit is contained in:
Syasya 2025-06-03 16:56:25 +08:00
parent 36aecfea44
commit 2a6807cc8f
26 changed files with 1685 additions and 89 deletions

View File

@ -32,7 +32,7 @@ function App({ children }: PropsWithChildren) {
<div
className={`${(themeConfig.sidebar && 'toggle-sidebar') || ''} ${themeConfig.menu} ${themeConfig.layout} ${
themeConfig.rtlClass
} main-section relative font-nunito text-sm font-normal antialiased`}
} main-section relative font-exo2 text-sm font-normal antialiased`}
>
{isLoading ? <Loading /> : children}
<Toaster />

View File

@ -0,0 +1,29 @@
// components/layouts/DashboardLayout.tsx
'use client';
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
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
const themeConfig = useSelector((state: IRootState) => state.themeConfig);
const semidark = useSelector((state: IRootState) => state.themeConfig.semidark);
return (
<div className={`${semidark ? 'dark' : ''} ${themeConfig.sidebar ? 'toggle-sidebar' : ''}`}>
{/* Only render the single, consolidated Header component */}
<Header/>
<Sidebar />
<div className="main-content">
{/* This is where your page content will be injected */}
<div className="p-6 space-y-6 min-h-screen">
{children}
</div>
</div>
</div>
);
};
export default DashboardLayout;

View File

@ -0,0 +1,119 @@
// app/adminDashboard/page.tsx
'use client';
import { useState, useEffect } from 'react';
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';
import DashboardLayout from './dashlayout';
import { SiteName, SiteDetails, mockSiteData } from '@/types/SiteData';
const AdminDashboard = () => {
const searchParams = useSearchParams();
const siteParam = searchParams?.get('site');
const [selectedSite, setSelectedSite] = useState<SiteName>(() => {
const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
if (siteParam && validSiteNames.includes(siteParam as SiteName)) {
return siteParam as SiteName;
}
return 'Site A';
});
useEffect(() => {
const validSiteNames: SiteName[] = ['Site A', 'Site B', 'Site C'];
if (siteParam && validSiteNames.includes(siteParam as SiteName) && siteParam !== selectedSite) {
setSelectedSite(siteParam as SiteName);
}
}, [siteParam, selectedSite]);
const currentSiteDetails: SiteDetails = mockSiteData[selectedSite] || {
location: 'N/A',
inverterProvider: 'N/A',
emergencyContact: 'N/A',
lastSyncTimestamp: 'N/A',
consumptionData: [],
generationData: [],
systemStatus: 'N/A', // Fallback
temperature: 'N/A', // Fallback
solarPower: 0, // Fallback
realTimePower: 0, // Fallback
installedPower: 0, // Fallback
};
const handleCSVExport = () => {
alert('Exported raw data to CSV (mock)');
};
const handlePDFExport = () => {
alert('Exported chart images to PDF (mock)');
};
return (
<DashboardLayout>
<div className="px-6 space-y-6">
<h1 className='text-lg font-semibold'>Admin Dashboard</h1>
{/* Top Section: Site Selector, Site Status, and KPI Table */}
{/* This grid will now properly arrange them into two columns on md screens and up */}
<div className="grid md:grid-cols-2 gap-6">
{/* First Column: Site Selector and Site Status */}
<div className="space-y-4">
<SiteSelector selectedSite={selectedSite} setSelectedSite={setSelectedSite} />
<SiteStatus
selectedSite={selectedSite}
location={currentSiteDetails.location}
inverterProvider={currentSiteDetails.inverterProvider}
emergencyContact={currentSiteDetails.emergencyContact}
lastSyncTimestamp={currentSiteDetails.lastSyncTimestamp}
/>
</div>
{/* Second Column: KPI Table */}
<div> {/* This div will now be the second column */}
<KPI_Table />
</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">
<EnergyLineChart
consumptionData={currentSiteDetails.consumptionData}
generationData={currentSiteDetails.generationData}
/>
</div>
<div className="pb-16">
<MonthlyBarChart />
</div>
</div>
<div className="flex gap-4">
<button onClick={handleCSVExport} className="btn-primary">
Export Raw Data to CSV
</button>
<button onClick={handlePDFExport} className="btn-primary">
Export Chart Images to PDF
</button>
</div>
</div>
</DashboardLayout>
);
};
export default AdminDashboard;

View File

@ -0,0 +1,46 @@
'use client';
import React from 'react';
import DashboardLayout from '../adminDashboard/dashlayout';
import SiteCard from '@/components/dashboards/SiteCard'; // Import the new SiteCard component
import { mockSiteData, SiteName } from '@/types/SiteData'; // Import your mock data and SiteName type
const SitesPage = () => {
// Helper function to determine status (can be externalized if used elsewhere)
const getSiteStatus = (siteName: SiteName): string => {
const statusMap: Record<SiteName, string> = {
'Site A': 'Active',
'Site B': 'Inactive',
'Site C': 'Faulty',
};
return statusMap[siteName];
};
return (
<DashboardLayout>
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold mb-6 dark:text-white-light">All Sites Overview</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Iterate over the keys of mockSiteData (which are your SiteNames) */}
{Object.keys(mockSiteData).map((siteNameKey) => {
const siteName = siteNameKey as SiteName; // Cast to SiteName type
const siteDetails = mockSiteData[siteName];
const siteStatus = getSiteStatus(siteName);
return (
<SiteCard
key={siteName} // Important for React list rendering
siteName={siteName}
details={siteDetails}
status={siteStatus}
/>
);
})}
</div>
</div>
</DashboardLayout>
);
};
export default SitesPage;

View File

@ -25,6 +25,9 @@ const InverterViewPage = (props: Props) => {
const fetchData = async () => {
try {
if (!params || !params.id) {
throw new Error("Invalid params or params.id is missing");
}
const res = await axios.get(`https://api-a.fomware.com.cn/asset/v1/list?type=2&key=${params.id.toString()}`, {
headers: {
"Authorization": "Bearer " + process.env.NEXT_PUBLIC_CHINT_TOKEN
@ -45,7 +48,7 @@ const InverterViewPage = (props: Props) => {
{loading ? <p>Loading...</p> : (
<>
<PanelCodeHighlight title={params.id.toString() || ""}>
<PanelCodeHighlight title={params?.id?.toString() || ""}>
<div className="mb-5">
{isMounted && (
<Tab.Group>

View File

@ -6,43 +6,33 @@ import Header from '@/components/layouts/header';
import MainContainer from '@/components/layouts/main-container';
import Overlay from '@/components/layouts/overlay';
import ScrollToTop from '@/components/layouts/scroll-to-top';
import Setting from '@/components/layouts/setting';
import Sidebar from '@/components/layouts/sidebar';
import Portals from '@/components/portals';
import withAuth from '@/hoc/withAuth';
import { FC } from 'react';
import withAuth from '@/hoc/withAuth'; // make sure this matches your export style
import { FC, ReactNode } from 'react';
const DefaultLayout: FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<>
{/* BEGIN MAIN CONTAINER */}
<div className="relative">
<Overlay />
<ScrollToTop />
<MainContainer>
{/* BEGIN SIDEBAR */}
<Sidebar />
{/* END SIDEBAR */}
<div className="main-content flex min-h-screen flex-col">
{/* BEGIN TOP NAVBAR */}
<Header />
{/* END TOP NAVBAR */}
{/* BEGIN CONTENT AREA */}
<ContentAnimation>{children}</ContentAnimation>
{/* END CONTENT AREA */}
{/* BEGIN FOOTER */}
<Footer />
{/* END FOOTER */}
<Portals />
</div>
</MainContainer>
</div>
</>
);
interface DefaultLayoutProps {
children: ReactNode;
}
const DefaultLayout: FC<DefaultLayoutProps> = ({ children }) => {
return (
<div className="relative">
<Overlay />
<ScrollToTop />
<MainContainer>
<Sidebar />
<div className="main-content flex min-h-screen flex-col">
<Header />
<ContentAnimation>{children}</ContentAnimation>
<Footer />
<Portals />
</div>
</MainContainer>
</div>
);
};
export default withAuth(DefaultLayout);

View File

@ -1,11 +1,47 @@
'use client'
import { Metadata } from 'next';
import React from 'react';
import React, { useState } from 'react';
export const metadata: Metadata = {
};
const Sales = () => {
return <div>starter page</div>;
const [selectedSite, setSelectedSite] = useState('');
const sites = ['Site A', 'Site B', 'Site C']; // replace with your actual site list
return (
<div className="min-h-screen flex flex-col items-center justify-center p-4 bg-gray-50">
<h1 className="text-3xl font-bold mb-4 text-gray-800">
Welcome to Rooftop Dashboard !
</h1>
<h2 className="text-2xl font-bold mb-4 text-gray-800">
Select a site to get started.
</h2>
<div className="w-full max-w-sm">
<label className="block text-gray-700 mb-2">Select Site:</label>
<select
value={selectedSite}
onChange={(e) => setSelectedSite(e.target.value)}
className="w-full p-2 border-2 border-yellow-300 rounded-md"
>
<option value="" disabled>
-- Choose a site --
</option>
{sites.map((site) => (
<option key={site} value={site}>
{site}
</option>
))}
</select>
{selectedSite && (
<div className="flex flex-col space-y-2">
<p className="mt-4 text-green-700">You selected: {selectedSite}</p>
<button className="btn-primary">Go to Dashboard</button>
</div>
)}
</div>
</div>
);
};
export default Sales;

View File

@ -1,15 +1,17 @@
'use client';
import ProviderComponent from '@/components/layouts/provider-component';
import 'react-perfect-scrollbar/dist/css/styles.css';
import '../styles/tailwind.css';
import { Metadata } from 'next';
import { Nunito } from 'next/font/google';
import { Exo_2 } from "next/font/google";
const exo2 = Exo_2({
subsets: ["latin"],
variable: "--font-exo2",
weight: ["200", "400"],
});
export const metadata: Metadata = {
title: {
template: '%s | Rooftop Energy - Admin',
default: 'Rooftop Energy - Admin',
},
};
const nunito = Nunito({
weight: ['400', '500', '600', '700', '800'],
subsets: ['latin'],
@ -20,7 +22,7 @@ const nunito = Nunito({
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={nunito.variable}>
<body className={exo2.variable}>
<ProviderComponent>{children}</ProviderComponent>
</body>
</html>

View File

@ -0,0 +1,130 @@
// components/dashboards/EnergyLineChart.tsx
'use client';
import { useRef } from 'react';
import {
Chart as ChartJS,
LineElement,
PointElement,
LinearScale,
CategoryScale,
Title,
Tooltip,
Legend,
} from 'chart.js';
import zoomPlugin from 'chartjs-plugin-zoom';
import { Line } from 'react-chartjs-2';
ChartJS.register(
LineElement,
PointElement,
LinearScale,
CategoryScale,
Title,
Tooltip,
Legend,
zoomPlugin
);
// Define props interface for EnergyLineChart
interface EnergyLineChartProps {
consumptionData: number[];
generationData: number[];
}
const labels = [
'08:00', '08:30', '09:00', '09:30', '10:00',
'10:30', '11:00', '11:30', '12:00', '12:30',
'13:00', '13:30', '14:00', '14:30', '15:00',
'15:30', '16:00', '16:30', '17:00',
];
const EnergyLineChart = ({ consumptionData, generationData }: EnergyLineChartProps) => {
const chartRef = useRef<any>(null);
// Calculate suggestedMax dynamically based on the current data
const allDataPoints = [...consumptionData, ...generationData];
const maxDataValue = allDataPoints.length > 0 ? Math.max(...allDataPoints) : 0; // Handle empty array
const yAxisSuggestedMax = maxDataValue * 1.15; // Adds 15% padding
const data = {
labels,
datasets: [
{
label: 'Consumption',
data: consumptionData, // Use prop data
borderColor: '#8884d8',
tension: 0.4,
fill: false,
},
{
label: 'Generation',
data: generationData, // Use prop data
borderColor: '#82ca9d',
tension: 0.4,
fill: false,
},
],
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top' as const,
},
zoom: {
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true,
},
mode: "x" as const,
},
pan: {
enabled: true,
mode: "x" as const,
},
},
tooltip: {
enabled: true,
mode: 'index' as const,
intersect: false,
}
},
scales: {
y: {
beginAtZero: true,
suggestedMax: yAxisSuggestedMax, // Use the dynamically calculated value
},
},
};
const handleResetZoom = () => {
chartRef.current?.resetZoom();
};
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="flex justify-between items-center mb-2">
<h2 className="text-lg font-bold dark:text-white-light">Power/Energy Consumption & Generation</h2>
<button
onClick={handleResetZoom}
className="btn-primary text-sm"
>
Reset Zoom
</button>
</div>
<div className="h-full w-full">
<Line ref={chartRef} data={data} options={options} />
</div>
</div>
</div>
);
};
export default EnergyLineChart;

View File

@ -0,0 +1,33 @@
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' },
];
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>
);
};
export default KPI_Table;

View File

@ -0,0 +1,48 @@
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
Cell
} from 'recharts';
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 },
];
const barColors = ['#003049', '#669bbc']; // Alternate between yellow tones
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>
);
};
export default MonthlyBarChart;

View File

@ -0,0 +1,63 @@
// components/dashboards/SiteCard.tsx
import React from 'react';
import Link from 'next/link'; // Import Link from Next.js
import { SiteName, SiteDetails } from '@/types/SiteData'; // Adjust path as necessary
interface SiteCardProps {
siteName: SiteName;
details: SiteDetails;
status: string;
}
const SiteCard: React.FC<SiteCardProps> = ({ siteName, details, status }) => {
const statusColorClass =
status === 'Active' ? 'text-green-500' :
status === 'Inactive' ? 'text-orange-500' :
'text-red-500';
return (
<div className="bg-white p-4 rounded-lg shadow-md dark:bg-gray-800 dark:text-white-light flex flex-col space-y-2">
<h3 className="text-xl font-bold text-primary-600 dark:text-primary-400 border-b pb-2 mb-2">
{siteName}
</h3>
<div className="flex justify-between items-center">
<p className="text-gray-600 dark:text-gray-400 font-medium">Status:</p>
<p className={`font-semibold ${statusColorClass}`}>{status}</p>
</div>
<div className="flex justify-between items-center">
<p className="text-gray-600 dark:text-gray-400 font-medium">Location:</p>
<p className="font-semibold">{details.location}</p>
</div>
<div className="flex justify-between items-center">
<p className="text-gray-600 dark:text-gray-400 font-medium">Inverter Provider:</p>
<p className="font-semibold">{details.inverterProvider}</p>
</div>
<div className="flex justify-between items-center">
<p className="text-gray-600 dark:text-gray-400 font-medium">Emergency Contact:</p>
<p className="font-semibold">{details.emergencyContact}</p>
</div>
<div className="flex justify-between items-center">
<p className="text-gray-600 dark:text-gray-400 font-medium">Last Sync:</p>
<p className="font-semibold">{details.lastSyncTimestamp}</p>
</div>
{/* New: View Dashboard Button */}
<Link
href={{
pathname: '/adminDashboard', // Path to your AdminDashboard page
query: { site: siteName }, // Pass the siteName as a query parameter
}}
className="mt-4 w-full text-center text-sm btn-primary" // Tailwind classes for basic button styling
>
View Dashboard
</Link>
</div>
);
};
export default SiteCard;

View File

@ -0,0 +1,26 @@
import type { SiteName } from '@/components/dashboards/SiteStatus';
type SiteSelectorProps = {
selectedSite: SiteName;
setSelectedSite: (site: SiteName) => void;
};
const SiteSelector = ({ selectedSite, setSelectedSite }: SiteSelectorProps) => {
return (
<div className="flex flex-col ">
<label htmlFor="site" className="font-semibold text-lg">Select Site:</label>
<select
id="site"
className="border p-2 rounded"
value={selectedSite}
onChange={(e) => setSelectedSite(e.target.value as SiteName)}
>
<option>Site A</option>
<option>Site B</option>
<option>Site C</option>
</select>
</div>
);
};
export default SiteSelector;

View File

@ -0,0 +1,67 @@
export type SiteName = 'Site A' | 'Site B' | 'Site C';
interface SiteStatusProps {
selectedSite: SiteName;
location: string;
inverterProvider: string;
emergencyContact: string;
lastSyncTimestamp: string;
}
const SiteStatus = ({
selectedSite,
location,
inverterProvider,
emergencyContact,
lastSyncTimestamp,
}: SiteStatusProps) => {
const statusMap: Record<SiteName, string> = {
'Site A': 'Active',
'Site B': 'Inactive',
'Site C': 'Faulty',
};
return (
<div className="bg-white p-4 rounded-lg shadow-md space-y-2 dark:bg-gray-800 dark:text-white-light">
<h2 className="text-xl font-semibold mb-3">Site Details</h2>
{/* Status */}
<div className="flex justify-between items-center text-base">
<p className="text-gray-600 dark:text-gray-400 font-medium">Status:</p>
<p className={`font-semibold ${
statusMap[selectedSite] === 'Active' ? 'text-green-500' :
statusMap[selectedSite] === 'Inactive' ? 'text-orange-500' :
'text-red-500'
}`}>
{statusMap[selectedSite]}
</p>
</div>
{/* Location */}
<div className="flex justify-between items-center text-base">
<p className="text-gray-600 dark:text-gray-400 font-medium">Location:</p>
<p className="font-medium">{location}</p>
</div>
{/* Inverter Provider */}
<div className="flex justify-between items-center text-base">
<p className="text-gray-600 dark:text-gray-400 font-medium">Inverter Provider:</p>
<p className="font-medium">{inverterProvider}</p>
</div>
{/* Emergency Contact */}
<div className="flex justify-between items-center text-base">
<p className="text-gray-600 dark:text-gray-400 font-medium">Emergency Contact:</p>
<p className="font-medium">{emergencyContact}</p>
</div>
{/* Last Sync */}
<div className="flex justify-between items-center text-base">
<p className="text-gray-600 dark:text-gray-400 font-medium">Last Sync:</p>
<p className="font-medium">{lastSyncTimestamp}</p>
</div>
</div>
);
};
export default SiteStatus;

View File

@ -0,0 +1,137 @@
// 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;

View File

@ -0,0 +1,26 @@
'use client';
import { useDispatch, useSelector } from 'react-redux';
import { toggleSidebar } from '@/store/themeConfigSlice';
import { IRootState } from '@/store';
const SidebarToggleButton = () => {
const dispatch = useDispatch();
const themeConfig = useSelector((state: IRootState) => state.themeConfig);
return (
<button
type="button"
className="flex h-8 w-8 items-center justify-center rounded-full transition duration-300 hover:bg-gray-500/10 dark:text-white-light dark:hover:bg-dark-light/10"
onClick={() => dispatch(toggleSidebar())}
// Optional: You might want to hide this button if the sidebar is already open
// or show a different icon depending on the sidebar state.
// For simplicity, it just toggles.
>
{/* You can use a generic menu icon here, or condition it based on themeConfig.sidebar */}
</button>
);
};
export default SidebarToggleButton;

View File

@ -62,11 +62,12 @@ const Sidebar = () => {
};
return (
<div className={semidark ? 'dark' : ''}>
<nav
className={`sidebar fixed bottom-0 top-0 z-50 h-full min-h-screen w-[260px] shadow-[5px_0_25px_0_rgba(94,92,154,0.1)] transition-all duration-300 ${semidark ? 'text-white-dark' : ''}`}
>
<div className="h-full bg-white dark:bg-black">
<div className="h-full bg-[white] dark:bg-black">
<div className="flex items-center justify-between px-4 py-3">
<Link href="/" className="main-logo flex shrink-0 items-center">
<img className="ml-[5px] w-8 flex-none" src="/assets/images/logo.png" alt="logo" />
@ -81,8 +82,8 @@ const Sidebar = () => {
<IconCaretsDown className="m-auto rotate-90" />
</button>
</div>
<PerfectScrollbar className="relative h-[calc(100vh-80px)]">
<ul className="relative space-y-0.5 p-4 py-0 font-semibold">
<PerfectScrollbar className="relative h-[calc(100vh-80px)] ">
<ul className="relative space-y-0.5 p-4 py-0 font-md">
<h2 className="-mx-4 mb-1 flex items-center bg-white-light/30 px-7 py-3 font-extrabold uppercase dark:bg-dark dark:bg-opacity-[0.08]">
<IconMinus className="hidden h-5 w-4 flex-none" />
<span>Customer</span>
@ -103,14 +104,6 @@ const Sidebar = () => {
</div>
</Link>
</li>
<li className="menu nav-item">
<Link href="#" className="nav-link group">
<div className="flex items-center">
<IconMenuComponents className="shrink-0 group-hover:!text-primary" />
<span className="text-black ltr:pl-3 rtl:pr-3 dark:text-[#506690] dark:group-hover:text-white-dark">Inverters</span>
</div>
</Link>
</li>
<li className="menu nav-item">
<button type="button" className={`nav-link group w-full`} onClick={() => toggleMenu('setting')}>
<div className="flex items-center">
@ -139,6 +132,34 @@ const Sidebar = () => {
<IconMinus className="hidden h-5 w-4 flex-none" />
<span>Admin</span>
</h2>
<li className="menu nav-item">
<Link href="/adminDashboard" className="nav-link group">
<div className="flex items-center">
<IconMenuComponents className="shrink-0 group-hover:!text-primary" />
<span className="text-black ltr:pl-3 rtl:pr-3 dark:text-[#506690] dark:group-hover:text-white-dark">Dashboard</span>
</div>
</Link>
</li>
<li className="menu nav-item">
<Link href="/sites" className="nav-link group">
<div className="flex items-center">
<IconMenuComponents className="shrink-0 group-hover:!text-primary" />
<span className="text-black ltr:pl-3 rtl:pr-3 dark:text-[#506690] dark:group-hover:text-white-dark">Sites</span>
</div>
</Link>
</li>
<li className="menu nav-item">
<Link href="#" className="nav-link group">
<div className="flex items-center">
<IconMenuComponents className="shrink-0 group-hover:!text-primary" />
<span className="text-black ltr:pl-3 rtl:pr-3 dark:text-[#506690] dark:group-hover:text-white-dark">Devices</span>
</div>
</Link>
</li>
<h2 className="-mx-4 mb-1 flex items-center px-7 py-3 font-extrabold uppercase dark:bg-opacity-[0.08]">
<IconMinus className="hidden h-5 w-4 flex-none" />
<span>Providers</span>
</h2>
<li className="menu nav-item">
<Link href="#" className="nav-link group">
<div className="flex items-center">

View File

@ -1,27 +1,31 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { ComponentType } from "react";
const withAuth = (WrappedComponent: React.FC) => {
return (props: any) => {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const router = useRouter()
const withAuth = <P extends object>(WrappedComponent: ComponentType<P>) => {
const WithAuthComponent = (props: P) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem("token")
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) {
router.replace("/login") // Redirect to login if no token
} else {
setIsAuthenticated(true)
}
}, [])
if (!token) {
router.replace("/login");
} else {
setIsAuthenticated(true);
}
}, []);
if (!isAuthenticated) {
return null // Avoid rendering until auth check is done
}
if (!isAuthenticated) {
return null;
}
return <WrappedComponent {...props} />
};
return <WrappedComponent {...props} />;
};
return WithAuthComponent;
};
export default withAuth
export default withAuth;

738
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,15 +11,18 @@
"dependencies": {
"@emotion/react": "^11.10.6",
"@headlessui/react": "^1.7.8",
"@prisma/client": "^6.4.1",
"@prisma/client": "^6.8.2",
"@reduxjs/toolkit": "^1.9.1",
"@tippyjs/react": "^4.2.6",
"@types/bcrypt": "^5.0.2",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"apexcharts": "^4.5.0",
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"chart.js": "^4.4.9",
"chartjs-plugin-zoom": "^2.2.0",
"cookie": "^1.0.2",
"eslint": "8.32.0",
"eslint-config-next": "13.1.2",
@ -30,18 +33,21 @@
"react": "18.2.0",
"react-animate-height": "^3.1.0",
"react-apexcharts": "^1.7.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "18.2.0",
"react-hot-toast": "^2.5.2",
"react-i18next": "^12.1.5",
"react-perfect-scrollbar": "^1.5.8",
"react-popper": "^2.3.0",
"react-redux": "^8.1.3",
"recharts": "^2.15.3",
"universal-cookie": "^6.1.1",
"yup": "^0.32.11"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/jsonwebtoken": "^9.0.9",
"@types/lodash": "^4.14.191",
"@types/react-redux": "^7.1.32",
"autoprefixer": "^10.4.17",

BIN
public/images/building.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

BIN
public/images/gridtower.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@ -237,7 +237,7 @@ hover:text-primary hover:before:!bg-primary ltr:before:mr-2 rtl:before:ml-2 dark
}
.btn-primary {
@apply border-primary bg-primary text-white shadow-primary/60;
@apply rounded-3xl px-10 py-2.5 bg-rtyellow-200 text-black text-lg font-medium font-exo2 hover:bg-rtyellow-300;
}
.btn-outline-primary {
@apply border-primary text-primary shadow-none hover:bg-primary hover:text-white;

View File

@ -7,6 +7,7 @@ const rotateX = plugin(function ({ addUtilities }) {
},
});
});
module.exports = {
content: ['./App.tsx', './app/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', './src/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class',
@ -16,6 +17,29 @@ module.exports = {
},
extend: {
colors: {
rtgray: {
50: '#F9FAFB',
100: '#F3F4F6',
200: '#E6E6EA',
300: '#D3D4D9',
400: '#9EA1AD',
500: '#6D707E',
600: '#4D5261',
700: '#3A3F4E',
800: '#222634',
900: '#141624',
1000: '#080912',
},
rtyellow: {
200: '#FCD913',
300: '#E6C812',
500: '#F2BE03',
600: '#D9A003',
},
rtbrown: {
800: '#301E03',
},
rtwhite: '#FFFFFF',
primary: {
DEFAULT: '#fcd913',
light: '#eaf1ff',
@ -61,10 +85,13 @@ module.exports = {
light: '#e0e6ed',
dark: '#888ea8',
},
},
fontFamily: {
nunito: ['var(--font-nunito)'],
},
},
fontFamily: {
nunito: ['var(--font-nunito)'],
exo2: ['var(--font-exo2)'],
sans: ['var(--font-sans)'],
mono: ['var(--font-mono)'],
},
spacing: {
4.5: '18px',
},

59
types/SiteData.ts Normal file
View File

@ -0,0 +1,59 @@
// types/siteData.ts
export type SiteName = 'Site A' | 'Site B' | 'Site C';
export interface SiteDetails {
location: string;
inverterProvider: string;
emergencyContact: string;
lastSyncTimestamp: string;
consumptionData: number[];
generationData: number[];
// New properties for SystemOverview
systemStatus: 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)
}
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
},
'Site B': {
location: 'Kuala Lumpur, Wilayah Persekutuan',
inverterProvider: 'Huawei',
emergencyContact: '+60 19-876 5432',
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
},
'Site C': {
location: 'Johor Bahru, Johor',
inverterProvider: 'Enphase',
emergencyContact: '+60 13-555 1234',
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
},
};