Progress 1
This commit is contained in:
parent
36aecfea44
commit
2a6807cc8f
2
App.tsx
2
App.tsx
@ -32,7 +32,7 @@ function App({ children }: PropsWithChildren) {
|
|||||||
<div
|
<div
|
||||||
className={`${(themeConfig.sidebar && 'toggle-sidebar') || ''} ${themeConfig.menu} ${themeConfig.layout} ${
|
className={`${(themeConfig.sidebar && 'toggle-sidebar') || ''} ${themeConfig.menu} ${themeConfig.layout} ${
|
||||||
themeConfig.rtlClass
|
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}
|
{isLoading ? <Loading /> : children}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|||||||
29
app/(admin)/adminDashboard/dashlayout.tsx
Normal file
29
app/(admin)/adminDashboard/dashlayout.tsx
Normal 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;
|
||||||
119
app/(admin)/adminDashboard/page.tsx
Normal file
119
app/(admin)/adminDashboard/page.tsx
Normal 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;
|
||||||
|
|
||||||
46
app/(admin)/sites/page.tsx
Normal file
46
app/(admin)/sites/page.tsx
Normal 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;
|
||||||
@ -25,6 +25,9 @@ const InverterViewPage = (props: Props) => {
|
|||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
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()}`, {
|
const res = await axios.get(`https://api-a.fomware.com.cn/asset/v1/list?type=2&key=${params.id.toString()}`, {
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": "Bearer " + process.env.NEXT_PUBLIC_CHINT_TOKEN
|
"Authorization": "Bearer " + process.env.NEXT_PUBLIC_CHINT_TOKEN
|
||||||
@ -45,7 +48,7 @@ const InverterViewPage = (props: Props) => {
|
|||||||
|
|
||||||
{loading ? <p>Loading...</p> : (
|
{loading ? <p>Loading...</p> : (
|
||||||
<>
|
<>
|
||||||
<PanelCodeHighlight title={params.id.toString() || ""}>
|
<PanelCodeHighlight title={params?.id?.toString() || ""}>
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
{isMounted && (
|
{isMounted && (
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
|
|||||||
@ -6,43 +6,33 @@ import Header from '@/components/layouts/header';
|
|||||||
import MainContainer from '@/components/layouts/main-container';
|
import MainContainer from '@/components/layouts/main-container';
|
||||||
import Overlay from '@/components/layouts/overlay';
|
import Overlay from '@/components/layouts/overlay';
|
||||||
import ScrollToTop from '@/components/layouts/scroll-to-top';
|
import ScrollToTop from '@/components/layouts/scroll-to-top';
|
||||||
import Setting from '@/components/layouts/setting';
|
|
||||||
import Sidebar from '@/components/layouts/sidebar';
|
import Sidebar from '@/components/layouts/sidebar';
|
||||||
import Portals from '@/components/portals';
|
import Portals from '@/components/portals';
|
||||||
import withAuth from '@/hoc/withAuth';
|
import withAuth from '@/hoc/withAuth'; // make sure this matches your export style
|
||||||
import { FC } from 'react';
|
import { FC, ReactNode } from 'react';
|
||||||
|
|
||||||
const DefaultLayout: FC<{ children: React.ReactNode }> = ({ children }) => {
|
interface DefaultLayoutProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultLayout: FC<DefaultLayoutProps> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
{/* BEGIN MAIN CONTAINER */}
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Overlay />
|
<Overlay />
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
|
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
{/* BEGIN SIDEBAR */}
|
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
{/* END SIDEBAR */}
|
|
||||||
<div className="main-content flex min-h-screen flex-col">
|
<div className="main-content flex min-h-screen flex-col">
|
||||||
{/* BEGIN TOP NAVBAR */}
|
|
||||||
<Header />
|
<Header />
|
||||||
{/* END TOP NAVBAR */}
|
|
||||||
|
|
||||||
{/* BEGIN CONTENT AREA */}
|
|
||||||
<ContentAnimation>{children}</ContentAnimation>
|
<ContentAnimation>{children}</ContentAnimation>
|
||||||
{/* END CONTENT AREA */}
|
|
||||||
|
|
||||||
{/* BEGIN FOOTER */}
|
|
||||||
<Footer />
|
<Footer />
|
||||||
{/* END FOOTER */}
|
|
||||||
<Portals />
|
<Portals />
|
||||||
</div>
|
</div>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default withAuth(DefaultLayout);
|
export default withAuth(DefaultLayout);
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,47 @@
|
|||||||
|
'use client'
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
};
|
|
||||||
|
|
||||||
const Sales = () => {
|
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;
|
export default Sales;
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
|
'use client';
|
||||||
import ProviderComponent from '@/components/layouts/provider-component';
|
import ProviderComponent from '@/components/layouts/provider-component';
|
||||||
import 'react-perfect-scrollbar/dist/css/styles.css';
|
import 'react-perfect-scrollbar/dist/css/styles.css';
|
||||||
import '../styles/tailwind.css';
|
import '../styles/tailwind.css';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { Nunito } from 'next/font/google';
|
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({
|
const nunito = Nunito({
|
||||||
weight: ['400', '500', '600', '700', '800'],
|
weight: ['400', '500', '600', '700', '800'],
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@ -20,7 +22,7 @@ const nunito = Nunito({
|
|||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={nunito.variable}>
|
<body className={exo2.variable}>
|
||||||
<ProviderComponent>{children}</ProviderComponent>
|
<ProviderComponent>{children}</ProviderComponent>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
130
components/dashboards/EnergyLineChart.tsx
Normal file
130
components/dashboards/EnergyLineChart.tsx
Normal 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;
|
||||||
|
|
||||||
33
components/dashboards/KPIStatus.tsx
Normal file
33
components/dashboards/KPIStatus.tsx
Normal 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;
|
||||||
48
components/dashboards/MonthlyBarChart.tsx
Normal file
48
components/dashboards/MonthlyBarChart.tsx
Normal 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;
|
||||||
63
components/dashboards/SiteCard.tsx
Normal file
63
components/dashboards/SiteCard.tsx
Normal 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;
|
||||||
26
components/dashboards/SiteSelector.tsx
Normal file
26
components/dashboards/SiteSelector.tsx
Normal 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;
|
||||||
67
components/dashboards/SiteStatus.tsx
Normal file
67
components/dashboards/SiteStatus.tsx
Normal 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;
|
||||||
137
components/dashboards/SystemOverview.tsx
Normal file
137
components/dashboards/SystemOverview.tsx
Normal 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;
|
||||||
26
components/layouts/sidebar-toggle.tsx
Normal file
26
components/layouts/sidebar-toggle.tsx
Normal 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;
|
||||||
@ -62,11 +62,12 @@ const Sidebar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className={semidark ? 'dark' : ''}>
|
<div className={semidark ? 'dark' : ''}>
|
||||||
<nav
|
<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' : ''}`}
|
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">
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
<Link href="/" className="main-logo flex shrink-0 items-center">
|
<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" />
|
<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" />
|
<IconCaretsDown className="m-auto rotate-90" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<PerfectScrollbar className="relative h-[calc(100vh-80px)]">
|
<PerfectScrollbar className="relative h-[calc(100vh-80px)] ">
|
||||||
<ul className="relative space-y-0.5 p-4 py-0 font-semibold">
|
<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]">
|
<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" />
|
<IconMinus className="hidden h-5 w-4 flex-none" />
|
||||||
<span>Customer</span>
|
<span>Customer</span>
|
||||||
@ -103,14 +104,6 @@ const Sidebar = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</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">
|
<li className="menu nav-item">
|
||||||
<button type="button" className={`nav-link group w-full`} onClick={() => toggleMenu('setting')}>
|
<button type="button" className={`nav-link group w-full`} onClick={() => toggleMenu('setting')}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@ -139,6 +132,34 @@ const Sidebar = () => {
|
|||||||
<IconMinus className="hidden h-5 w-4 flex-none" />
|
<IconMinus className="hidden h-5 w-4 flex-none" />
|
||||||
<span>Admin</span>
|
<span>Admin</span>
|
||||||
</h2>
|
</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">
|
<li className="menu nav-item">
|
||||||
<Link href="#" className="nav-link group">
|
<Link href="#" className="nav-link group">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
@ -1,27 +1,31 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { ComponentType } from "react";
|
||||||
|
|
||||||
const withAuth = (WrappedComponent: React.FC) => {
|
const withAuth = <P extends object>(WrappedComponent: ComponentType<P>) => {
|
||||||
return (props: any) => {
|
const WithAuthComponent = (props: P) => {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("token")
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
router.replace("/login") // Redirect to login if no token
|
router.replace("/login");
|
||||||
} else {
|
} else {
|
||||||
setIsAuthenticated(true)
|
setIsAuthenticated(true);
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return null // Avoid rendering until auth check is done
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <WrappedComponent {...props} />
|
return <WrappedComponent {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return WithAuthComponent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withAuth
|
export default withAuth;
|
||||||
|
|
||||||
|
|||||||
738
package-lock.json
generated
738
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,15 +11,18 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@headlessui/react": "^1.7.8",
|
"@headlessui/react": "^1.7.8",
|
||||||
"@prisma/client": "^6.4.1",
|
"@prisma/client": "^6.8.2",
|
||||||
"@reduxjs/toolkit": "^1.9.1",
|
"@reduxjs/toolkit": "^1.9.1",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
"@types/react": "18.0.27",
|
"@types/react": "18.0.27",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.10",
|
||||||
"apexcharts": "^4.5.0",
|
"apexcharts": "^4.5.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
|
"chartjs-plugin-zoom": "^2.2.0",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"eslint": "8.32.0",
|
"eslint": "8.32.0",
|
||||||
"eslint-config-next": "13.1.2",
|
"eslint-config-next": "13.1.2",
|
||||||
@ -30,18 +33,21 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-animate-height": "^3.1.0",
|
"react-animate-height": "^3.1.0",
|
||||||
"react-apexcharts": "^1.7.0",
|
"react-apexcharts": "^1.7.0",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-i18next": "^12.1.5",
|
"react-i18next": "^12.1.5",
|
||||||
"react-perfect-scrollbar": "^1.5.8",
|
"react-perfect-scrollbar": "^1.5.8",
|
||||||
"react-popper": "^2.3.0",
|
"react-popper": "^2.3.0",
|
||||||
"react-redux": "^8.1.3",
|
"react-redux": "^8.1.3",
|
||||||
|
"recharts": "^2.15.3",
|
||||||
"universal-cookie": "^6.1.1",
|
"universal-cookie": "^6.1.1",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tailwindcss/typography": "^0.5.8",
|
"@tailwindcss/typography": "^0.5.8",
|
||||||
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/lodash": "^4.14.191",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/react-redux": "^7.1.32",
|
"@types/react-redux": "^7.1.32",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
|
|||||||
BIN
public/images/building.png
Normal file
BIN
public/images/building.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
BIN
public/images/gridtower.png
Normal file
BIN
public/images/gridtower.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
BIN
public/images/solarpanel.png
Normal file
BIN
public/images/solarpanel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
@ -237,7 +237,7 @@ hover:text-primary hover:before:!bg-primary ltr:before:mr-2 rtl:before:ml-2 dark
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.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 {
|
.btn-outline-primary {
|
||||||
@apply border-primary text-primary shadow-none hover:bg-primary hover:text-white;
|
@apply border-primary text-primary shadow-none hover:bg-primary hover:text-white;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const rotateX = plugin(function ({ addUtilities }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = {
|
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}'],
|
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',
|
darkMode: 'class',
|
||||||
@ -16,6 +17,29 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
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: {
|
primary: {
|
||||||
DEFAULT: '#fcd913',
|
DEFAULT: '#fcd913',
|
||||||
light: '#eaf1ff',
|
light: '#eaf1ff',
|
||||||
@ -64,6 +88,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
nunito: ['var(--font-nunito)'],
|
nunito: ['var(--font-nunito)'],
|
||||||
|
exo2: ['var(--font-exo2)'],
|
||||||
|
sans: ['var(--font-sans)'],
|
||||||
|
mono: ['var(--font-mono)'],
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
4.5: '18px',
|
4.5: '18px',
|
||||||
|
|||||||
59
types/SiteData.ts
Normal file
59
types/SiteData.ts
Normal 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
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user