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 | ||||
|             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 /> | ||||
|  | ||||
							
								
								
									
										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 () => { | ||||
|         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> | ||||
|  | ||||
| @ -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); | ||||
| 
 | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
							
								
								
									
										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 ( | ||||
|          | ||||
|         <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"> | ||||
|  | ||||
| @ -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
									
									
									
								
							
							
						
						
									
										738
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -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
									
								
							
							
						
						
									
										
											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 { | ||||
|         @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; | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										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