From 2a6807cc8f603af2f69abc0d17d3c0404bf75df7 Mon Sep 17 00:00:00 2001 From: Syasya Date: Tue, 3 Jun 2025 16:56:25 +0800 Subject: [PATCH 01/27] Progress 1 --- App.tsx | 2 +- app/(admin)/adminDashboard/dashlayout.tsx | 29 + app/(admin)/adminDashboard/page.tsx | 119 +++ app/(admin)/sites/page.tsx | 46 ++ app/(defaults)/chint/inverters/[id]/page.tsx | 5 +- app/(defaults)/layout.tsx | 58 +- app/(defaults)/page.tsx | 44 +- app/layout.tsx | 16 +- components/dashboards/EnergyLineChart.tsx | 130 ++++ components/dashboards/KPIStatus.tsx | 33 + components/dashboards/MonthlyBarChart.tsx | 48 ++ components/dashboards/SiteCard.tsx | 63 ++ components/dashboards/SiteSelector.tsx | 26 + components/dashboards/SiteStatus.tsx | 67 ++ components/dashboards/SystemOverview.tsx | 137 ++++ components/layouts/sidebar-toggle.tsx | 26 + components/layouts/sidebar.tsx | 43 +- hoc/withAuth.tsx | 40 +- package-lock.json | 738 ++++++++++++++++++- package.json | 8 +- public/images/building.png | Bin 0 -> 121940 bytes public/images/gridtower.png | Bin 0 -> 137168 bytes public/images/solarpanel.png | Bin 0 -> 154259 bytes styles/tailwind.css | 2 +- tailwind.config.js | 35 +- types/SiteData.ts | 59 ++ 26 files changed, 1685 insertions(+), 89 deletions(-) create mode 100644 app/(admin)/adminDashboard/dashlayout.tsx create mode 100644 app/(admin)/adminDashboard/page.tsx create mode 100644 app/(admin)/sites/page.tsx create mode 100644 components/dashboards/EnergyLineChart.tsx create mode 100644 components/dashboards/KPIStatus.tsx create mode 100644 components/dashboards/MonthlyBarChart.tsx create mode 100644 components/dashboards/SiteCard.tsx create mode 100644 components/dashboards/SiteSelector.tsx create mode 100644 components/dashboards/SiteStatus.tsx create mode 100644 components/dashboards/SystemOverview.tsx create mode 100644 components/layouts/sidebar-toggle.tsx create mode 100644 public/images/building.png create mode 100644 public/images/gridtower.png create mode 100644 public/images/solarpanel.png create mode 100644 types/SiteData.ts diff --git a/App.tsx b/App.tsx index aa626fc..a94c018 100644 --- a/App.tsx +++ b/App.tsx @@ -32,7 +32,7 @@ function App({ children }: PropsWithChildren) {
{isLoading ? : children} diff --git a/app/(admin)/adminDashboard/dashlayout.tsx b/app/(admin)/adminDashboard/dashlayout.tsx new file mode 100644 index 0000000..5157945 --- /dev/null +++ b/app/(admin)/adminDashboard/dashlayout.tsx @@ -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 ( +
+ {/* Only render the single, consolidated Header component */} +
+ + +
+ {/* This is where your page content will be injected */} +
+ {children} +
+
+
+ ); +}; + +export default DashboardLayout; \ No newline at end of file diff --git a/app/(admin)/adminDashboard/page.tsx b/app/(admin)/adminDashboard/page.tsx new file mode 100644 index 0000000..5f5aaeb --- /dev/null +++ b/app/(admin)/adminDashboard/page.tsx @@ -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(() => { + 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 ( + +
+

Admin Dashboard

+ {/* Top Section: Site Selector, Site Status, and KPI Table */} + {/* This grid will now properly arrange them into two columns on md screens and up */} +
+ {/* First Column: Site Selector and Site Status */} +
+ + +
+ {/* Second Column: KPI Table */} +
{/* This div will now be the second column */} + +
+
+ + {/* System Overview - Now covers the whole width */} + + + + {/* Charts Section */} +
+
+ +
+
+ +
+
+ +
+ + +
+
+
+ ); +}; + +export default AdminDashboard; + diff --git a/app/(admin)/sites/page.tsx b/app/(admin)/sites/page.tsx new file mode 100644 index 0000000..2f6d3de --- /dev/null +++ b/app/(admin)/sites/page.tsx @@ -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 = { + 'Site A': 'Active', + 'Site B': 'Inactive', + 'Site C': 'Faulty', + }; + return statusMap[siteName]; + }; + + return ( + +
+

All Sites Overview

+ +
+ {/* 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 ( + + ); + })} +
+
+
+ ); +}; + +export default SitesPage; \ No newline at end of file diff --git a/app/(defaults)/chint/inverters/[id]/page.tsx b/app/(defaults)/chint/inverters/[id]/page.tsx index e34f686..4b64183 100644 --- a/app/(defaults)/chint/inverters/[id]/page.tsx +++ b/app/(defaults)/chint/inverters/[id]/page.tsx @@ -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 ?

Loading...

: ( <> - +
{isMounted && ( diff --git a/app/(defaults)/layout.tsx b/app/(defaults)/layout.tsx index 6c27841..579f997 100644 --- a/app/(defaults)/layout.tsx +++ b/app/(defaults)/layout.tsx @@ -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 */} -
- - - - - {/* BEGIN SIDEBAR */} - - {/* END SIDEBAR */} -
- {/* BEGIN TOP NAVBAR */} -
- {/* END TOP NAVBAR */} - - {/* BEGIN CONTENT AREA */} - {children} - {/* END CONTENT AREA */} - - {/* BEGIN FOOTER */} -
- {/* END FOOTER */} - -
-
-
- - ); +interface DefaultLayoutProps { + children: ReactNode; } +const DefaultLayout: FC = ({ children }) => { + return ( +
+ + + + + +
+
+ {children} +
+ +
+
+
+ ); +}; + export default withAuth(DefaultLayout); + diff --git a/app/(defaults)/page.tsx b/app/(defaults)/page.tsx index 8d06fa8..3e8e4c7 100644 --- a/app/(defaults)/page.tsx +++ b/app/(defaults)/page.tsx @@ -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
starter page
; + const [selectedSite, setSelectedSite] = useState(''); + const sites = ['Site A', 'Site B', 'Site C']; // replace with your actual site list + + return ( +
+

+ Welcome to Rooftop Dashboard ! +

+

+ Select a site to get started. +

+
+ + + {selectedSite && ( +
+

You selected: {selectedSite}

+ +
+ + )} +
+
+ ); }; export default Sales; + diff --git a/app/layout.tsx b/app/layout.tsx index 8ba396c..ce1fc5c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 ( - + {children} diff --git a/components/dashboards/EnergyLineChart.tsx b/components/dashboards/EnergyLineChart.tsx new file mode 100644 index 0000000..50e8c7f --- /dev/null +++ b/components/dashboards/EnergyLineChart.tsx @@ -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(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 ( +
+
+
+

Power/Energy Consumption & Generation

+ +
+
+ +
+
+
+ ); +}; + +export default EnergyLineChart; + diff --git a/components/dashboards/KPIStatus.tsx b/components/dashboards/KPIStatus.tsx new file mode 100644 index 0000000..ffe7b7e --- /dev/null +++ b/components/dashboards/KPIStatus.tsx @@ -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 ( +
+

Monthly KPI

+ + + + + + + + + {data.map((row) => ( + + + + + ))} + +
KPIValue
{row.kpi}{row.value}
+
+ ); +}; + +export default KPI_Table; diff --git a/components/dashboards/MonthlyBarChart.tsx b/components/dashboards/MonthlyBarChart.tsx new file mode 100644 index 0000000..14d7d6c --- /dev/null +++ b/components/dashboards/MonthlyBarChart.tsx @@ -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 ( +
+

Monthly Energy Consumption

+ + + + + + + {monthlyData.map((entry, index) => ( + + ))} + + + +
+ ); +}; + +export default MonthlyBarChart; diff --git a/components/dashboards/SiteCard.tsx b/components/dashboards/SiteCard.tsx new file mode 100644 index 0000000..f38ec36 --- /dev/null +++ b/components/dashboards/SiteCard.tsx @@ -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 = ({ siteName, details, status }) => { + const statusColorClass = + status === 'Active' ? 'text-green-500' : + status === 'Inactive' ? 'text-orange-500' : + 'text-red-500'; + + return ( +
+

+ {siteName} +

+ +
+

Status:

+

{status}

+
+ +
+

Location:

+

{details.location}

+
+ +
+

Inverter Provider:

+

{details.inverterProvider}

+
+ +
+

Emergency Contact:

+

{details.emergencyContact}

+
+ +
+

Last Sync:

+

{details.lastSyncTimestamp}

+
+ + {/* New: View Dashboard Button */} + + View Dashboard + +
+ ); +}; + +export default SiteCard; \ No newline at end of file diff --git a/components/dashboards/SiteSelector.tsx b/components/dashboards/SiteSelector.tsx new file mode 100644 index 0000000..5dd6502 --- /dev/null +++ b/components/dashboards/SiteSelector.tsx @@ -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 ( +
+ + +
+ ); +}; + +export default SiteSelector; diff --git a/components/dashboards/SiteStatus.tsx b/components/dashboards/SiteStatus.tsx new file mode 100644 index 0000000..2e39ec1 --- /dev/null +++ b/components/dashboards/SiteStatus.tsx @@ -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 = { + 'Site A': 'Active', + 'Site B': 'Inactive', + 'Site C': 'Faulty', + }; + + return ( +
+

Site Details

+ + {/* Status */} +
+

Status:

+

+ {statusMap[selectedSite]} +

+
+ + {/* Location */} +
+

Location:

+

{location}

+
+ + {/* Inverter Provider */} +
+

Inverter Provider:

+

{inverterProvider}

+
+ + {/* Emergency Contact */} +
+

Emergency Contact:

+

{emergencyContact}

+
+ + {/* Last Sync */} +
+

Last Sync:

+

{lastSyncTimestamp}

+
+
+ ); +}; + +export default SiteStatus; diff --git a/components/dashboards/SystemOverview.tsx b/components/dashboards/SystemOverview.tsx new file mode 100644 index 0000000..079084b --- /dev/null +++ b/components/dashboards/SystemOverview.tsx @@ -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 = ({ + 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 ( +
+ {/* Top Status & Temperature Info */} +
+

Status:

+ + {/* Display checkmark only for 'Normal' status */} + {status === 'Normal'} + {status} + +
+

{temperature}

+ + {/* Main Visual Section for Icons, SVG Lines, and Power Metrics */} + {/* This container will control the overall aspect ratio and height of the visual elements */} +
+ + {/* SVG for drawing the connecting lines */} + {/* viewBox="0 0 1000 500" gives a 2:1 aspect ratio for internal coordinates. */} + + + {/* Define a reusable arrowhead marker */} + + {/* Fill with blue-400 color */} + + + + {/* 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 2: House (mid-right) to Grid Tower (mid-left) */} + {/* A relatively straight line between the house and grid tower */} + + + {/* Path 3: Grid Tower (top-left) to Solar Panel (top-left) */} + {/* This path roughly connects the grid to the solar panel */} + + + + {/* --- Icons (Images) --- */} + {/* Positioned using absolute CSS with percentages and translate for centering */} + {/* Solar Panel Icon */} +
+ Solar Panels +

{solarPower} kW

+
+ + {/* House Icon */} +
+ House +
+ + {/* Grid Tower Icon */} +
+ Grid Tower +
+ + {/* --- Power Metrics Texts --- */} + {/* Positioned relative to the main visual container */} +
+

Real-time power

+

{realTimePower} kW

+
+
+

Installed power

+

{installedPower} kWp

+
+
+
+ ); +}; + +export default SystemOverview; \ No newline at end of file diff --git a/components/layouts/sidebar-toggle.tsx b/components/layouts/sidebar-toggle.tsx new file mode 100644 index 0000000..2c95de8 --- /dev/null +++ b/components/layouts/sidebar-toggle.tsx @@ -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 ( + + ); +}; + +export default SidebarToggleButton; \ No newline at end of file diff --git a/components/layouts/sidebar.tsx b/components/layouts/sidebar.tsx index 86e07c8..956199a 100644 --- a/components/layouts/sidebar.tsx +++ b/components/layouts/sidebar.tsx @@ -62,11 +62,12 @@ const Sidebar = () => { }; return ( +