diff --git a/nextjs-interface/src/app/dashboard/esp/[espId]/page.tsx b/nextjs-interface/src/app/dashboard/esp/[espId]/page.tsx index e2f95cb..b48e856 100644 --- a/nextjs-interface/src/app/dashboard/esp/[espId]/page.tsx +++ b/nextjs-interface/src/app/dashboard/esp/[espId]/page.tsx @@ -108,7 +108,7 @@ export default function Page({ params }: { params: any }) { date?.from && date?.to ? differenceInDays(date.to, date.from) : 0; return ( -
+
{esp.name}
@@ -194,7 +194,7 @@ export default function Page({ params }: { params: any }) { diff --git a/nextjs-interface/src/app/dashboard/page.tsx b/nextjs-interface/src/app/dashboard/page.tsx index 65cac2a..f0a7f15 100644 --- a/nextjs-interface/src/app/dashboard/page.tsx +++ b/nextjs-interface/src/app/dashboard/page.tsx @@ -2,12 +2,19 @@ // import components import DataCircle from "@/app/ui/dashboard/DataCircle"; import { useAllEsp } from "@/lib/data"; +import DataGraph from "@/app/ui/dashboard/DataGraph"; export default function Page() { const ESPList = useAllEsp(); return ( <> -
+
+ +
+ +
+ +
{ESPList.map((esp, index) => ( ))} diff --git a/nextjs-interface/src/app/ui/dashboard/ChartElement.tsx b/nextjs-interface/src/app/ui/dashboard/ChartElement.tsx index 7386c07..394b2bd 100644 --- a/nextjs-interface/src/app/ui/dashboard/ChartElement.tsx +++ b/nextjs-interface/src/app/ui/dashboard/ChartElement.tsx @@ -160,7 +160,6 @@ export function ChartElement({ data }: { data: avgData[] }) { x={date} stroke="Gray" ifOverflow="extendDomain" - label={{ value: "Date change", position: "top", fill: "green" }} /> ))} diff --git a/nextjs-interface/src/app/ui/dashboard/DataCircle.tsx b/nextjs-interface/src/app/ui/dashboard/DataCircle.tsx index e3be941..952863c 100644 --- a/nextjs-interface/src/app/ui/dashboard/DataCircle.tsx +++ b/nextjs-interface/src/app/ui/dashboard/DataCircle.tsx @@ -13,7 +13,7 @@ export default function DataCircle({ esp }: { esp: any }) { {esp.name} - + diff --git a/nextjs-interface/src/app/ui/dashboard/DataGraph.tsx b/nextjs-interface/src/app/ui/dashboard/DataGraph.tsx new file mode 100644 index 0000000..4747e26 --- /dev/null +++ b/nextjs-interface/src/app/ui/dashboard/DataGraph.tsx @@ -0,0 +1,621 @@ +"use client"; + +import { Card, CardContent, CardTitle } from "@/components/ui/card"; +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; +import React, { useContext, useEffect, useState, Suspense } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import useFindIpById, { + useAllEsp, + useFetchAllData, + useFetchData, +} from "@/lib/data"; +import { differenceInDays, endOfWeek, format, startOfWeek } from "date-fns"; +import { DateRange } from "react-day-picker"; +import { DateRangeElement } from "@/app/ui/dashboard/DateRangeElement"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Check, CornerLeftDown, CornerRightDown } from "lucide-react"; +import { ThemeContext } from "@/lib/Theme"; +import { avgData } from "@/lib/context"; + +export default function DataGraph() { + const router = useRouter(); + + const [date, setDate] = useState(() => { + const now = new Date(); + return { + from: startOfWeek(now), + to: endOfWeek(now), + }; + }); + + const [selectedEsp, setSelectedEsp] = useState(undefined); + const [selectedEsp2, setSelectedEsp2] = useState( + undefined, + ); + const [selectedOption, setSelectedOption] = useState("1"); + + const dateRangeInDays = + date?.from && date?.to ? differenceInDays(date.to, date.from) : 0; + + const ip1 = useFindIpById(selectedEsp ? selectedEsp : ""); + const ip2 = useFindIpById(selectedEsp2 ? selectedEsp2 : ""); + + const from = date?.from ? format(date.from, "yyyy-MM-dd") : ""; + const to = date?.to ? format(date.to, "yyyy-MM-dd") : ""; + const [precision, setPrecision] = useState("Day"); + const esp = useAllEsp(); + + let allDataEsp1: avgData[] | null = useFetchData(precision, ip1, from, to); + let allDataEsp2: avgData[] | null = useFetchData(precision, ip2, from, to); + + const combinedDataMap = new Map(); + + const allDataEsp = useFetchAllData(precision, from, to); + + if (selectedOption === "1" && Array.isArray(allDataEsp)) { + allDataEsp.forEach((data) => { + if (!combinedDataMap.has(data.date)) { + combinedDataMap.set(data.date, { + date: data.date, + avg_temperature: [], + avg_humidity: [], + }); + } + const existingData = combinedDataMap.get(data.date); + existingData.avg_temperature.push(data.avg_temperature); + existingData.avg_humidity.push(data.avg_humidity); + combinedDataMap.set(data.date, existingData); + }); + } else { + if (Array.isArray(allDataEsp1)) { + allDataEsp1.forEach((data) => { + combinedDataMap.set(data.date, { + ...data, + avg_temperature_esp1: data.avg_temperature, + avg_humidity_esp1: data.avg_humidity, + }); + }); + } + + if (Array.isArray(allDataEsp2)) { + allDataEsp2.forEach((data) => { + if (combinedDataMap.has(data.date)) { + const existingData = combinedDataMap.get(data.date); + combinedDataMap.set(data.date, { + ...existingData, + avg_temperature_esp2: data.avg_temperature, + avg_humidity_esp2: data.avg_humidity, + }); + } else { + combinedDataMap.set(data.date, { + ...data, + avg_temperature_esp2: data.avg_temperature, + avg_humidity_esp2: data.avg_humidity, + }); + } + }); + } + } + + const combinedData = Array.from(combinedDataMap.values()); + + const { darkMode } = useContext(ThemeContext); + let textColor = darkMode ? "white" : ""; + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const esp1 = params.get("esp1"); + const esp2 = params.get("esp2"); + + if (esp1) setSelectedEsp(esp1); + if (esp2) setSelectedEsp2(esp2); + }, []); + + useEffect(() => { + const query = new URLSearchParams(window.location.search); + if (selectedEsp) query.set("esp1", selectedEsp); + if (selectedEsp2) query.set("esp2", selectedEsp2); + + if (selectedOption === "1") { + query.delete("esp1"); + query.delete("esp2"); + allDataEsp1 = []; + allDataEsp2 = []; + } + + if (dateRangeInDays > 7) { + setPrecision("Day"); + query.set("precision", "Day"); + } else { + query.set("precision", precision); + } + + router.push(`?${query.toString()}`); + }, [ + selectedEsp, + selectedEsp2, + selectedOption, + precision, + dateRangeInDays, + router, + ]); + + const handleDateChange = (newDate: DateRange | undefined) => { + const from = newDate?.from ? newDate.from.toISOString() : ""; + const to = newDate?.to ? newDate.to.toISOString() : ""; + + router.push(`?precision=${precision}&from=${from}&to=${to}`); + }; + + const handleSelect = (value: any) => { + const query = new URLSearchParams(window.location.search); + if (dateRangeInDays > 7 && value === "Hour") { + value = "Day"; + } + setPrecision(value); + query.set("precision", value); + router.push( + `?${query.toString()}&from=${date?.from?.toISOString()}&to=${date?.to?.toISOString()}`, + ); + }; + + const colorPalette = [ + "#1f77b4", // Bleu + "#ff7f0e", // Orange + "#2ca02c", // Vert + "#d62728", // Rouge + "#9467bd", // Violet + "#8c564b", // Marron + "#e377c2", // Rose + "#7f7f7f", // Gris + "#bcbd22", // Jaune-vert + "#17becf", // Bleu-vert + "#f4a582", // Pêche + "#d95f02", // Orange foncé + "#7570b3", // Bleu-violet + "#e7298a", // Rose-rouge + "#66a61e", // Vert clair + "#e6ab02", // Jaune + "#a6761d", // Terre cuite + "#6a3d9a", // Violet foncé + "#ff9896", // Rose pâle + "#f5b7b1", // Rose clair + "#d84d1f", // Rouge-orang + "#a6d854", // Vert clair + "#ffcc00", // Jaune doré + "#6c2c3b", // Brun foncé + "#c5b0d5", // Violet pâle + "#f7b7a3", // Pêche clair + "#1f77b4", // Bleu + "#8c6d31", // Bronze + "#d62728", // Rouge + "#e377c2", // Rose + "#7f7f7f", // Gris + "#bcbd22", // Jaune-vert + "#17becf", // Bleu-vert + ]; + + /* + const allData = useFetchData(precision, "1", from, to); + */ + + return ( +
+ Loading...
}> + + +

+ Comparison of Temperature and Humidity + + (From {from || "..."} to {to || "..."} ) + +

+
+ + + +
+
+ +
+ + {selectedOption === "2" && ( +
+
+ +
+ +
+ +
+
+ )} + {selectedOption === "1" && ( +
+

+ Comparer tout les esp +

+
+ )} + +
+ { + setDate(newDate); + handleDateChange(newDate); + }} + /> +
+ +
+ +
+
+ +
+ {selectedOption === "1" ? ( + Array.isArray(allDataEsp) && esp.length > 0 ? ( + esp.map((espItem, index) => { + const borderColor = colorPalette[index % colorPalette.length]; + return ( +
+

+ ESP {index + 1} +

+

+ {espItem.name} +

+
+ ); + }) + ) : ( +

+ ) + ) : ( +

+ )} +
+ +
+
+ {selectedEsp && selectedOption === "2" && ( +
+

+ ESP 1 +

+

+ {selectedEsp && + esp.find( + (espItem) => espItem.id.toString() === selectedEsp, + )?.name} +

+
+ )} + + {selectedEsp2 && selectedOption === "2" && ( +
+

+ ESP 2 +

+

+ {selectedEsp2 && + esp.find( + (espItem) => espItem.id.toString() === selectedEsp2, + )?.name} +

+
+ )} +
+
+ + +
+

+ Temperature +

+ + + + { + const date = new Date(value); + return `${date.toLocaleDateString()}\n${date.toLocaleTimeString()}`; + }} + tick={{ fill: textColor, fontSize: 12, width: 75, dy: 10 }} + height={60} + /> + + [ + `${value.toFixed(2)}`, + name, + ]} + labelFormatter={(label: string) => { + const date = new Date(label); + return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; + }} + /> + + + {selectedOption === "1" ? ( + Array.isArray(allDataEsp) && + esp.length > 0 && + esp.map((_, index) => ( + + )) + ) : ( + <> + espItem.id.toString() === selectedEsp, + )?.name || "ESP 1" + } + /> + espItem.id.toString() === selectedEsp2, + )?.name || "ESP 2" + } + /> + + )} + + +
+ +
+

+ Humidity +

+ + + + { + const date = new Date(value); + return `${date.toLocaleDateString()}\n${date.toLocaleTimeString()}`; + }} + tick={{ fill: textColor, fontSize: 12, width: 75, dy: 10 }} + height={60} + /> + + [ + `${value.toFixed(2)}`, + name, + ]} + labelFormatter={(label: string) => { + const date = new Date(label); + return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; + }} + /> + + {selectedOption === "1" ? ( + Array.isArray(allDataEsp) && + esp.length > 0 && + esp.map((_, index) => ( + + )) + ) : ( + <> + espItem.id.toString() === selectedEsp, + )?.name || "ESP 1" + } + /> + espItem.id.toString() === selectedEsp2, + )?.name || "ESP 2" + } + /> + + )} + + +
+
+
+ +
+ ); +} diff --git a/nextjs-interface/src/app/ui/dashboard/DateRangeElement.tsx b/nextjs-interface/src/app/ui/dashboard/DateRangeElement.tsx index a878073..231b648 100644 --- a/nextjs-interface/src/app/ui/dashboard/DateRangeElement.tsx +++ b/nextjs-interface/src/app/ui/dashboard/DateRangeElement.tsx @@ -27,7 +27,7 @@ export function DateRangeElement({ id="date" variant={"outline"} className={cn( - "w-[300px] justify-start text-left font-normal dark:border-zinc-700 dark:bg-zinc-900", + "w-fit justify-start text-left font-normal dark:border-zinc-700 dark:bg-zinc-900", !date && "text-muted-foreground", )} > diff --git a/nextjs-interface/src/components/ui/select.tsx b/nextjs-interface/src/components/ui/select.tsx index 9218f68..efe1f75 100644 --- a/nextjs-interface/src/components/ui/select.tsx +++ b/nextjs-interface/src/components/ui/select.tsx @@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "flex h-10 w-fit items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className, )} {...props} diff --git a/nextjs-interface/src/lib/data.ts b/nextjs-interface/src/lib/data.ts index 6c59631..f43426c 100644 --- a/nextjs-interface/src/lib/data.ts +++ b/nextjs-interface/src/lib/data.ts @@ -1,6 +1,35 @@ import { useEffect, useState } from "react"; import { getToken, data, avgData, esp, user } from "@/lib/context"; +interface TemperatureAndHumidityData { + avg_temperature: number; + avg_humidity: number; +} + +export const useFetchTemperatureAndHumidity = ( + precision: string, + ip: string, + from: string, + to: string, +) => { + const [data, setData] = useState([]); + + useEffect(() => { + const url = `/postgrest/rpc/avg_date?delta=${precision}&ip=eq.${ip}&and=(date.gte.${from},date.lt.${to})`; + fetchWithAuth(url); + fetch(url, { headers: { Authorization: `Bearer ${getToken()}` } }) + .then((response) => response.json()) + .then((apiData: TemperatureAndHumidityData[]) => { + setData(apiData); + }) + .catch((e) => { + console.error("Une erreur s'est produite :", e); + }); + }, [from, ip, precision, to]); + + return data; +}; + export const fetchWithAuth = async (url: string) => { try { const response = await fetch(url, { @@ -42,6 +71,28 @@ export const useFetchData = ( return data; }; +export const useFetchAllData = ( + precision: string, + from: string, + to: string, +) => { + const [data, setData] = useState([]); + + useEffect(() => { + const url = `/postgrest/rpc/avg_date?delta=${precision}&and=(date.gte.${from},date.lt.${to})`; + fetchWithAuth(url); + fetch(url, { headers: { Authorization: `Bearer ${getToken()}` } }) + .then((response) => response.json()) + .then((apiData: avgData[]) => { + setData(apiData); + }) + .catch((e) => { + console.error("Une erreur s'est produite :", e); + }); + }, [from, precision, to]); + return data; +}; + export function useLastData(type: string, ip: string) { const [value, setValue] = useState(undefined);