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