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
 |   const [ready, setReady] = useState(false); // gate to avoid UI flash
 | ||||||
| 
 | 
 | ||||||
|   // Use ONE client-exposed API env var everywhere
 |   // 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(() => { |   useEffect(() => { | ||||||
|     let cancelled = false; |     let cancelled = false; | ||||||
|  | |||||||
| @ -55,7 +55,7 @@ const RegisterPage = (props: Props) => { | |||||||
|                             <div className="mt-6 text-sm text-gray-200 dark:text-gray-300"> |                             <div className="mt-6 text-sm text-gray-200 dark:text-gray-300"> | ||||||
|                                 Already have an account ?{" "} |                                 Already have an account ?{" "} | ||||||
|                                 <Link |                                 <Link | ||||||
|                                     href="/register" |                                     href="/login" | ||||||
|                                     className="text-yellow-400 font-semibold underline transition hover:text-white" |                                     className="text-yellow-400 font-semibold underline transition hover:text-white" | ||||||
|                                 > |                                 > | ||||||
|                                     SIGN IN |                                     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 DatePicker from 'react-datepicker'; | ||||||
| import 'react-datepicker/dist/react-datepicker.css'; | import 'react-datepicker/dist/react-datepicker.css'; | ||||||
| import './datepicker-dark.css'; // custom dark mode styles
 | import './datepicker-dark.css'; // custom dark mode styles
 | ||||||
|  | import 'chartjs-adapter-date-fns'; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| ChartJS.register(zoomPlugin); | ChartJS.register(zoomPlugin); | ||||||
| @ -68,7 +70,6 @@ function powerSeriesToEnergySeries( | |||||||
|   return out; |   return out; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| function groupTimeSeries( | function groupTimeSeries( | ||||||
|   data: TimeSeriesEntry[], |   data: TimeSeriesEntry[], | ||||||
|   mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly', |   mode: 'day' | 'daily' | 'weekly' | 'monthly' | 'yearly', | ||||||
| @ -82,11 +83,12 @@ function groupTimeSeries( | |||||||
| 
 | 
 | ||||||
|     switch (mode) { |     switch (mode) { | ||||||
|       case 'day': { |       case 'day': { | ||||||
|  |         // Snap to 5-minute buckets in local (KL) time
 | ||||||
|         const local = new Date( |         const local = new Date( | ||||||
|           date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' }) |           date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' }) | ||||||
|         ); |         ); | ||||||
|         const minute = local.getMinutes() < 30 ? 0 : 30; |         const snappedMin = Math.floor(local.getMinutes() / 5) * 5; | ||||||
|         local.setMinutes(minute, 0, 0); |         local.setMinutes(snappedMin, 0, 0); | ||||||
|         key = local.toISOString(); |         key = local.toISOString(); | ||||||
|         break; |         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 EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { | ||||||
|   const chartRef = useRef<any>(null); |   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 LIVE_REFRESH_MS = 300000;       // 5min when viewing a single day
 | ||||||
|   const SLOW_REFRESH_MS = 600000;      // 10min for weekly/monthly/yearly
 |   const SLOW_REFRESH_MS = 600000;      // 10min for weekly/monthly/yearly
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   const fetchAndSet = React.useCallback(async () => { |   const fetchAndSet = React.useCallback(async () => { | ||||||
|     const now = new Date(); |     const now = new Date(); | ||||||
|     let start: Date; |     let start: Date; | ||||||
| @ -174,15 +185,6 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { | |||||||
|       const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd); |       const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd); | ||||||
|       setConsumption(res.consumption); |       setConsumption(res.consumption); | ||||||
|       setGeneration(res.generation); |       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) { |     } catch (error) { | ||||||
|       console.error('Failed to fetch energy timeseries:', error); |       console.error('Failed to fetch energy timeseries:', error); | ||||||
|     } |     } | ||||||
| @ -222,23 +224,22 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { | |||||||
|     }; |     }; | ||||||
|   }, [fetchAndSet, viewMode]); |   }, [fetchAndSet, viewMode]); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   function useIsDarkMode() { |   function useIsDarkMode() { | ||||||
|   const [isDark, setIsDark] = useState(() => |     const [isDark, setIsDark] = useState(() => | ||||||
|     typeof document !== 'undefined' |       typeof document !== 'undefined' | ||||||
|       ? document.body.classList.contains('dark') |         ? document.body.classList.contains('dark') | ||||||
|       : false |         : false | ||||||
|   ); |     ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |     useEffect(() => { | ||||||
|     const check = () => setIsDark(document.body.classList.contains('dark')); |       const check = () => setIsDark(document.body.classList.contains('dark')); | ||||||
|     const observer = new MutationObserver(check); |       const observer = new MutationObserver(check); | ||||||
|     observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); |       observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); | ||||||
|     return () => observer.disconnect(); |       return () => observer.disconnect(); | ||||||
|   }, []); |     }, []); | ||||||
| 
 | 
 | ||||||
|   return isDark; |     return isDark; | ||||||
| } |   } | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     const now = new Date(); |     const now = new Date(); | ||||||
| @ -278,17 +279,17 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { | |||||||
|         setGeneration(res.generation); |         setGeneration(res.generation); | ||||||
| 
 | 
 | ||||||
|         // ⬇️ ADD THIS here — fetch forecast
 |         // ⬇️ ADD THIS here — fetch forecast
 | ||||||
|       const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 30.67); |         const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 30.67); | ||||||
|       const selectedDateStr = selectedDate.toISOString().split('T')[0]; |         const selectedDateStr = selectedDate.toISOString().split('T')[0]; | ||||||
| 
 | 
 | ||||||
|       setForecast( |         setForecast( | ||||||
|         forecastData |           forecastData | ||||||
|           .filter(({ time }) => time.startsWith(selectedDateStr))  // ✅ filter only selected date
 |             .filter(({ time }) => time.startsWith(selectedDateStr))  // ✅ filter only selected date
 | ||||||
|           .map(({ time, forecast }) => ({ |             .map(({ time, forecast }) => ({ | ||||||
|             time, |               time, | ||||||
|             value: forecast |               value: forecast | ||||||
|           })) |             })) | ||||||
|       ); |         ); | ||||||
| 
 | 
 | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         console.error('Failed to fetch energy timeseries:', error); |         console.error('Failed to fetch energy timeseries:', error); | ||||||
| @ -300,48 +301,70 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { | |||||||
| 
 | 
 | ||||||
|   const isEnergyView = viewMode !== 'day'; |   const isEnergyView = viewMode !== 'day'; | ||||||
| 
 | 
 | ||||||
| // Convert to energy series for aggregated views
 |   // Convert to energy series for aggregated views
 | ||||||
| const consumptionForGrouping = isEnergyView |   const consumptionForGrouping = isEnergyView | ||||||
|   ? powerSeriesToEnergySeries(consumption, 30) |     ? powerSeriesToEnergySeries(consumption, 30) | ||||||
|   : consumption; |     : consumption; | ||||||
| 
 | 
 | ||||||
| const generationForGrouping = isEnergyView |   const generationForGrouping = isEnergyView | ||||||
|   ? powerSeriesToEnergySeries(generation, 30) |     ? powerSeriesToEnergySeries(generation, 30) | ||||||
|   : generation; |     : generation; | ||||||
| 
 | 
 | ||||||
| const forecastForGrouping = isEnergyView |   const forecastForGrouping = isEnergyView | ||||||
|   ? powerSeriesToEnergySeries(forecast, 60) // if forecast is hourly, guess 60
 |     ? powerSeriesToEnergySeries(forecast, 60) // if forecast is hourly, guess 60
 | ||||||
|   : forecast; |     : forecast; | ||||||
| 
 | 
 | ||||||
| // Group: sum for energy views, mean for day view
 |   // Group: sum for energy views, mean for day view
 | ||||||
| const groupedConsumption = groupTimeSeries( |   const groupedConsumption = groupTimeSeries( | ||||||
|   consumptionForGrouping, |     consumptionForGrouping, | ||||||
|   viewMode, |     viewMode, | ||||||
|   isEnergyView ? 'sum' : 'mean' |     isEnergyView ? 'sum' : 'mean' | ||||||
| ); |   ); | ||||||
| 
 | 
 | ||||||
| const groupedGeneration = groupTimeSeries( |   const groupedGeneration = groupTimeSeries( | ||||||
|   generationForGrouping, |     generationForGrouping, | ||||||
|   viewMode, |     viewMode, | ||||||
|   isEnergyView ? 'sum' : 'mean' |     isEnergyView ? 'sum' : 'mean' | ||||||
| ); |   ); | ||||||
| 
 | 
 | ||||||
| const groupedForecast = groupTimeSeries( |   const groupedForecast = groupTimeSeries( | ||||||
|   forecastForGrouping, |     forecastForGrouping, | ||||||
|   viewMode, |     viewMode, | ||||||
|   isEnergyView ? 'sum' : 'mean' |     isEnergyView ? 'sum' : 'mean' | ||||||
| ); |   ); | ||||||
| 
 | 
 | ||||||
|   const forecastMap = Object.fromEntries(groupedForecast.map(d => [d.time, d.value])); |   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([ |   // ---- CHANGED: use a 5-minute grid for day view
 | ||||||
|   ...groupedConsumption.map(d => d.time), |   const dayGrid = | ||||||
|   ...groupedGeneration.map(d => d.time), |   viewMode === 'day' | ||||||
|   ...groupedForecast.map(d => d.time), |     ? (() => { | ||||||
| ])).sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); |         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 consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value])); | ||||||
|   const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value])); |   const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value])); | ||||||
| @ -349,6 +372,38 @@ const groupedForecast = groupTimeSeries( | |||||||
|   const [startIndex, setStartIndex] = useState(0); |   const [startIndex, setStartIndex] = useState(0); | ||||||
|   const [endIndex, setEndIndex] = useState(allTimes.length - 1); |   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(() => { |   useEffect(() => { | ||||||
|     if (typeof window !== 'undefined') { |     if (typeof window !== 'undefined') { | ||||||
| @ -357,9 +412,18 @@ const groupedForecast = groupTimeSeries( | |||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|  |   if (selectableIndices.length) { | ||||||
|  |     const minIdx = selectableIndices[0]; | ||||||
|  |     const maxIdx = selectableIndices[selectableIndices.length - 1]; | ||||||
|  |     setStartIndex(minIdx); | ||||||
|  |     setEndIndex(maxIdx); | ||||||
|  |   } else { | ||||||
|     setStartIndex(0); |     setStartIndex(0); | ||||||
|     setEndIndex(allTimes.length - 1); |     setEndIndex(Math.max(0, allTimes.length - 1)); | ||||||
|   }, [viewMode, allTimes.length]); |   } | ||||||
|  |   // run whenever mode changes or the timeline changes
 | ||||||
|  | }, [viewMode, allTimes, firstAvailableIndex, lastAvailableIndex]); | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|   const formatLabel = (key: string) => { |   const formatLabel = (key: string) => { | ||||||
|     switch (viewMode) { |     switch (viewMode) { | ||||||
| @ -380,35 +444,38 @@ const groupedForecast = groupTimeSeries( | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const filteredLabels = allTimes.slice(startIndex, endIndex + 1); |   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 maxValue = allValues.length > 0 ? Math.max(...allValues) : 0; | ||||||
|   const yAxisSuggestedMax = maxValue * 1.15; |   const yAxisSuggestedMax = maxValue * 1.15; | ||||||
| 
 | 
 | ||||||
|   const isDark = useIsDarkMode(); |   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) { |   function areaGradient(ctx: any, hex: string, alphaTop = 0.22, alphaBottom = 0.02) { | ||||||
|   const { ctx: g, chartArea } = ctx.chart; |     const { ctx: g, chartArea } = ctx.chart; | ||||||
|   if (!chartArea) return hex; // initial render fallback
 |     if (!chartArea) return hex; // initial render fallback
 | ||||||
|   const gradient = g.createLinearGradient(0, chartArea.top, 0, chartArea.bottom); |     const gradient = g.createLinearGradient(0, chartArea.top, 0, chartArea.bottom); | ||||||
|   // top more opaque → bottom fades out
 |     // top more opaque → bottom fades out
 | ||||||
|   gradient.addColorStop(0, hex + Math.floor(alphaTop * 255).toString(16).padStart(2, '0')); |     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')); |     gradient.addColorStop(1, hex + Math.floor(alphaBottom * 255).toString(16).padStart(2, '0')); | ||||||
|   return gradient; |     return gradient; | ||||||
| } |   } | ||||||
| 
 | 
 | ||||||
| // Define colors for both light and dark modes
 |   // Define colors for both light and dark modes
 | ||||||
| const consumptionColor = isDark ? '#B80F0A' : '#EF4444'; // Example: Brighter red for dark mode
 |   const consumptionColor = isDark ? '#B80F0A' : '#EF4444'; // Example: Brighter red for dark mode
 | ||||||
| const generationColor = isDark ? '#48A860' : '#22C55E'; // Example: Brighter green 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 forecastColor = '#fcd913'; // A golden yellow that works well in both modes
 | ||||||
| const yUnit = isEnergyView ? 'kWh' : 'kW'; |   const yUnit = isEnergyView ? 'kWh' : 'kW'; | ||||||
| const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)'; |   const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)'; | ||||||
| 
 | 
 | ||||||
|   const data = { |   const data = { | ||||||
|     labels: filteredLabels.map(formatLabel), |     labels: filteredLabels.map(formatLabel), | ||||||
| @ -418,29 +485,38 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)'; | |||||||
|         data: filteredConsumption, |         data: filteredConsumption, | ||||||
|         borderColor: consumptionColor, |         borderColor: consumptionColor, | ||||||
|         backgroundColor: (ctx: any) => areaGradient(ctx, consumptionColor), |         backgroundColor: (ctx: any) => areaGradient(ctx, consumptionColor), | ||||||
|         fill: true,                                   // <-- fill under line
 |         fill: true, | ||||||
|         tension: 0.4, |         tension: 0.4, | ||||||
|         spanGaps: true, |         spanGaps: true, | ||||||
|  |         pointRadius: 1,        // default is 3, make smaller
 | ||||||
|  |         pointHoverRadius: 4,   // a bit bigger on hover
 | ||||||
|  |         borderWidth: 2, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         label: 'Generation', |         label: 'Generation', | ||||||
|         data: filteredGeneration, |         data: filteredGeneration, | ||||||
|         borderColor: generationColor, |         borderColor: generationColor, | ||||||
|         backgroundColor: (ctx: any) => areaGradient(ctx, generationColor), |         backgroundColor: (ctx: any) => areaGradient(ctx, generationColor), | ||||||
|         fill: true,                                   // <-- fill under line
 |         fill: true, | ||||||
|         tension: 0.4, |         tension: 0.4, | ||||||
|         spanGaps: true, |         spanGaps: true, | ||||||
|  |         pointRadius: 1,        // default is 3, make smaller
 | ||||||
|  |         pointHoverRadius: 4,   // a bit bigger on hover
 | ||||||
|  |         borderWidth: 2, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|       label: 'Forecasted Solar', |         label: 'Forecasted Solar', | ||||||
|       data: filteredForecast, |         data: filteredForecast, | ||||||
|       borderColor: '#fcd913', // orange
 |         borderColor: '#fcd913', | ||||||
|       backgroundColor: (ctx: any) => areaGradient(ctx, '#fcd913', 0.18, 0.03), |         backgroundColor: (ctx: any) => areaGradient(ctx, '#fcd913', 0.18, 0.03), | ||||||
|       tension: 0.4, |         tension: 0.4, | ||||||
|       borderDash: [5, 5], // dashed line to distinguish forecast
 |         borderDash: [5, 5], | ||||||
|       fill: true, |         fill: true, | ||||||
|       spanGaps: 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, |     responsive: true, | ||||||
|     maintainAspectRatio: false, |     maintainAspectRatio: false, | ||||||
|     plugins: { |     plugins: { | ||||||
|     legend: { |       legend: { | ||||||
|       position: 'top', |         position: 'top', | ||||||
|       labels: { |         labels: { | ||||||
|         color: axisColor, // legend text color
 |           color: axisColor, // legend text color
 | ||||||
|  |         }, | ||||||
|       }, |       }, | ||||||
|     }, |  | ||||||
|       zoom: { |       zoom: { | ||||||
|         zoom: { |         zoom: { | ||||||
|           wheel: { enabled: true }, |           wheel: { enabled: true }, | ||||||
| @ -463,23 +539,23 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)'; | |||||||
|         pan: { enabled: true, mode: 'x' as const }, |         pan: { enabled: true, mode: 'x' as const }, | ||||||
|       }, |       }, | ||||||
|       tooltip: { |       tooltip: { | ||||||
|       enabled: true, |         enabled: true, | ||||||
|       mode: 'index', |         mode: 'index', | ||||||
|       intersect: false, |         intersect: false, | ||||||
|       backgroundColor: isDark ? '#232b3e' : '#fff', |         backgroundColor: isDark ? '#232b3e' : '#fff', | ||||||
|       titleColor: axisColor, |         titleColor: axisColor, | ||||||
|       bodyColor: axisColor, |         bodyColor: axisColor, | ||||||
|       borderColor: isDark ? '#444' : '#ccc', |         borderColor: isDark ? '#444' : '#ccc', | ||||||
|       borderWidth: 1, |         borderWidth: 1, | ||||||
|       callbacks: { |         callbacks: { | ||||||
|       label: (ctx: any) => { |           label: (ctx: any) => { | ||||||
|         const dsLabel = ctx.dataset.label || ''; |             const dsLabel = ctx.dataset.label || ''; | ||||||
|         const val = ctx.parsed.y; |             const val = ctx.parsed.y; | ||||||
|         return `${dsLabel}: ${val?.toFixed(2)} ${yUnit}`; |             return `${dsLabel}: ${val?.toFixed(2)} ${yUnit}`; | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     }, |  | ||||||
|     }, |  | ||||||
|     scales: { |     scales: { | ||||||
|       x: { |       x: { | ||||||
|         title: { |         title: { | ||||||
| @ -498,18 +574,17 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)'; | |||||||
|           font: { weight: 'normal' as const }, |           font: { weight: 'normal' as const }, | ||||||
|         }, |         }, | ||||||
|         ticks: { |         ticks: { | ||||||
|         color: axisColor, |           color: axisColor, | ||||||
|       }, |         }, | ||||||
|       }, |       }, | ||||||
|       y: { |       y: { | ||||||
|         beginAtZero: true, |         beginAtZero: true, | ||||||
|         suggestedMax: yAxisSuggestedMax, |         suggestedMax: yAxisSuggestedMax, | ||||||
|         title: { display: true, text: yTitle, color: axisColor, font: { weight: 'normal' as const } }, |         title: { display: true, text: yTitle, color: axisColor, font: { weight: 'normal' as const } }, | ||||||
|         ticks: { |         ticks: { | ||||||
|         color: axisColor, |           color: axisColor, | ||||||
|  |         }, | ||||||
|       }, |       }, | ||||||
|       }, |  | ||||||
|        |  | ||||||
|     }, |     }, | ||||||
|   } as const; |   } as const; | ||||||
| 
 | 
 | ||||||
| @ -519,7 +594,7 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)'; | |||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="bg-white p-4 rounded-lg shadow-md dark:bg-rtgray-800 dark:text-white-light"> |     <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"> |         <div className="flex justify-between items-center mb-2"> | ||||||
|           <h2 className="text-lg font-bold dark:text-white-light">Energy Consumption & Generation</h2> |           <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"> |           <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); |                 const val = Number(e.target.value); | ||||||
|                 setStartIndex(val <= endIndex ? val : endIndex); |                 setStartIndex(val <= endIndex ? val : endIndex); | ||||||
|               }} |               }} | ||||||
|  |               disabled={selectableIndices.length === 0} | ||||||
|               className="dark:bg-rtgray-700 border dark:border-rtgray-700 rounded p-1" |               className="dark:bg-rtgray-700 border dark:border-rtgray-700 rounded p-1" | ||||||
|             > |             > | ||||||
|               {allTimes.map((label, idx) => ( |               {selectableIndices.map((absIdx) => ( | ||||||
|                 <option key={idx} value={idx}>{formatLabel(label)}</option> |                 <option key={absIdx} value={absIdx}>{formatLabel(allTimes[absIdx])}</option> | ||||||
|               ))} |               ))} | ||||||
|             </select> |             </select> | ||||||
|           </label> |           </label> | ||||||
|  | 
 | ||||||
|           <label className="font-medium "> |           <label className="font-medium "> | ||||||
|             To:{' '} |             To:{' '} | ||||||
|             <select |             <select | ||||||
| @ -563,13 +640,15 @@ const yTitle = isEnergyView ? 'Energy (kWh)' : 'Power (kW)'; | |||||||
|                 const val = Number(e.target.value); |                 const val = Number(e.target.value); | ||||||
|                 setEndIndex(val >= startIndex ? val : startIndex); |                 setEndIndex(val >= startIndex ? val : startIndex); | ||||||
|               }} |               }} | ||||||
|  |               disabled={selectableIndices.length === 0} | ||||||
|               className="dark:bg-rtgray-700 border dark:border-rtgray-700 rounded p-1" |               className="dark:bg-rtgray-700 border dark:border-rtgray-700 rounded p-1" | ||||||
|             > |             > | ||||||
|               {allTimes.map((label, idx) => ( |               {selectableIndices.map((absIdx) => ( | ||||||
|                 <option key={idx} value={idx}>{formatLabel(label)}</option> |                 <option key={absIdx} value={absIdx}>{formatLabel(allTimes[absIdx])}</option> | ||||||
|               ))} |               ))} | ||||||
|             </select> |             </select> | ||||||
|           </label> |           </label> | ||||||
|  | 
 | ||||||
|           <label className="font-medium"> |           <label className="font-medium"> | ||||||
|             View:{' '} |             View:{' '} | ||||||
|             <select |             <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", |                 "axios": "^1.7.9", | ||||||
|                 "bcrypt": "^5.1.1", |                 "bcrypt": "^5.1.1", | ||||||
|                 "chart.js": "^4.4.9", |                 "chart.js": "^4.4.9", | ||||||
|  |                 "chartjs-adapter-date-fns": "^3.0.0", | ||||||
|                 "chartjs-plugin-zoom": "^2.2.0", |                 "chartjs-plugin-zoom": "^2.2.0", | ||||||
|                 "cookie": "^1.0.2", |                 "cookie": "^1.0.2", | ||||||
|                 "date-fns": "^4.1.0", |                 "date-fns": "^4.1.0", | ||||||
| @ -4794,6 +4795,16 @@ | |||||||
|                 "pnpm": ">=8" |                 "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": { |         "node_modules/chartjs-plugin-zoom": { | ||||||
|             "version": "2.2.0", |             "version": "2.2.0", | ||||||
|             "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", |             "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", | ||||||
| @ -13074,6 +13085,12 @@ | |||||||
|                 "@kurkle/color": "^0.3.0" |                 "@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": { |         "chartjs-plugin-zoom": { | ||||||
|             "version": "2.2.0", |             "version": "2.2.0", | ||||||
|             "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", |             "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ | |||||||
|         "axios": "^1.7.9", |         "axios": "^1.7.9", | ||||||
|         "bcrypt": "^5.1.1", |         "bcrypt": "^5.1.1", | ||||||
|         "chart.js": "^4.4.9", |         "chart.js": "^4.4.9", | ||||||
|  |         "chartjs-adapter-date-fns": "^3.0.0", | ||||||
|         "chartjs-plugin-zoom": "^2.2.0", |         "chartjs-plugin-zoom": "^2.2.0", | ||||||
|         "cookie": "^1.0.2", |         "cookie": "^1.0.2", | ||||||
|         "date-fns": "^4.1.0", |         "date-fns": "^4.1.0", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user