Fixes sidebar logo and adds logo
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 787 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -12,7 +12,7 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Litoral Central",
|
"title": "Central LRX",
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 600
|
"height": 600
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 646 KiB |
@@ -17,7 +17,7 @@ import {
|
|||||||
Wind,
|
Wind,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import logo from "../../assets/logo.png";
|
import logo from "../../assets/logo5.png";
|
||||||
import type { AppPage } from "../../app/App";
|
import type { AppPage } from "../../app/App";
|
||||||
|
|
||||||
type SidebarProps = {
|
type SidebarProps = {
|
||||||
@@ -120,29 +120,36 @@ export function Sidebar({
|
|||||||
<aside
|
<aside
|
||||||
className={
|
className={
|
||||||
isDark
|
isDark
|
||||||
? `${collapsed ? "w-20" : "w-[245px]"} flex h-full min-h-0 flex-col border-r border-[#263247] bg-[#0B1220] text-slate-100 transition-all duration-200`
|
? `${collapsed ? "w-20" : "w-[280px]"} flex h-full min-h-0 flex-col border-r border-[#263247] bg-[#0B1220] text-slate-100 transition-all duration-200`
|
||||||
: `${collapsed ? "w-20" : "w-[245px]"} flex h-full min-h-0 flex-col border-r border-[#D7DEE8] bg-[#F3F6FA] text-[#0F172A] transition-all duration-200`
|
: `${collapsed ? "w-20" : "w-[280px]"} flex h-full min-h-0 flex-col border-r border-[#D7DEE8] bg-[#F3F6FA] text-[#0F172A] transition-all duration-200`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="shrink-0 px-4 pt-5">
|
<div className={collapsed ? "shrink-0 px-3 pt-5" : "shrink-0 px-4 pt-5"}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
collapsed
|
collapsed
|
||||||
? "mb-6 flex items-center justify-center"
|
? "mb-7 flex justify-center"
|
||||||
: "mb-6 flex items-center gap-3 px-1"
|
: isDark
|
||||||
|
? `relative mb-8 overflow-hidden ${RADIUS} border border-[#22314A] bg-[#101A2C] px-3.5 py-3.5`
|
||||||
|
: `relative mb-8 overflow-hidden ${RADIUS} border border-slate-200 bg-white px-3.5 py-3.5 shadow-sm`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{!collapsed && (
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(79,209,197,0.18),transparent_42%)]" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={collapsed ? "relative flex justify-center" : "relative flex items-center gap-3"}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isDark
|
isDark
|
||||||
? `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center border border-[#2A3950] bg-[#111A2B] p-1.5`
|
? "flex h-12 w-12 shrink-0 items-center justify-center rounded-[6px] bg-[#0B1220] ring-1 ring-white/10"
|
||||||
: `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center border border-[#CBD5E1] bg-white p-1.5`
|
: "flex h-12 w-12 shrink-0 items-center justify-center rounded-[6px] bg-slate-50 ring-1 ring-slate-200"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
alt="Litoral Central"
|
alt="Litoral Regas"
|
||||||
className="h-full w-full object-contain"
|
className="h-14 w-14 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -151,20 +158,29 @@ export function Sidebar({
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
isDark
|
isDark
|
||||||
? "truncate text-[15px] font-extrabold tracking-[-0.02em] text-white"
|
? "truncate text-[18px] font-black leading-none tracking-[-0.04em] text-white"
|
||||||
: "truncate text-[15px] font-extrabold tracking-[-0.02em] text-[#0F172A]"
|
: "truncate text-[18px] font-black leading-none tracking-[-0.04em] text-slate-950"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Litoral Central
|
Central LRX
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-1 whitespace-nowrap text-[10px] font-bold uppercase tracking-[0.18em] text-[#7D8EA8]">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
Operações agrícolas
|
<span
|
||||||
|
className={
|
||||||
|
isDark
|
||||||
|
? "truncate text-[11px] font-semibold text-[#8FA3BF]"
|
||||||
|
: "truncate text-[11px] font-semibold text-slate-500"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
CENTRO DE OPERAÇÕES
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav className="app-scrollbar min-h-0 flex-1 space-y-1.5 overflow-y-auto overflow-x-hidden px-4 pb-4">
|
<nav className="app-scrollbar min-h-0 flex-1 space-y-1.5 overflow-y-auto overflow-x-hidden px-4 pb-4">
|
||||||
<NavItem
|
<NavItem
|
||||||
@@ -443,8 +459,14 @@ function navButtonClass(isDark: boolean, active: boolean, collapsed: boolean) {
|
|||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
return isDark
|
return isDark
|
||||||
? `relative flex w-full items-center gap-3 ${RADIUS} border border-[#2C3D56] bg-[#131D2F] ${alignment} py-3.5 text-left text-[14px] font-extrabold text-white`
|
? `relative flex w-full items-center gap-3 ${RADIUS}
|
||||||
: `relative flex w-full items-center gap-3 ${RADIUS} border border-[#CBD5E1] bg-white ${alignment} py-3.5 text-left text-[14px] font-extrabold text-[#0F172A]`;
|
${alignment}
|
||||||
|
py-3.5 text-left text-[14px]
|
||||||
|
font-bold text-white`
|
||||||
|
: `relative flex w-full items-center gap-3 ${RADIUS}
|
||||||
|
${alignment}
|
||||||
|
py-3.5 text-left text-[14px]
|
||||||
|
font-bold text-[#0F172A]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isDark
|
return isDark
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import { Gauge, Thermometer, Waves } from "lucide-react";
|
|
||||||
import type { DashboardOverview } from "../../dashboard/types/DashboardOverview";
|
|
||||||
import { StatusPill } from "../../dashboard/components/StatusPill";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
zones: DashboardOverview["climate"]["zones"];
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatValue(value: number | null, unit: string, decimals = 1) {
|
|
||||||
if (value === null) return "--";
|
|
||||||
return `${value.toFixed(decimals)} ${unit}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DashboardClimateSection({ zones }: Props) {
|
|
||||||
return (
|
|
||||||
<section className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
Clima por Zona
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
|
||||||
Temperatura, humidade, CO₂ e estados principais dos equipamentos.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
|
||||||
{zones.map((zone) => (
|
|
||||||
<div
|
|
||||||
key={zone.zoneNumber}
|
|
||||||
className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900"
|
|
||||||
>
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<h3 className="text-base font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
Zona {zone.zoneNumber}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<StatusPill active={zone.fansOn} activeLabel="Vent." inactiveLabel="Vent." />
|
|
||||||
<StatusPill active={zone.extractorsOn} activeLabel="Extr." inactiveLabel="Extr." />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3">
|
|
||||||
<MiniValue icon={Thermometer} label="Temperatura" value={formatValue(zone.temperature, "°C")} />
|
|
||||||
<MiniValue icon={Waves} label="Humidade" value={formatValue(zone.humidity, "%", 0)} />
|
|
||||||
<MiniValue icon={Gauge} label="CO₂" value={formatValue(zone.co2, "ppm", 0)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5 grid grid-cols-2 gap-3 md:grid-cols-4">
|
|
||||||
<OpeningBar label="Zenital E" value={zone.zenitalLeftPercent} />
|
|
||||||
<OpeningBar label="Zenital D" value={zone.zenitalRightPercent} />
|
|
||||||
<OpeningBar label="Lateral E" value={zone.lateralLeftPercent} />
|
|
||||||
<OpeningBar label="Lateral D" value={zone.lateralRightPercent} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type MiniValueProps = {
|
|
||||||
icon: React.ElementType;
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function MiniValue({ icon: Icon, label, value }: MiniValueProps) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl bg-slate-50 p-3 dark:bg-slate-950">
|
|
||||||
<div className="mb-2 flex items-center gap-2 text-slate-500 dark:text-slate-400">
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
<span className="text-xs">{label}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
{value}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpeningBarProps = {
|
|
||||||
label: string;
|
|
||||||
value: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
function OpeningBar({ label, value }: OpeningBarProps) {
|
|
||||||
const safeValue = value ?? 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mb-1 flex justify-between text-xs text-slate-500 dark:text-slate-400">
|
|
||||||
<span>{label}</span>
|
|
||||||
<span>{value === null ? "--" : `${value.toFixed(0)}%`}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-2 rounded-full bg-slate-200 dark:bg-slate-800">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-full bg-blue-500"
|
|
||||||
style={{ width: `${safeValue}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -57,16 +57,20 @@ export function DashboardPage({ theme, onOpenMeteo, onNavigate }: DashboardPageP
|
|||||||
<section className="grid grid-cols-[minmax(0,1fr)_340px] items-start gap-6 xl:grid-cols-[minmax(0,1fr)_360px] 2xl:grid-cols-[minmax(0,1fr)_380px]">
|
<section className="grid grid-cols-[minmax(0,1fr)_340px] items-start gap-6 xl:grid-cols-[minmax(0,1fr)_360px] 2xl:grid-cols-[minmax(0,1fr)_380px]">
|
||||||
<div className="min-w-0 max-w-[640px]">
|
<div className="min-w-0 max-w-[640px]">
|
||||||
<h1 className={isDark ? "text-[42px] font-black leading-[1.02] tracking-[-0.06em] text-white xl:text-[48px] 2xl:text-[54px]" : "text-[42px] font-black leading-[1.02] tracking-[-0.06em] text-[#0F172A] xl:text-[48px] 2xl:text-[54px]"}>
|
<h1 className={isDark ? "text-[42px] font-black leading-[1.02] tracking-[-0.06em] text-white xl:text-[48px] 2xl:text-[54px]" : "text-[42px] font-black leading-[1.02] tracking-[-0.06em] text-[#0F172A] xl:text-[48px] 2xl:text-[54px]"}>
|
||||||
Bem-vindo ao
|
Bem-vindo à
|
||||||
<br />
|
<br />
|
||||||
<span className={isDark ? "text-[#4FD1C5]" : "text-[#0F766E]"}>Litoral Central</span>
|
<span className={isDark ? "text-[#4FD1C5]" : "text-[#0F766E]"}>
|
||||||
|
Central LRX
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className={isDark ? "mt-5 h-[2px] w-14 bg-[#4FD1C5]" : "mt-5 h-[2px] w-14 bg-[#0F766E]"} />
|
<div className={isDark ? "mt-5 h-[2px] w-14 bg-[#4FD1C5]" : "mt-5 h-[2px] w-14 bg-[#0F766E]"} />
|
||||||
|
|
||||||
<p className={isDark ? "mt-5 max-w-[560px] text-[14px] leading-6 text-[#A8B3C7] xl:text-[15px] xl:leading-7" : "mt-5 max-w-[560px] text-[14px] leading-6 text-slate-700 xl:text-[15px] xl:leading-7"}>
|
<p className={isDark ? "mt-5 max-w-[560px] text-[14px] leading-6 text-[#A8B3C7] xl:text-[15px] xl:leading-7" : "mt-5 max-w-[560px] text-[14px] leading-6 text-slate-700 xl:text-[15px] xl:leading-7"}>
|
||||||
A sua plataforma inteligente para gestão de operações agrícolas.
|
A plataforma central da Litoral Regas, onde dados, meteorologia,
|
||||||
Acompanhe as condições, controle os sistemas e maximize a eficiência no campo.
|
rega e operações convergem num único sistema inteligente.
|
||||||
|
O <strong>LRX</strong> representa o ponto de ligação entre tecnologia,
|
||||||
|
decisão e eficiência agrícola.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,388 +0,0 @@
|
|||||||
import { useMemo, useState } from "react";
|
|
||||||
import { BarChart3, Table2, X } from "lucide-react";
|
|
||||||
import {
|
|
||||||
Bar,
|
|
||||||
BarChart,
|
|
||||||
CartesianGrid,
|
|
||||||
ResponsiveContainer,
|
|
||||||
Tooltip,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
} from "recharts";
|
|
||||||
import type { ModuleSensorResponse } from "../../../types/meteo";
|
|
||||||
import type {
|
|
||||||
AccumulatedBucket,
|
|
||||||
AccumulatedRange,
|
|
||||||
} from "../hooks/useAccumulatedHistory";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
sensor: ModuleSensorResponse | null;
|
|
||||||
title: string;
|
|
||||||
theme: "dark" | "light";
|
|
||||||
buckets: AccumulatedBucket[];
|
|
||||||
loading: boolean;
|
|
||||||
range: AccumulatedRange;
|
|
||||||
onRangeChange: (range: AccumulatedRange) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RADIUS = "rounded-[5px]";
|
|
||||||
|
|
||||||
const RANGE_OPTIONS: Array<{ label: string; value: AccumulatedRange }> = [
|
|
||||||
{ label: "7D", value: "7d" },
|
|
||||||
{ label: "30D", value: "30d" },
|
|
||||||
{ label: "Mês", value: "month" },
|
|
||||||
{ label: "Ano", value: "year" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function AccumulatedHistoryModal({
|
|
||||||
sensor,
|
|
||||||
title,
|
|
||||||
theme,
|
|
||||||
buckets,
|
|
||||||
loading,
|
|
||||||
range,
|
|
||||||
onRangeChange,
|
|
||||||
onClose,
|
|
||||||
}: Props) {
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
const palette = accumulatedPalette(isDark);
|
|
||||||
const [mode, setMode] = useState<"chart" | "table">("chart");
|
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
|
||||||
if (buckets.length === 0) {
|
|
||||||
return { total: 0, average: 0, max: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = buckets.map((bucket) => bucket.total);
|
|
||||||
|
|
||||||
return {
|
|
||||||
total: values.reduce((sum, value) => sum + value, 0),
|
|
||||||
average: values.reduce((sum, value) => sum + value, 0) / values.length,
|
|
||||||
max: Math.max(...values),
|
|
||||||
};
|
|
||||||
}, [buckets]);
|
|
||||||
|
|
||||||
if (!sensor) return null;
|
|
||||||
|
|
||||||
const unit = buckets[0]?.unit ?? sensor.unit ?? "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-sm">
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `${RADIUS} flex h-[82vh] w-full max-w-6xl flex-col overflow-hidden border border-white/10 bg-[#111827] text-slate-100 shadow-2xl`
|
|
||||||
: `${RADIUS} flex h-[82vh] w-full max-w-6xl flex-col overflow-hidden border border-slate-200 bg-white text-slate-950 shadow-[0_24px_70px_rgba(15,23,42,0.18)]`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<header className="flex items-start justify-between gap-5 px-6 py-4">
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mb-1 text-[11px] font-bold uppercase tracking-[0.22em] text-slate-500"
|
|
||||||
: "mb-1 text-[11px] font-bold uppercase tracking-[0.22em] text-slate-400"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Acumulado
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-xl font-black tracking-[-0.03em]">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p className="mt-1 text-xs text-slate-500">
|
|
||||||
Chave: {sensor.key}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `${RADIUS} p-2 text-slate-400 transition hover:bg-white/5 hover:text-white`
|
|
||||||
: `${RADIUS} p-2 text-slate-400 transition hover:bg-slate-100 hover:text-slate-900`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<X className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="flex min-h-0 flex-1 flex-col px-6 pb-5">
|
|
||||||
<section
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `${RADIUS} flex min-h-0 flex-1 flex-col border border-white/10 bg-[#0b1220] p-4`
|
|
||||||
: `${RADIUS} flex min-h-0 flex-1 flex-col border border-slate-200 bg-slate-50 p-4`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="mb-4 flex flex-wrap items-center gap-2">
|
|
||||||
{RANGE_OPTIONS.map((option) => (
|
|
||||||
<button
|
|
||||||
key={option.value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onRangeChange(option.value)}
|
|
||||||
className={
|
|
||||||
range === option.value
|
|
||||||
? activeButtonClass(isDark)
|
|
||||||
: buttonClass(isDark)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div className="ml-auto flex gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMode("chart")}
|
|
||||||
className={toggleButtonClass(isDark, mode === "chart")}
|
|
||||||
>
|
|
||||||
<BarChart3 className="mr-2 inline h-4 w-4" />
|
|
||||||
Gráfico
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMode("table")}
|
|
||||||
className={toggleButtonClass(isDark, mode === "table")}
|
|
||||||
>
|
|
||||||
<Table2 className="mr-2 inline h-4 w-4" />
|
|
||||||
Tabela
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4 grid grid-cols-1 gap-3 sm:grid-cols-3">
|
|
||||||
<StatCard theme={theme} label="Total" value={formatValue(stats.total, unit)} />
|
|
||||||
<StatCard theme={theme} label="Média" value={formatValue(stats.average, unit)} />
|
|
||||||
<StatCard theme={theme} label="Máximo" value={formatValue(stats.max, unit)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-h-0 flex-1">
|
|
||||||
{loading ? (
|
|
||||||
<EmptyState>A carregar acumulados...</EmptyState>
|
|
||||||
) : buckets.length === 0 ? (
|
|
||||||
<EmptyState>Sem dados acumulados para este período.</EmptyState>
|
|
||||||
) : mode === "chart" ? (
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<BarChart
|
|
||||||
data={buckets}
|
|
||||||
margin={{ top: 16, right: 18, bottom: 8, left: 0 }}
|
|
||||||
>
|
|
||||||
<CartesianGrid
|
|
||||||
stroke={palette.grid}
|
|
||||||
strokeDasharray="4 6"
|
|
||||||
vertical={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<XAxis
|
|
||||||
dataKey="label"
|
|
||||||
tick={{ fill: palette.axis, fontSize: 11 }}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={{ stroke: palette.axisLine }}
|
|
||||||
minTickGap={20}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<YAxis
|
|
||||||
tick={{ fill: palette.axis, fontSize: 11 }}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={{ stroke: palette.axisLine }}
|
|
||||||
width={56}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
cursor={{ fill: palette.cursor }}
|
|
||||||
contentStyle={{
|
|
||||||
background: palette.tooltipBg,
|
|
||||||
border: `1px solid ${palette.tooltipBorder}`,
|
|
||||||
borderRadius: "5px",
|
|
||||||
color: palette.tooltipText,
|
|
||||||
boxShadow: isDark
|
|
||||||
? "0 18px 45px rgba(0,0,0,0.30)"
|
|
||||||
: "0 18px 45px rgba(15,23,42,0.14)",
|
|
||||||
}}
|
|
||||||
formatter={(value) => [
|
|
||||||
formatValue(Number(value), unit),
|
|
||||||
"Acumulado",
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Bar
|
|
||||||
dataKey="total"
|
|
||||||
fill={palette.bar}
|
|
||||||
radius={[3, 3, 0, 0]}
|
|
||||||
isAnimationActive={false}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `${RADIUS} h-full overflow-auto border border-white/10 bg-[#111827]`
|
|
||||||
: `${RADIUS} h-full overflow-auto border border-slate-200 bg-white`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<table className="w-full text-left text-sm">
|
|
||||||
<thead
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "sticky top-0 bg-[#111827] text-xs uppercase tracking-[0.14em] text-slate-500"
|
|
||||||
: "sticky top-0 bg-slate-50 text-xs uppercase tracking-[0.14em] text-slate-500"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3">Período</th>
|
|
||||||
<th className="px-4 py-3">Início</th>
|
|
||||||
<th className="px-4 py-3">Fim</th>
|
|
||||||
<th className="px-4 py-3 text-right">Total</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{buckets.map((bucket) => (
|
|
||||||
<tr
|
|
||||||
key={`${bucket.from}-${bucket.to}`}
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "border-t border-white/10 text-slate-300"
|
|
||||||
: "border-t border-slate-200 text-slate-700"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<td className="px-4 py-3 font-semibold">
|
|
||||||
{bucket.label}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-4 py-3 text-slate-500">
|
|
||||||
{formatDate(bucket.from)}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-4 py-3 text-slate-500">
|
|
||||||
{formatDate(bucket.to)}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "px-4 py-3 text-right font-black text-slate-100"
|
|
||||||
: "px-4 py-3 text-right font-black text-slate-950"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{formatValue(bucket.total, unit)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function accumulatedPalette(isDark: boolean) {
|
|
||||||
return isDark
|
|
||||||
? {
|
|
||||||
bar: "#94a3b8",
|
|
||||||
grid: "rgba(148,163,184,0.16)",
|
|
||||||
axis: "#64748b",
|
|
||||||
axisLine: "rgba(148,163,184,0.18)",
|
|
||||||
cursor: "rgba(148,163,184,0.08)",
|
|
||||||
tooltipBg: "#111827",
|
|
||||||
tooltipBorder: "rgba(255,255,255,0.10)",
|
|
||||||
tooltipText: "#e5e7eb",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
bar: "#475569",
|
|
||||||
grid: "#e2e8f0",
|
|
||||||
axis: "#64748b",
|
|
||||||
axisLine: "#cbd5e1",
|
|
||||||
cursor: "rgba(148,163,184,0.12)",
|
|
||||||
tooltipBg: "#ffffff",
|
|
||||||
tooltipBorder: "#e2e8f0",
|
|
||||||
tooltipText: "#0f172a",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyState({ children }: { children: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full items-center justify-center text-sm text-slate-400">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatCard({
|
|
||||||
theme,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
theme: "dark" | "light";
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}) {
|
|
||||||
const isDark = theme === "dark";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? `${RADIUS} border border-white/10 bg-[#111827] p-3`
|
|
||||||
: `${RADIUS} border border-slate-200 bg-white p-3`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<p className="text-xs text-slate-500">{label}</p>
|
|
||||||
|
|
||||||
<p
|
|
||||||
className={
|
|
||||||
isDark
|
|
||||||
? "mt-1 text-xl font-black text-slate-100"
|
|
||||||
: "mt-1 text-xl font-black text-slate-950"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buttonClass(isDark: boolean) {
|
|
||||||
return isDark
|
|
||||||
? `${RADIUS} border border-white/10 bg-white/[0.03] px-3 py-2 text-xs font-semibold text-slate-300 transition hover:bg-white/[0.06] hover:text-slate-100`
|
|
||||||
: `${RADIUS} border border-slate-200 bg-white px-3 py-2 text-xs font-semibold text-slate-600 transition hover:bg-slate-100 hover:text-slate-950`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function activeButtonClass(isDark: boolean) {
|
|
||||||
return isDark
|
|
||||||
? `${RADIUS} border border-white/10 bg-slate-200 px-3 py-2 text-xs font-black text-slate-950`
|
|
||||||
: `${RADIUS} border border-slate-300 bg-slate-900 px-3 py-2 text-xs font-black text-white`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleButtonClass(isDark: boolean, active: boolean) {
|
|
||||||
if (active) return activeButtonClass(isDark);
|
|
||||||
return buttonClass(isDark);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatValue(value: number, unit: string) {
|
|
||||||
if (unit === "Wh/m²" && value >= 1000) {
|
|
||||||
return `${(value / 1000).toFixed(2)} kWh/m²`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${value.toFixed(1)}${unit ? ` ${unit}` : ""}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(value: string) {
|
|
||||||
return new Date(value).toLocaleString("pt-PT", {
|
|
||||||
day: "2-digit",
|
|
||||||
month: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1246,13 +1246,15 @@ function RealtimeChartPanel({
|
|||||||
const hasAnyChartData = chart.variables.some(
|
const hasAnyChartData = chart.variables.some(
|
||||||
(variable) => variable.data.length > 0,
|
(variable) => variable.data.length > 0,
|
||||||
);
|
);
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={theme === "dark" ? "relative flex min-h-0 flex-1 flex-col text-slate-100" : "relative flex min-h-0 flex-1 flex-col text-slate-950"}>
|
<section className={`${panelClass(isDark)} relative min-h-0 p-0`}>
|
||||||
<div className="absolute right-3 top-3 z-20">
|
<div className="absolute right-3 top-3 z-20">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={
|
className={
|
||||||
theme === "dark"
|
isDark
|
||||||
? "rounded-[5px] border border-sky-400/20 bg-sky-400/10 px-3 py-2 text-xs font-black text-sky-200 transition hover:bg-sky-400/15"
|
? "rounded-[5px] border border-sky-400/20 bg-sky-400/10 px-3 py-2 text-xs font-black text-sky-200 transition hover:bg-sky-400/15"
|
||||||
: "rounded-[5px] border border-sky-300 bg-sky-50 px-3 py-2 text-xs font-black text-sky-700 transition hover:bg-sky-100"
|
: "rounded-[5px] border border-sky-300 bg-sky-50 px-3 py-2 text-xs font-black text-sky-700 transition hover:bg-sky-100"
|
||||||
}
|
}
|
||||||
@@ -1274,7 +1276,7 @@ function RealtimeChartPanel({
|
|||||||
onIntervalChange={setInterval}
|
onIntervalChange={setInterval}
|
||||||
/>
|
/>
|
||||||
</CompactMeteoChart>
|
</CompactMeteoChart>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,13 +1295,9 @@ function chartColor(accent: Accent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompactMeteoChart({
|
function CompactMeteoChart({ children }: { children: React.ReactNode }) {
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full [&>section]:h-full [&>section>header]:px-4 [&>section>header]:py-3 [&>section>main]:px-3 [&>section>main]:pb-3 [&_main_section>div.relative]:h-[235px] [&_main_section>div.relative]:min-h-[235px]">
|
<div className="h-full [&>section]:h-full [&>section]:border-0 [&>section]:bg-transparent [&>section]:shadow-none [&>section>header]:px-4 [&>section>header]:py-3 [&>section>main]:px-3 [&>section>main]:pb-3 [&_main_section>div.relative]:h-[235px] [&_main_section>div.relative]:min-h-[235px]">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||