This commit is contained in:
parent
fce26a2bc4
commit
86682398db
@ -11,7 +11,7 @@ export default function LoginPage() {
|
||||
const [ready, setReady] = useState(false); // gate to avoid UI flash
|
||||
|
||||
// Use ONE client-exposed API env var everywhere
|
||||
const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
|
||||
const API = process.env.NEXT_PUBLIC_FASTAPI_URL;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
@ -55,7 +55,7 @@ const RegisterPage = (props: Props) => {
|
||||
<div className="mt-6 text-sm text-gray-200 dark:text-gray-300">
|
||||
Already have an account ?{" "}
|
||||
<Link
|
||||
href="/register"
|
||||
href="/login"
|
||||
className="text-yellow-400 font-semibold underline transition hover:text-white"
|
||||
>
|
||||
SIGN IN
|
||||
|
||||
@ -1,261 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import PanelCodeHighlight from '@/components/panel-code-highlight'
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import IconHome from '@/components/icon/icon-home';
|
||||
import IconUser from '@/components/icon/icon-user';
|
||||
import IconPhone from '@/components/icon/icon-phone';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useParams } from 'next/navigation';
|
||||
import axios from 'axios';
|
||||
|
||||
type Props = {}
|
||||
|
||||
const InverterViewPage = (props: Props) => {
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const params = useParams()
|
||||
const [inverter, setInverter] = useState<any>({})
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
console.log("res", res.data.data.devices[0])
|
||||
setInverter(res.data.data.devices[0])
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{loading ? <p>Loading...</p> : (
|
||||
<>
|
||||
<PanelCodeHighlight title={params?.id?.toString() || ""}>
|
||||
<div className="mb-5">
|
||||
{isMounted && (
|
||||
<Tab.Group>
|
||||
<Tab.List className="mt-3 flex flex-wrap border-b border-white-light dark:border-[#191e3a]">
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
||||
Brief
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
||||
Chart
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<div className="active pt-5">
|
||||
<p className="mb-3 text-base font-semibold">Last Updated ( 2025-02-24 16:03:10 +0800 )</p>
|
||||
|
||||
<blockquote className="rounded-br-md rounded-tr-md border-l-2 !border-l-primary bg-white py-2 px-2 text-black dark:border-[#060818] dark:bg-[#060818]">
|
||||
<div className="flex items-start">
|
||||
<p className="m-0 font-semibold text-sm not-italic text-[#515365] dark:text-white-light">Basic Information</p>
|
||||
</div>
|
||||
</blockquote>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 text-gray-600 mt-3">
|
||||
<p><span className="font-semibold text-gray-400">Model: </span>{inverter.model}</p>
|
||||
<p><span className="font-semibold text-gray-400">SN: </span>{inverter.sn}</p>
|
||||
<p><span className="font-semibold text-gray-400">Total Energy: </span>{inverter.eTotalWithUnit}</p>
|
||||
<p><span className="font-semibold text-gray-400">Today Energy: </span>{inverter.eTodayWithUnit}</p>
|
||||
<p><span className="font-semibold text-gray-400">Reactive Power: </span>{inverter.lastRTP["Reactive Power"].value} var</p>
|
||||
<p><span className="font-semibold text-gray-400">Active Power: </span>{inverter.activePowerWithUnit}</p>
|
||||
<p><span className="font-semibold text-gray-400">Inverter Mode: </span>{inverter.lastRTP["Inverter Mode"].value}</p>
|
||||
<p><span className="font-semibold text-gray-400">Inner Temperature: </span>{inverter.lastRTP["Inner Temperature"].value} °C</p>
|
||||
<p><span className="font-semibold text-gray-400">Create Time: </span>{inverter.createdAtStr}</p>
|
||||
<p><span className="font-semibold text-gray-400">Modules: </span>{inverter.moduleFw.map((item: {module:string, value:string}) => `${item.module}: ${item.value}`.trim()).join(", ")}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>Chart</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
)}
|
||||
</div>
|
||||
</PanelCodeHighlight>
|
||||
|
||||
|
||||
<div className="panel pt-1 mt-3">
|
||||
{isMounted && (
|
||||
<Tab.Group>
|
||||
<Tab.List className="flex flex-wrap border-b border-white-light dark:border-[#191e3a]">
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
||||
INV-DC
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
||||
INV-AC
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
||||
Meter-AC
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
||||
Meter-Load
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<div className="active pt-5">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-200 text-gray-600 text-left">
|
||||
<th className="p-2"></th>
|
||||
<th className="p-2">Voltage(V)</th>
|
||||
<th className="p-2">Current(A)</th>
|
||||
<th className="p-2">Power(W)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b text-gray-600">
|
||||
<td className="p-2">PV1/PV1</td>
|
||||
<td className="p-2">{inverter.lastRTP["PV1 Voltage"] && inverter.lastRTP["PV1 Voltage"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["PV1 Current"] && inverter.lastRTP["PV1 Current"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["MPPT1 Power"] && inverter.lastRTP["MPPT1 Power"].value}</td>
|
||||
</tr>
|
||||
<tr className="border-b text-gray-600">
|
||||
<td className="p-2">PV2/PV2</td>
|
||||
<td className="p-2">{inverter.lastRTP["PV2 Voltage"] && inverter.lastRTP["PV2 Voltage"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["PV2 Current"] && inverter.lastRTP["PV2 Current"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["MPPT2 Power"] && inverter.lastRTP["MPPT2 Power"].value}</td>
|
||||
</tr>
|
||||
<tr className="border-b text-gray-600">
|
||||
<td className="p-2">PV3/PV3</td>
|
||||
<td className="p-2">{inverter.lastRTP["PV3 Voltage"] && inverter.lastRTP["PV3 Voltage"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["PV3 Current"] && inverter.lastRTP["PV3 Current"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["MPPT3 Power"] && inverter.lastRTP["MPPT3 Power"].value}</td>
|
||||
</tr>
|
||||
<tr className="border-b text-gray-600">
|
||||
<td className="p-2">PV3/PV3</td>
|
||||
<td className="p-2">{inverter.lastRTP["PV4 Voltage"] && inverter.lastRTP["PV4 Voltage"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["PV4 Current"] && inverter.lastRTP["PV4 Current"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["MPPT4 Power"] && inverter.lastRTP["MPPT4 Power"].value}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<div className="pt-5">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-200 text-gray-600 text-left">
|
||||
<th className="p-2"></th>
|
||||
<th className="p-2">Voltage(V)</th>
|
||||
<th className="p-2">Current(A)</th>
|
||||
<th className="p-2">Power(W)</th>
|
||||
<th className="p-2">Frequency(Hz)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b text-gray-600">
|
||||
<td className="p-2">A</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L1 Voltage"] && inverter.lastRTP["Phase L1 Voltage"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L1 Current"] && inverter.lastRTP["Phase L1 Current"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L1 Power"] && inverter.lastRTP["Phase L1 Power"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L1 Frequency"] && inverter.lastRTP["Phase L1 Frequency"].value}</td>
|
||||
</tr>
|
||||
<tr className="border-b text-gray-600">
|
||||
<td className="p-2">B</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L2 Voltage"] && inverter.lastRTP["Phase L2 Voltage"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L2 Current"] && inverter.lastRTP["Phase L2 Current"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L2 Power"] && inverter.lastRTP["Phase L2 Power"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L2 Frequency"] && inverter.lastRTP["Phase L2 Frequency"].value}</td>
|
||||
</tr>
|
||||
<tr className="border-b text-gray-600">
|
||||
<td className="p-2">C</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L3 Voltage"] && inverter.lastRTP["Phase L3 Voltage"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L3 Current"] && inverter.lastRTP["Phase L3 Current"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L3 Power"] && inverter.lastRTP["Phase L3 Power"].value}</td>
|
||||
<td className="p-2">{inverter.lastRTP["Phase L3 Frequency"] && inverter.lastRTP["Phase L3 Frequency"].value}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<div className="pt-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 text-gray-600 mt-3">
|
||||
<p><span className="font-semibold text-gray-400">Today import Energy: </span>{inverter.lastRTP["Today import Energy"] && inverter.lastRTP["Today import Energy"].value} kWh</p>
|
||||
<p><span className="font-semibold text-gray-400">L1-N phase voltage of grid: </span>{inverter.lastRTP["L1-N phase voltage of grid"] && inverter.lastRTP["L1-N phase voltage of grid"].value} V</p>
|
||||
<p><span className="font-semibold text-gray-400">L2-N phase voltage of grid: </span>{inverter.lastRTP["L2-N phase voltage of grid"] && inverter.lastRTP["L2-N phase voltage of grid"].value} V</p>
|
||||
<p><span className="font-semibold text-gray-400">L3-N phase voltage of grid: </span>{inverter.lastRTP["L3-N phase voltage of grid"] && inverter.lastRTP["L3-N phase voltage of grid"].value} V</p>
|
||||
<p><span className="font-semibold text-gray-400">Today export Energy: </span>{inverter.lastRTP["Today export Energy"] && inverter.lastRTP["Today export Energy"].value} kWh</p>
|
||||
<p><span className="font-semibold text-gray-400">L1 current of grid: </span>{inverter.lastRTP["L1 current of grid"] && inverter.lastRTP["L1 current of grid"].value} A</p>
|
||||
<p><span className="font-semibold text-gray-400">L2 current of grid: </span>{inverter.lastRTP["L2 current of grid"] && inverter.lastRTP["L2 current of grid"].value} A</p>
|
||||
<p><span className="font-semibold text-gray-400">L3 current of grid: </span>{inverter.lastRTP["L3 current of grid"] && inverter.lastRTP["L3 current of grid"].value} A</p>
|
||||
<p><span className="font-semibold text-gray-400">Accumulated energy of positive: </span>{inverter.lastRTP["Accumulated energy of positive"] && inverter.lastRTP["Accumulated energy of positive"].value} kWh</p>
|
||||
<p><span className="font-semibold text-gray-400">Phase L1 watt of grid: </span>{inverter.lastRTP["Phase L1 watt of grid"] && inverter.lastRTP["Phase L1 watt of grid"].value} KW</p>
|
||||
<p><span className="font-semibold text-gray-400">Phase L2 watt of grid: </span>{inverter.lastRTP["Phase L2 watt of grid"] && inverter.lastRTP["Phase L2 watt of grid"].value} KW</p>
|
||||
<p><span className="font-semibold text-gray-400">Phase L3 watt of grid: </span>{inverter.lastRTP["Phase L3 watt of grid"] && inverter.lastRTP["Phase L3 watt of grid"].value} KW</p>
|
||||
<p><span className="font-semibold text-gray-400">Accumulated energy of negative: </span>{inverter.lastRTP["Accumulated energy of negative"] && inverter.lastRTP["Accumulated energy of negative"].value} kWh</p>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<div className="pt-5">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 text-gray-600 mt-3">
|
||||
<p><span className="font-semibold text-gray-400">Today load Energy: </span>{inverter.lastRTP["Today load Energy"] && inverter.lastRTP["Today load Energy"].value} kWh</p>
|
||||
<p><span className="font-semibold text-gray-400">L1-N phase voltage of load: </span>{inverter.lastRTP["L1-N phase voltage of load"] && inverter.lastRTP["L1-N phase voltage of load"].value} V</p>
|
||||
<p><span className="font-semibold text-gray-400">L2-N phase voltage of load: </span>{inverter.lastRTP["L2-N phase voltage of load"] && inverter.lastRTP["L2-N phase voltage of load"].value} V</p>
|
||||
<p><span className="font-semibold text-gray-400">L3-N phase voltage of load: </span>{inverter.lastRTP["L3-N phase voltage of load"] && inverter.lastRTP["L3-N phase voltage of load"].value} V</p>
|
||||
<p><span className="font-semibold text-gray-400">Accumulated energy of load: </span>{inverter.lastRTP["Accumulated energy of load"] && inverter.lastRTP["Accumulated energy of load"].value} kWh</p>
|
||||
<p><span className="font-semibold text-gray-400">L1 current of load: </span>{inverter.lastRTP["L1 current of load"] && inverter.lastRTP["L1 current of load"].value} A</p>
|
||||
<p><span className="font-semibold text-gray-400">L2 current of load: </span>{inverter.lastRTP["L2 current of load"] && inverter.lastRTP["L2 current of load"].value} A</p>
|
||||
<p><span className="font-semibold text-gray-400">L3 current of load: </span>{inverter.lastRTP["L3 current of load"] && inverter.lastRTP["L3 current of load"].value} A</p>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InverterViewPage
|
||||
@ -1,174 +0,0 @@
|
||||
"use client";
|
||||
import IconTrashLines from '@/components/icon/icon-trash-lines';
|
||||
import PanelCodeHighlight from '@/components/panel-code-highlight';
|
||||
import ComponentsTablesSimple from '@/components/tables/components-tables-simple';
|
||||
import { formatUnixTimestamp } from '@/utils/helpers';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import axios from 'axios';
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
// import ReactApexChart from 'react-apexcharts';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Link from 'next/link';
|
||||
const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
||||
|
||||
type Props = {}
|
||||
|
||||
const SungrowInverters = (props: Props) => {
|
||||
const [inverters, setInverters] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await axios.get("https://api-a.fomware.com.cn/asset/v1/list?type=2", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + process.env.NEXT_PUBLIC_CHINT_TOKEN
|
||||
}
|
||||
})
|
||||
console.log("res", res.data.data.devices)
|
||||
setInverters(res.data.data.devices)
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
|
||||
const chartConfigs: any = {
|
||||
options: {
|
||||
chart: {
|
||||
height: 58,
|
||||
type: 'line',
|
||||
fontFamily: 'Nunito, sans-serif',
|
||||
sparkline: {
|
||||
enabled: true,
|
||||
},
|
||||
dropShadow: {
|
||||
enabled: true,
|
||||
blur: 3,
|
||||
color: '#009688',
|
||||
opacity: 0.4,
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 2,
|
||||
},
|
||||
colors: ['#009688'],
|
||||
grid: {
|
||||
padding: {
|
||||
top: 5,
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
right: 5,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
show: false,
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
formatter: () => {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// inverter status 0: initial, 1: standby, 2: fault, 3: running, 5: offline, 9: shutdown, 10: unknown
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading ? <p>Loading...</p> : (
|
||||
<PanelCodeHighlight title="Chint Inverters">
|
||||
<div className="table-responsive mb-5">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Inverter Name</th>
|
||||
<th>Site Name</th>
|
||||
<th>Gateway SN</th>
|
||||
<th>Inverter Status</th>
|
||||
<th>Model</th>
|
||||
<th>SN</th>
|
||||
<th>Real Time Power</th>
|
||||
<th>E-Today</th>
|
||||
<th>WeekData</th>
|
||||
<th>Created At</th>
|
||||
<th>Updated At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{inverters.map((data) => (
|
||||
<tr key={data.id}>
|
||||
<td>
|
||||
<div className="whitespace-nowrap"><Link href={`/chint/inverters/${data.name}`}>{data.name}</Link></div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="whitespace-nowrap">{data.siteName}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{data.gatewaySn}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className={`whitespace-nowrap ${
|
||||
data.status === 0 ? "text-gray-500" // Initial
|
||||
: data.status === 1 ? "text-blue-500" // Standby
|
||||
: data.status === 2 ? "text-red-500" // Fault
|
||||
: data.status === 3 ? "text-green-500" // Running
|
||||
: data.status === 5 ? "text-yellow-500" // Offline
|
||||
: data.status === 9 ? "text-purple-500" // Shutdown
|
||||
: "text-gray-400" // Unknown (default)
|
||||
}`}>
|
||||
{data.statusLabel}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{data.model}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{data.sn}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{data.activePowerWithUnit}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{data.eTodayWithUnit}</div>
|
||||
</td>
|
||||
<td>
|
||||
{isMounted && (
|
||||
<ReactApexChart
|
||||
series={[{ data: data.weekTrend.map((point: any) => point.y) }]}
|
||||
options={{
|
||||
...chartConfigs.options,
|
||||
xaxis: { categories: data.weekTrend.map((point: any) => point.x) },
|
||||
}}
|
||||
type="line"
|
||||
height={58}
|
||||
width={'100%'}
|
||||
/>
|
||||
)} </td>
|
||||
<td>{formatUnixTimestamp(data.createdAt)}</td>
|
||||
<td>{formatUnixTimestamp(data.updatedAt)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</PanelCodeHighlight>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SungrowInverters
|
||||
@ -1,13 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import { Metadata } from 'next';
|
||||
import React from 'react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
|
||||
const SungrowIndex = async () => {
|
||||
return <div>SungrowIndex</div>;
|
||||
};
|
||||
|
||||
export default SungrowIndex;
|
||||
@ -1,39 +0,0 @@
|
||||
"use client";
|
||||
// app/(defaults)/sungrow/assets/page.tsx
|
||||
|
||||
import ComponentsTablesSimple from "@/components/tables/components-tables-simple";
|
||||
import axios from "axios";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const SungrowAssets = () => {
|
||||
const [sites, setSites] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await axios.get("https://api-a.fomware.com.cn/site/v1/list", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + process.env.NEXT_PUBLIC_CHINT_TOKEN
|
||||
}
|
||||
})
|
||||
console.log("res", res.data.data.siteInfos)
|
||||
setSites(res.data.data.siteInfos)
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading ? <p>Loading...</p> : <ComponentsTablesSimple tableData={sites} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SungrowAssets;
|
||||
@ -1,105 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import IconTrashLines from '@/components/icon/icon-trash-lines';
|
||||
import PanelCodeHighlight from '@/components/panel-code-highlight';
|
||||
import ComponentsTablesSimple from '@/components/tables/components-tables-simple'
|
||||
import { formatUnixTimestamp } from '@/utils/helpers';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import axios from 'axios';
|
||||
import React, { useEffect, useState } from "react"
|
||||
|
||||
type Props = {}
|
||||
|
||||
const SungrowPlant = (props: Props) => {
|
||||
const [sites, setSites] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSites = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/sungrow/site")
|
||||
const data = await res.json()
|
||||
console.log("data", data)
|
||||
setSites(data)
|
||||
} catch (error) {
|
||||
console.error("Error fetching inverters:", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchSites()
|
||||
}, [])
|
||||
|
||||
const statusLabels: Record<number, string> = {
|
||||
0: "Offline",
|
||||
1: "Normal",
|
||||
}
|
||||
const plantTypeLabel: Record<number, string> = {
|
||||
3: "Commercial PV",
|
||||
4: "Residential PV",
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading ? <p>Loading...</p> : (
|
||||
<PanelCodeHighlight title="Sungrow Sites">
|
||||
<div className="table-responsive mb-5">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Site Name</th>
|
||||
<th>Status</th>
|
||||
<th>Plant Type</th>
|
||||
{/* <th>Installed Power</th>
|
||||
<th>Real-time Power</th>
|
||||
<th>Yield Today</th>
|
||||
<th>Monthly Yield</th>
|
||||
<th>Annual Yield</th>
|
||||
<th>Total Yield</th>
|
||||
<th>Equivalent Hours</th>
|
||||
<th>Remarks</th> */}
|
||||
<th className="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sites.map((data) => (
|
||||
<tr key={data.id}>
|
||||
<td>
|
||||
<div className="whitespace-nowrap">{data.ps_name}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className={`whitespace-nowrap ${ data.online_status !== 1 ? "text-danger" : "text-success" }`} >
|
||||
{statusLabels[data.online_status] || "-"}
|
||||
</div>
|
||||
</td>
|
||||
<td>{plantTypeLabel[data.ps_type] || "-"}</td>
|
||||
{/* <td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td> */}
|
||||
<td className="text-center">
|
||||
<Tippy content="Delete">
|
||||
<button type="button">
|
||||
<IconTrashLines className="m-auto" />
|
||||
</button>
|
||||
</Tippy>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</PanelCodeHighlight>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SungrowPlant
|
||||
@ -1,22 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import axios from "axios";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const res = await axios.post("https://gateway.isolarcloud.com.hk/openapi/platform/queryPowerStationList", {
|
||||
"page": 1,
|
||||
"size": 10,
|
||||
"appkey": `${process.env.SUNGROW_APP_KEY}`
|
||||
} ,{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${process.env.SUNGROW_ACCESS_TOKEN}`,
|
||||
"x-access-key": `${process.env.SUNGROW_SECRET_KEY}`
|
||||
}
|
||||
})
|
||||
// console.log("res", res.data)
|
||||
return NextResponse.json(res.data.result_data.pageList)
|
||||
} catch (error) {
|
||||
console.error("API fetch error:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch inverters" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,8 @@ import { color } from 'html2canvas/dist/types/css/types/color';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import './datepicker-dark.css'; // custom dark mode styles
|
||||
import 'chartjs-adapter-date-fns';
|
||||
|
||||
|
||||
|
||||
ChartJS.register(zoomPlugin);
|
||||
@ -68,7 +70,6 @@ function powerSeriesToEnergySeries(
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
function groupTimeSeries(
|
||||
data: TimeSeriesEntry[],
|
||||
mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly',
|
||||
@ -82,11 +83,12 @@ function groupTimeSeries(
|
||||
|
||||
switch (mode) {
|
||||
case 'day': {
|
||||
// Snap to 5-minute buckets in local (KL) time
|
||||
const local = new Date(
|
||||
date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' })
|
||||
);
|
||||
const minute = local.getMinutes() < 30 ? 0 : 30;
|
||||
local.setMinutes(minute, 0, 0);
|
||||
const snappedMin = Math.floor(local.getMinutes() / 5) * 5;
|
||||
local.setMinutes(snappedMin, 0, 0);
|
||||
key = local.toISOString();
|
||||
break;
|
||||
}
|
||||
@ -125,7 +127,17 @@ function groupTimeSeries(
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ---- NEW: build a 5-minute time grid for the day view
|
||||
function buildTimeGrid(start: Date, end: Date, stepMinutes = 5): string[] {
|
||||
const grid: string[] = [];
|
||||
const t = new Date(start);
|
||||
t.setSeconds(0, 0);
|
||||
while (t.getTime() <= end.getTime()) {
|
||||
grid.push(new Date(t).toISOString());
|
||||
t.setTime(t.getTime() + stepMinutes * 60 * 1000);
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
||||
const chartRef = useRef<any>(null);
|
||||
@ -138,7 +150,6 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
||||
const LIVE_REFRESH_MS = 300000; // 5min when viewing a single day
|
||||
const SLOW_REFRESH_MS = 600000; // 10min for weekly/monthly/yearly
|
||||
|
||||
|
||||
const fetchAndSet = React.useCallback(async () => {
|
||||
const now = new Date();
|
||||
let start: Date;
|
||||
@ -174,15 +185,6 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
||||
const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd);
|
||||
setConsumption(res.consumption);
|
||||
setGeneration(res.generation);
|
||||
|
||||
// Forecast only needs updating for the selected day
|
||||
const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 25.67);
|
||||
const selectedDateStr = selectedDate.toISOString().split('T')[0];
|
||||
setForecast(
|
||||
forecastData
|
||||
.filter(({ time }: any) => time.startsWith(selectedDateStr))
|
||||
.map(({ time, forecast }: any) => ({ time, value: forecast }))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch energy timeseries:', error);
|
||||
}
|
||||
@ -222,23 +224,22 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
||||
};
|
||||
}, [fetchAndSet, viewMode]);
|
||||
|
||||
|
||||
function useIsDarkMode() {
|
||||
const [isDark, setIsDark] = useState(() =>
|
||||
typeof document !== 'undefined'
|
||||
? document.body.classList.contains('dark')
|
||||
: false
|
||||
);
|
||||
const [isDark, setIsDark] = useState(() =>
|
||||
typeof document !== 'undefined'
|
||||
? document.body.classList.contains('dark')
|
||||
: false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const check = () => setIsDark(document.body.classList.contains('dark'));
|
||||
const observer = new MutationObserver(check);
|
||||
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const check = () => setIsDark(document.body.classList.contains('dark'));
|
||||
const observer = new MutationObserver(check);
|
||||
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return isDark;
|
||||
}
|
||||
return isDark;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const now = new Date();
|
||||
@ -278,17 +279,17 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
||||
setGeneration(res.generation);
|
||||
|
||||
// ⬇️ ADD THIS here — fetch forecast
|
||||
const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 30.67);
|
||||
const selectedDateStr = selectedDate.toISOString().split('T')[0];
|
||||
const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 30.67);
|
||||
const selectedDateStr = selectedDate.toISOString().split('T')[0];
|
||||
|
||||
setForecast(
|
||||
forecastData
|
||||
.filter(({ time }) => time.startsWith(selectedDateStr)) // ✅ filter only selected date
|
||||
.map(({ time, forecast }) => ({
|
||||
time,
|
||||
value: forecast
|
||||
}))
|
||||
);
|
||||
setForecast(
|
||||
forecastData
|
||||
.filter(({ time }) => time.startsWith(selectedDateStr)) // ✅ filter only selected date
|
||||
.map(({ time, forecast }) => ({
|
||||
time,
|
||||
value: forecast
|
||||
}))
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch energy timeseries:', error);
|
||||
@ -300,48 +301,70 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
||||
|
||||
const isEnergyView = viewMode !== 'day';
|
||||
|
||||
// Convert to energy series for aggregated views
|
||||
const consumptionForGrouping = isEnergyView
|
||||
? powerSeriesToEnergySeries(consumption, 30)
|
||||
: consumption;
|
||||
// Convert to energy series for aggregated views
|
||||
const consumptionForGrouping = isEnergyView
|
||||
? powerSeriesToEnergySeries(consumption, 30)
|
||||
: consumption;
|
||||
|
||||
const generationForGrouping = isEnergyView
|
||||
? powerSeriesToEnergySeries(generation, 30)
|
||||
: generation;
|
||||
const generationForGrouping = isEnergyView
|
||||
? powerSeriesToEnergySeries(generation, 30)
|
||||
: generation;
|
||||
|
||||
const forecastForGrouping = isEnergyView
|
||||
? powerSeriesToEnergySeries(forecast, 60) // if forecast is hourly, guess 60
|
||||
: forecast;
|
||||
const forecastForGrouping = isEnergyView
|
||||
? powerSeriesToEnergySeries(forecast, 60) // if forecast is hourly, guess 60
|
||||
: forecast;
|
||||
|
||||
// Group: sum for energy views, mean for day view
|
||||
const groupedConsumption = groupTimeSeries(
|
||||
consumptionForGrouping,
|
||||
viewMode,
|
||||
isEnergyView ? 'sum' : 'mean'
|
||||
);
|
||||
// Group: sum for energy views, mean for day view
|
||||
const groupedConsumption = groupTimeSeries(
|
||||
consumptionForGrouping,
|
||||
viewMode,
|
||||
isEnergyView ? 'sum' : 'mean'
|
||||
);
|
||||
|
||||
const groupedGeneration = groupTimeSeries(
|
||||
generationForGrouping,
|
||||
viewMode,
|
||||
isEnergyView ? 'sum' : 'mean'
|
||||
);
|
||||
const groupedGeneration = groupTimeSeries(
|
||||
generationForGrouping,
|
||||
viewMode,
|
||||
isEnergyView ? 'sum' : 'mean'
|
||||
);
|
||||
|
||||
const groupedForecast = groupTimeSeries(
|
||||
forecastForGrouping,
|
||||
viewMode,
|
||||
isEnergyView ? 'sum' : 'mean'
|
||||
);
|
||||
const groupedForecast = groupTimeSeries(
|
||||
forecastForGrouping,
|
||||
viewMode,
|
||||
isEnergyView ? 'sum' : 'mean'
|
||||
);
|
||||
|
||||
const forecastMap = Object.fromEntries(groupedForecast.map(d => [d.time, d.value]));
|
||||
|
||||
const dataTimesDay = [
|
||||
...groupedConsumption.map(d => Date.parse(d.time)),
|
||||
...groupedGeneration.map(d => Date.parse(d.time)),
|
||||
...groupedForecast.map(d => Date.parse(d.time)),
|
||||
].filter(Number.isFinite).sort((a, b) => a - b);
|
||||
|
||||
const allTimes = Array.from(new Set([
|
||||
...groupedConsumption.map(d => d.time),
|
||||
...groupedGeneration.map(d => d.time),
|
||||
...groupedForecast.map(d => d.time),
|
||||
])).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
|
||||
|
||||
// ---- CHANGED: use a 5-minute grid for day view
|
||||
const dayGrid =
|
||||
viewMode === 'day'
|
||||
? (() => {
|
||||
const dayStart = startOfDay(selectedDate).getTime();
|
||||
const dayEnd = endOfDay(selectedDate).getTime();
|
||||
if (dataTimesDay.length) {
|
||||
const minT = Math.max(dayStart, dataTimesDay[0]);
|
||||
const maxT = Math.min(dayEnd, dataTimesDay[dataTimesDay.length - 1]);
|
||||
return buildTimeGrid(new Date(minT), new Date(maxT), 5)
|
||||
}
|
||||
// no data → keep full day
|
||||
return buildTimeGrid(new Date(dayStart), new Date(dayEnd), 5);
|
||||
})()
|
||||
: [];
|
||||
|
||||
|
||||
const unionTimes = Array.from(new Set([
|
||||
...groupedConsumption.map(d => d.time),
|
||||
...groupedGeneration.map(d => d.time),
|
||||
...groupedForecast.map(d => d.time),
|
||||
])).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
|
||||
|
||||
const allTimes = viewMode === 'day' ? dayGrid : unionTimes;
|
||||
|
||||
const consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value]));
|
||||
const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value]));
|
||||
@ -349,7 +372,39 @@ const groupedForecast = groupTimeSeries(
|
||||
const [startIndex, setStartIndex] = useState(0);
|
||||
const [endIndex, setEndIndex] = useState(allTimes.length - 1);
|
||||
|
||||
|
||||
// after allTimes, consumptionMap, generationMap, forecastMap
|
||||
const hasDataAt = (t: string) =>
|
||||
t in consumptionMap || t in generationMap || t in forecastMap;
|
||||
|
||||
const firstAvailableIndex = allTimes.findIndex(hasDataAt);
|
||||
const lastAvailableIndex = (() => {
|
||||
for (let i = allTimes.length - 1; i >= 0; i--) {
|
||||
if (hasDataAt(allTimes[i])) return i;
|
||||
}
|
||||
return -1;
|
||||
})();
|
||||
|
||||
const selectableIndices =
|
||||
firstAvailableIndex === -1 || lastAvailableIndex === -1
|
||||
? []
|
||||
: Array.from(
|
||||
{ length: lastAvailableIndex - firstAvailableIndex + 1 },
|
||||
(_, k) => firstAvailableIndex + k
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectableIndices.length === 0) {
|
||||
setStartIndex(0);
|
||||
setEndIndex(Math.max(0, allTimes.length - 1));
|
||||
return;
|
||||
}
|
||||
const minIdx = selectableIndices[0];
|
||||
const maxIdx = selectableIndices[selectableIndices.length - 1];
|
||||
setStartIndex(prev => Math.min(Math.max(prev, minIdx), maxIdx));
|
||||
setEndIndex(prev => Math.min(Math.max(prev, minIdx), maxIdx));
|
||||
}, [viewMode, allTimes.length, firstAvailableIndex, lastAvailableIndex]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
import('hammerjs');
|
||||
@ -357,9 +412,18 @@ const groupedForecast = groupTimeSeries(
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectableIndices.length) {
|
||||
const minIdx = selectableIndices[0];
|
||||
const maxIdx = selectableIndices[selectableIndices.length - 1];
|
||||
setStartIndex(minIdx);
|
||||
setEndIndex(maxIdx);
|
||||
} else {
|
||||
setStartIndex(0);
|
||||
setEndIndex(allTimes.length - 1);
|
||||
}, [viewMode, allTimes.length]);
|
||||
setEndIndex(Math.max(0, allTimes.length - 1));
|
||||
}
|
||||
// run whenever mode changes or the timeline changes
|
||||
}, [viewMode, allTimes, firstAvailableIndex, lastAvailableIndex]);
|
||||
|
||||
|
||||
const formatLabel = (key: string) => {
|
||||
switch (viewMode) {
|
||||
@ -380,35 +444,38 @@ const groupedForecast = groupTimeSeries(
|
||||
};
|
||||
|
||||
const filteredLabels = allTimes.slice(startIndex, endIndex + 1);
|
||||
const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? 0);
|
||||
const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? 0);
|
||||
const filteredForecast = filteredLabels.map(t => forecastMap[t] ?? null);
|
||||
|
||||
// ---- CHANGED: use nulls for missing buckets (not zeros)
|
||||
const filteredConsumption = filteredLabels.map(t => (t in consumptionMap ? consumptionMap[t] : null));
|
||||
const filteredGeneration = filteredLabels.map(t => (t in generationMap ? generationMap[t] : null));
|
||||
const filteredForecast = filteredLabels.map(t => (t in forecastMap ? forecastMap[t] : null));
|
||||
|
||||
const allValues = [...filteredConsumption, ...filteredGeneration].filter(v => v !== null) as number[];
|
||||
const allValues = [...filteredConsumption, ...filteredGeneration].filter(
|
||||
(v): v is number => v !== null
|
||||
);
|
||||
const maxValue = allValues.length > 0 ? Math.max(...allValues) : 0;
|
||||
const yAxisSuggestedMax = maxValue * 1.15;
|
||||
|
||||
const isDark = useIsDarkMode();
|
||||
|
||||
const axisColor = isDark ? '#fff' : '#222';
|
||||
const axisColor = isDark ? '#fff' : '#222';
|
||||
|
||||
function areaGradient(ctx: any, hex: string, alphaTop = 0.22, alphaBottom = 0.02) {
|
||||
const { ctx: g, chartArea } = ctx.chart;
|
||||
if (!chartArea) return hex; // initial render fallback
|
||||
const gradient = g.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
|
||||
// top more opaque → bottom fades out
|
||||
gradient.addColorStop(0, hex + Math.floor(alphaTop * 255).toString(16).padStart(2, '0'));
|
||||
gradient.addColorStop(1, hex + Math.floor(alphaBottom * 255).toString(16).padStart(2, '0'));
|
||||
return gradient;
|
||||
}
|
||||
function areaGradient(ctx: any, hex: string, alphaTop = 0.22, alphaBottom = 0.02) {
|
||||
const { ctx: g, chartArea } = ctx.chart;
|
||||
if (!chartArea) return hex; // initial render fallback
|
||||
const gradient = g.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
|
||||
// top more opaque → bottom fades out
|
||||
gradient.addColorStop(0, hex + Math.floor(alphaTop * 255).toString(16).padStart(2, '0'));
|
||||
gradient.addColorStop(1, hex + Math.floor(alphaBottom * 255).toString(16).padStart(2, '0'));
|
||||
return gradient;
|
||||
}
|
||||
|
||||
// Define colors for both light and dark modes
|
||||
const consumptionColor = isDark ? '#B80F0A' : '#EF4444'; // Example: Brighter red for dark mode
|
||||
const generationColor = isDark ? '#48A860' : '#22C55E'; // Example: Brighter green for dark mode
|
||||
const forecastColor = '#fcd913'; // A golden yellow that works well in both modes
|
||||
const yUnit = isEnergyView ? 'kWh' : 'kW';
|
||||
const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||
// Define colors for both light and dark modes
|
||||
const consumptionColor = isDark ? '#B80F0A' : '#EF4444'; // Example: Brighter red for dark mode
|
||||
const generationColor = isDark ? '#48A860' : '#22C55E'; // Example: Brighter green for dark mode
|
||||
const forecastColor = '#fcd913'; // A golden yellow that works well in both modes
|
||||
const yUnit = isEnergyView ? 'kWh' : 'kW';
|
||||
const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||
|
||||
const data = {
|
||||
labels: filteredLabels.map(formatLabel),
|
||||
@ -418,29 +485,38 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||
data: filteredConsumption,
|
||||
borderColor: consumptionColor,
|
||||
backgroundColor: (ctx: any) => areaGradient(ctx, consumptionColor),
|
||||
fill: true, // <-- fill under line
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
spanGaps: true,
|
||||
pointRadius: 1, // default is 3, make smaller
|
||||
pointHoverRadius: 4, // a bit bigger on hover
|
||||
borderWidth: 2,
|
||||
},
|
||||
{
|
||||
label: 'Generation',
|
||||
data: filteredGeneration,
|
||||
borderColor: generationColor,
|
||||
backgroundColor: (ctx: any) => areaGradient(ctx, generationColor),
|
||||
fill: true, // <-- fill under line
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
spanGaps: true,
|
||||
pointRadius: 1, // default is 3, make smaller
|
||||
pointHoverRadius: 4, // a bit bigger on hover
|
||||
borderWidth: 2,
|
||||
},
|
||||
{
|
||||
label: 'Forecasted Solar',
|
||||
data: filteredForecast,
|
||||
borderColor: '#fcd913', // orange
|
||||
backgroundColor: (ctx: any) => areaGradient(ctx, '#fcd913', 0.18, 0.03),
|
||||
tension: 0.4,
|
||||
borderDash: [5, 5], // dashed line to distinguish forecast
|
||||
fill: true,
|
||||
spanGaps: true,
|
||||
}
|
||||
label: 'Forecasted Solar',
|
||||
data: filteredForecast,
|
||||
borderColor: '#fcd913',
|
||||
backgroundColor: (ctx: any) => areaGradient(ctx, '#fcd913', 0.18, 0.03),
|
||||
tension: 0.4,
|
||||
borderDash: [5, 5],
|
||||
fill: true,
|
||||
spanGaps: true,
|
||||
pointRadius: 2, // default is 3, make smaller
|
||||
pointHoverRadius: 4, // a bit bigger on hover
|
||||
borderWidth: 2,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
@ -448,12 +524,12 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: axisColor, // legend text color
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: axisColor, // legend text color
|
||||
},
|
||||
},
|
||||
},
|
||||
zoom: {
|
||||
zoom: {
|
||||
wheel: { enabled: true },
|
||||
@ -463,23 +539,23 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||
pan: { enabled: true, mode: 'x' as const },
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
backgroundColor: isDark ? '#232b3e' : '#fff',
|
||||
titleColor: axisColor,
|
||||
bodyColor: axisColor,
|
||||
borderColor: isDark ? '#444' : '#ccc',
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
label: (ctx: any) => {
|
||||
const dsLabel = ctx.dataset.label || '';
|
||||
const val = ctx.parsed.y;
|
||||
return `${dsLabel}: ${val?.toFixed(2)} ${yUnit}`;
|
||||
enabled: true,
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
backgroundColor: isDark ? '#232b3e' : '#fff',
|
||||
titleColor: axisColor,
|
||||
bodyColor: axisColor,
|
||||
borderColor: isDark ? '#444' : '#ccc',
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
label: (ctx: any) => {
|
||||
const dsLabel = ctx.dataset.label || '';
|
||||
const val = ctx.parsed.y;
|
||||
return `${dsLabel}: ${val?.toFixed(2)} ${yUnit}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
@ -498,18 +574,17 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||
font: { weight: 'normal' as const },
|
||||
},
|
||||
ticks: {
|
||||
color: axisColor,
|
||||
},
|
||||
color: axisColor,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
suggestedMax: yAxisSuggestedMax,
|
||||
title: { display: true, text: yTitle, color: axisColor, font: { weight: 'normal' as const } },
|
||||
ticks: {
|
||||
color: axisColor,
|
||||
color: axisColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
} as const;
|
||||
|
||||
@ -519,7 +594,7 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||
|
||||
return (
|
||||
<div className="bg-white p-4 rounded-lg shadow-md dark:bg-rtgray-800 dark:text-white-light">
|
||||
<div className="h-98 w-full">
|
||||
<div className="h-98 w-full" onDoubleClick={handleResetZoom}>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-lg font-bold dark:text-white-light">Energy Consumption & Generation</h2>
|
||||
<button onClick={handleResetZoom} className="btn-primary px-8 py-2 text-sm">
|
||||
@ -548,13 +623,15 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||
const val = Number(e.target.value);
|
||||
setStartIndex(val <= endIndex ? val : endIndex);
|
||||
}}
|
||||
disabled={selectableIndices.length === 0}
|
||||
className="dark:bg-rtgray-700 border dark:border-rtgray-700 rounded p-1"
|
||||
>
|
||||
{allTimes.map((label, idx) => (
|
||||
<option key={idx} value={idx}>{formatLabel(label)}</option>
|
||||
{selectableIndices.map((absIdx) => (
|
||||
<option key={absIdx} value={absIdx}>{formatLabel(allTimes[absIdx])}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="font-medium ">
|
||||
To:{' '}
|
||||
<select
|
||||
@ -563,13 +640,15 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)';
|
||||
const val = Number(e.target.value);
|
||||
setEndIndex(val >= startIndex ? val : startIndex);
|
||||
}}
|
||||
disabled={selectableIndices.length === 0}
|
||||
className="dark:bg-rtgray-700 border dark:border-rtgray-700 rounded p-1"
|
||||
>
|
||||
{allTimes.map((label, idx) => (
|
||||
<option key={idx} value={idx}>{formatLabel(label)}</option>
|
||||
{selectableIndices.map((absIdx) => (
|
||||
<option key={absIdx} value={absIdx}>{formatLabel(allTimes[absIdx])}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="font-medium">
|
||||
View:{' '}
|
||||
<select
|
||||
@ -602,3 +681,4 @@ export default EnergyLineChart;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@ -22,6 +22,7 @@
|
||||
"axios": "^1.7.9",
|
||||
"bcrypt": "^5.1.1",
|
||||
"chart.js": "^4.4.9",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"cookie": "^1.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
@ -4794,6 +4795,16 @@
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-adapter-date-fns": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": ">=2.8.0",
|
||||
"date-fns": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-plugin-zoom": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz",
|
||||
@ -13074,6 +13085,12 @@
|
||||
"@kurkle/color": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"chartjs-adapter-date-fns": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||
"requires": {}
|
||||
},
|
||||
"chartjs-plugin-zoom": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz",
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
"axios": "^1.7.9",
|
||||
"bcrypt": "^5.1.1",
|
||||
"chart.js": "^4.4.9",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-plugin-zoom": "^2.2.0",
|
||||
"cookie": "^1.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user