Add dark/light theme toggle and Conectores section
- Add theme toggle button in TopMenu with Sun/Moon icons - Save theme preference to localStorage - Add dark mode CSS configuration with Tailwind @custom-variant - Apply dark mode classes to Home.tsx, TopMenu, and connector pages - Add new Conectores section in sidebar with Cable icon - Create placeholder pages for SH-METERS, XMETERS, and TTS connectors - Update App.tsx page types and routing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@@ -10,6 +10,9 @@ import UsersPage from "./pages/UsersPage";
|
|||||||
import RolesPage from "./pages/RolesPage";
|
import RolesPage from "./pages/RolesPage";
|
||||||
import ConsumptionPage from "./pages/consumption/ConsumptionPage";
|
import ConsumptionPage from "./pages/consumption/ConsumptionPage";
|
||||||
import AuditoriaPage from "./pages/AuditoriaPage";
|
import AuditoriaPage from "./pages/AuditoriaPage";
|
||||||
|
import SHMetersPage from "./pages/conectores/SHMetersPage";
|
||||||
|
import XMetersPage from "./pages/conectores/XMetersPage";
|
||||||
|
import TTSPage from "./pages/conectores/TTSPage";
|
||||||
import ProfileModal from "./components/layout/common/ProfileModal";
|
import ProfileModal from "./components/layout/common/ProfileModal";
|
||||||
import { updateMyProfile } from "./api/me";
|
import { updateMyProfile } from "./api/me";
|
||||||
|
|
||||||
@@ -40,7 +43,10 @@ export type Page =
|
|||||||
| "consumption"
|
| "consumption"
|
||||||
| "auditoria"
|
| "auditoria"
|
||||||
| "users"
|
| "users"
|
||||||
| "roles";
|
| "roles"
|
||||||
|
| "sh-meters"
|
||||||
|
| "xmeters"
|
||||||
|
| "tts";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [isAuth, setIsAuth] = useState<boolean>(false);
|
const [isAuth, setIsAuth] = useState<boolean>(false);
|
||||||
@@ -183,6 +189,12 @@ export default function App() {
|
|||||||
return <UsersPage />;
|
return <UsersPage />;
|
||||||
case "roles":
|
case "roles":
|
||||||
return <RolesPage />;
|
return <RolesPage />;
|
||||||
|
case "sh-meters":
|
||||||
|
return <SHMetersPage />;
|
||||||
|
case "xmeters":
|
||||||
|
return <XMetersPage />;
|
||||||
|
case "tts":
|
||||||
|
return <TTSPage />;
|
||||||
case "home":
|
case "home":
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
@@ -242,7 +254,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ✅ AQUÍ VA LA MARCA DE AGUA */}
|
{/* ✅ AQUÍ VA LA MARCA DE AGUA */}
|
||||||
<main className="relative min-w-0 flex-1 overflow-auto">
|
<main className="relative min-w-0 flex-1 overflow-auto bg-slate-50 dark:bg-gray-900">
|
||||||
<Watermark />
|
<Watermark />
|
||||||
<div className="relative z-10">{renderPage()}</div>
|
<div className="relative z-10">{renderPage()}</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ExpandLess,
|
ExpandLess,
|
||||||
Menu,
|
Menu,
|
||||||
People,
|
People,
|
||||||
|
Cable,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { Page } from "../../App";
|
import { Page } from "../../App";
|
||||||
import { getCurrentUserRole } from "../../api/auth";
|
import { getCurrentUserRole } from "../../api/auth";
|
||||||
@@ -17,6 +18,7 @@ interface SidebarProps {
|
|||||||
export default function Sidebar({ setPage }: SidebarProps) {
|
export default function Sidebar({ setPage }: SidebarProps) {
|
||||||
const [systemOpen, setSystemOpen] = useState(true);
|
const [systemOpen, setSystemOpen] = useState(true);
|
||||||
const [usersOpen, setUsersOpen] = useState(true);
|
const [usersOpen, setUsersOpen] = useState(true);
|
||||||
|
const [conectoresOpen, setConectoresOpen] = useState(true);
|
||||||
const [pinned, setPinned] = useState(false);
|
const [pinned, setPinned] = useState(false);
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
|
|
||||||
@@ -172,6 +174,55 @@ export default function Sidebar({ setPage }: SidebarProps) {
|
|||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* CONECTORES */}
|
||||||
|
{!isOperator && (
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => isExpanded && setConectoresOpen(!conectoresOpen)}
|
||||||
|
className="flex items-center w-full px-2 py-2 rounded-md hover:bg-white/10 font-bold"
|
||||||
|
>
|
||||||
|
<Cable className="w-5 h-5 shrink-0" />
|
||||||
|
{isExpanded && (
|
||||||
|
<>
|
||||||
|
<span className="ml-3 flex-1 text-left">
|
||||||
|
Conectores
|
||||||
|
</span>
|
||||||
|
{conectoresOpen ? <ExpandLess /> : <ExpandMore />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isExpanded && conectoresOpen && (
|
||||||
|
<ul className="mt-1 space-y-1 text-xs">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage("sh-meters")}
|
||||||
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
|
>
|
||||||
|
SH-METERS
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage("xmeters")}
|
||||||
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
|
>
|
||||||
|
XMETERS
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setPage("tts")}
|
||||||
|
className="pl-10 w-full text-left px-2 py-1.5 rounded-md hover:bg-white/10"
|
||||||
|
>
|
||||||
|
TTS
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Bell, User, LogOut } from "lucide-react";
|
import { Bell, User, LogOut, Sun, Moon } from "lucide-react";
|
||||||
import NotificationDropdown from "../NotificationDropdown";
|
import NotificationDropdown from "../NotificationDropdown";
|
||||||
import { useNotifications } from "../../hooks/useNotifications";
|
import { useNotifications } from "../../hooks/useNotifications";
|
||||||
import ProjectBadge from "./common/ProjectBadge";
|
import ProjectBadge from "./common/ProjectBadge";
|
||||||
@@ -33,11 +33,32 @@ const TopMenu: React.FC<TopMenuProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [openUserMenu, setOpenUserMenu] = useState(false);
|
const [openUserMenu, setOpenUserMenu] = useState(false);
|
||||||
const [openNotifications, setOpenNotifications] = useState(false);
|
const [openNotifications, setOpenNotifications] = useState(false);
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||||
|
return document.documentElement.classList.contains("dark");
|
||||||
|
});
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const notificationRef = useRef<HTMLDivElement | null>(null);
|
const notificationRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const { unreadCount } = useNotifications();
|
const { unreadCount } = useNotifications();
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
const newIsDark = !isDarkMode;
|
||||||
|
|
||||||
|
if (newIsDark) {
|
||||||
|
root.classList.add("dark");
|
||||||
|
} else {
|
||||||
|
root.classList.remove("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDarkMode(newIsDark);
|
||||||
|
|
||||||
|
// Save to localStorage
|
||||||
|
const settings = JSON.parse(localStorage.getItem("water_project_settings_v1") || "{}");
|
||||||
|
settings.theme = newIsDark ? "dark" : "light";
|
||||||
|
localStorage.setItem("water_project_settings_v1", JSON.stringify(settings));
|
||||||
|
};
|
||||||
|
|
||||||
const initials = useMemo(() => {
|
const initials = useMemo(() => {
|
||||||
const parts = (userName || "").trim().split(/\s+/).filter(Boolean);
|
const parts = (userName || "").trim().split(/\s+/).filter(Boolean);
|
||||||
const a = parts[0]?.[0] ?? "U";
|
const a = parts[0]?.[0] ?? "U";
|
||||||
@@ -100,6 +121,17 @@ const TopMenu: React.FC<TopMenuProps> = ({
|
|||||||
|
|
||||||
{/* DERECHA */}
|
{/* DERECHA */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Theme Toggle */}
|
||||||
|
<button
|
||||||
|
aria-label={isDarkMode ? "Cambiar a modo claro" : "Cambiar a modo oscuro"}
|
||||||
|
className="p-2 rounded-full hover:bg-white/10 transition"
|
||||||
|
type="button"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
title={isDarkMode ? "Modo claro" : "Modo oscuro"}
|
||||||
|
>
|
||||||
|
{isDarkMode ? <Sun size={20} /> : <Moon size={20} />}
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="relative" ref={notificationRef}>
|
<div className="relative" ref={notificationRef}>
|
||||||
<button
|
<button
|
||||||
aria-label="Notificaciones"
|
aria-label="Notificaciones"
|
||||||
@@ -148,14 +180,14 @@ const TopMenu: React.FC<TopMenuProps> = ({
|
|||||||
role="menu"
|
role="menu"
|
||||||
className="
|
className="
|
||||||
absolute right-0 mt-2 w-80
|
absolute right-0 mt-2 w-80
|
||||||
rounded-2xl bg-white border border-slate-200
|
rounded-2xl bg-white dark:bg-gray-800 border border-slate-200 dark:border-gray-700
|
||||||
shadow-xl overflow-hidden z-50
|
shadow-xl overflow-hidden z-50
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{/* Header usuario */}
|
{/* Header usuario */}
|
||||||
<div className="px-5 py-4 border-b border-slate-200">
|
<div className="px-5 py-4 border-b border-slate-200 dark:border-gray-700">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-11 h-11 rounded-full bg-slate-100 overflow-hidden flex items-center justify-center">
|
<div className="w-11 h-11 rounded-full bg-slate-100 dark:bg-gray-700 overflow-hidden flex items-center justify-center">
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<img
|
<img
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
@@ -163,22 +195,22 @@ const TopMenu: React.FC<TopMenuProps> = ({
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm font-semibold text-slate-700">
|
<span className="text-sm font-semibold text-slate-700 dark:text-gray-200">
|
||||||
{initials}
|
{initials}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-semibold text-slate-900 truncate">
|
<div className="text-sm font-semibold text-slate-900 dark:text-white truncate">
|
||||||
{userName}
|
{userName}
|
||||||
</div>
|
</div>
|
||||||
{userEmail ? (
|
{userEmail ? (
|
||||||
<div className="text-xs text-slate-500 truncate">
|
<div className="text-xs text-slate-500 dark:text-gray-400 truncate">
|
||||||
{userEmail}
|
{userEmail}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-slate-400 truncate">—</div>
|
<div className="text-xs text-slate-400 dark:text-gray-500 truncate">—</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,14 +225,14 @@ const TopMenu: React.FC<TopMenuProps> = ({
|
|||||||
left={<User size={16} />}
|
left={<User size={16} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="h-px bg-slate-200 my-1" />
|
<div className="h-px bg-slate-200 dark:bg-gray-700 my-1" />
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label="Cerrar sesión"
|
label="Cerrar sesión"
|
||||||
tone="danger"
|
tone="danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenUserMenu(false);
|
setOpenUserMenu(false);
|
||||||
onRequestLogout?.(); // ✅ abre confirm modal en App
|
onRequestLogout?.();
|
||||||
}}
|
}}
|
||||||
left={<LogOut size={16} />}
|
left={<LogOut size={16} />}
|
||||||
/>
|
/>
|
||||||
@@ -234,13 +266,13 @@ function MenuItem({
|
|||||||
className={[
|
className={[
|
||||||
"w-full flex items-center gap-3 px-5 py-3 text-sm text-left",
|
"w-full flex items-center gap-3 px-5 py-3 text-sm text-left",
|
||||||
"transition-colors",
|
"transition-colors",
|
||||||
disabled ? "opacity-40 cursor-not-allowed" : "hover:bg-slate-100",
|
disabled ? "opacity-40 cursor-not-allowed" : "hover:bg-slate-100 dark:hover:bg-gray-700",
|
||||||
tone === "danger"
|
tone === "danger"
|
||||||
? "text-red-600 hover:text-red-700"
|
? "text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||||
: "text-slate-700",
|
: "text-slate-700 dark:text-gray-200",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<span className="text-slate-400">{left}</span>
|
<span className="text-slate-400 dark:text-gray-500">{left}</span>
|
||||||
<span className="font-medium">{label}</span>
|
<span className="font-medium">{label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1 +1,23 @@
|
|||||||
@import 'tailwindcss'
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
/* Dark mode configuration */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
/* Base styles */
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode body */
|
||||||
|
body {
|
||||||
|
@apply bg-slate-50 text-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark body,
|
||||||
|
body:where(.dark *) {
|
||||||
|
@apply bg-gray-900 text-gray-100;
|
||||||
|
}
|
||||||
@@ -392,10 +392,10 @@ export default function Home({
|
|||||||
{/* ✅ Título + logo a la derecha */}
|
{/* ✅ Título + logo a la derecha */}
|
||||||
<div className="relative flex items-start justify-between gap-6">
|
<div className="relative flex items-start justify-between gap-6">
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<h1 className="text-3xl font-bold text-gray-800">
|
<h1 className="text-3xl font-bold text-gray-800 dark:text-white">
|
||||||
Sistema de Tomas de Agua
|
Sistema de Tomas de Agua
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 mt-2">
|
<p className="text-gray-600 dark:text-gray-300 mt-2">
|
||||||
Monitorea, administra y controla tus operaciones en un solo lugar.
|
Monitorea, administra y controla tus operaciones en un solo lugar.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -412,41 +412,44 @@ export default function Home({
|
|||||||
{/* Cards de Secciones */}
|
{/* Cards de Secciones */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div
|
<div
|
||||||
className="bg-white rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-blue-50 transition cursor-pointer"
|
className="bg-white dark:bg-gray-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-blue-50 dark:hover:bg-gray-700 transition cursor-pointer"
|
||||||
onClick={() => setPage("meters")}
|
onClick={() => setPage("meters")}
|
||||||
>
|
>
|
||||||
<Cpu size={40} className="text-blue-600" />
|
<Cpu size={40} className="text-blue-600" />
|
||||||
<span className="font-semibold text-gray-700">Tomas</span>
|
<span className="font-semibold text-gray-700 dark:text-gray-200">Tomas</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-red-50 transition">
|
<div
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-red-50 dark:hover:bg-gray-700 transition cursor-pointer"
|
||||||
|
onClick={() => setPage("auditoria")}
|
||||||
|
>
|
||||||
<Bell size={40} className="text-red-600" />
|
<Bell size={40} className="text-red-600" />
|
||||||
<span className="font-semibold text-gray-700">Alertas</span>
|
<span className="font-semibold text-gray-700 dark:text-gray-200">Alertas</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="cursor-pointer bg-white rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-yellow-50 transition"
|
<div className="cursor-pointer bg-white dark:bg-gray-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-yellow-50 dark:hover:bg-gray-700 transition"
|
||||||
onClick={() => setPage("projects")}
|
onClick={() => setPage("projects")}
|
||||||
>
|
>
|
||||||
<Settings size={40} className="text-yellow-600" />
|
<Settings size={40} className="text-yellow-600" />
|
||||||
<span className="font-semibold text-gray-700">Proyectos</span>
|
<span className="font-semibold text-gray-700 dark:text-gray-200">Proyectos</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-green-50 transition">
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-6 flex flex-col items-center justify-center gap-2 hover:bg-green-50 dark:hover:bg-gray-700 transition">
|
||||||
<BarChart3 size={40} className="text-green-600" />
|
<BarChart3 size={40} className="text-green-600" />
|
||||||
<span className="font-semibold text-gray-700">Reportes</span>
|
<span className="font-semibold text-gray-700 dark:text-gray-200">Reportes</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="bg-white rounded-xl shadow p-4">
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">Organismos Operadores</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Organismos Operadores</p>
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
Seleccionado:{" "}
|
Seleccionado:{" "}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold dark:text-gray-300">
|
||||||
{selectedOrganism === "Todos"
|
{selectedOrganism === "Todos"
|
||||||
? "Todos"
|
? "Todos"
|
||||||
: organismsData.find(o => o.id === selectedOrganism)?.name || "Ninguno"}
|
: organismsData.find(o => o.id === selectedOrganism)?.name || "Ninguno"}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -473,21 +476,21 @@ export default function Home({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Panel */}
|
{/* Panel */}
|
||||||
<div className="relative w-full max-w-2xl max-h-[90vh] bg-white rounded-xl shadow-2xl flex flex-col">
|
<div className="relative w-full max-w-2xl max-h-[90vh] bg-white dark:bg-gray-800 rounded-xl shadow-2xl flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-5 border-b flex items-start justify-between gap-3">
|
<div className="p-5 border-b dark:border-gray-700 flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-gray-800">
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-white">
|
||||||
Organismos Operadores
|
Organismos Operadores
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Selecciona un organismo para filtrar la información del dashboard
|
Selecciona un organismo para filtrar la información del dashboard
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-lg px-3 py-2 text-sm border border-gray-300 hover:bg-gray-50"
|
className="rounded-lg px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 dark:text-gray-300"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowOrganisms(false);
|
setShowOrganisms(false);
|
||||||
setOrganismQuery("");
|
setOrganismQuery("");
|
||||||
@@ -498,12 +501,12 @@ export default function Home({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="p-5 border-b">
|
<div className="p-5 border-b dark:border-gray-700">
|
||||||
<input
|
<input
|
||||||
value={organismQuery}
|
value={organismQuery}
|
||||||
onChange={(e) => setOrganismQuery(e.target.value)}
|
onChange={(e) => setOrganismQuery(e.target.value)}
|
||||||
placeholder="Buscar organismo…"
|
placeholder="Buscar organismo…"
|
||||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200"
|
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-500 dark:placeholder-gray-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -519,16 +522,16 @@ export default function Home({
|
|||||||
className={[
|
className={[
|
||||||
"rounded-xl border p-4 transition",
|
"rounded-xl border p-4 transition",
|
||||||
selectedOrganism === "Todos"
|
selectedOrganism === "Todos"
|
||||||
? "border-blue-600 bg-blue-50/40"
|
? "border-blue-600 bg-blue-50/40 dark:bg-blue-900/20"
|
||||||
: "border-gray-200 bg-white hover:bg-gray-50",
|
: "border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-gray-800">
|
<p className="text-sm font-semibold text-gray-800 dark:text-white">
|
||||||
Todos los Organismos
|
Todos los Organismos
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">Ver todos los datos del sistema</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400">Ver todos los datos del sistema</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-xs font-semibold px-2 py-1 rounded-full bg-blue-100 text-blue-700">
|
<span className="text-xs font-semibold px-2 py-1 rounded-full bg-blue-100 text-blue-700">
|
||||||
@@ -565,16 +568,16 @@ export default function Home({
|
|||||||
className={[
|
className={[
|
||||||
"rounded-xl border p-4 transition",
|
"rounded-xl border p-4 transition",
|
||||||
active
|
active
|
||||||
? "border-blue-600 bg-blue-50/40"
|
? "border-blue-600 bg-blue-50/40 dark:bg-blue-900/20"
|
||||||
: "border-gray-200 bg-white hover:bg-gray-50",
|
: "border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-gray-800">
|
<p className="text-sm font-semibold text-gray-800 dark:text-white">
|
||||||
{o.name}
|
{o.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">{o.region}</p>
|
<p className="text-xs text-gray-500 dark:text-gray-400">{o.region}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
@@ -591,36 +594,36 @@ export default function Home({
|
|||||||
|
|
||||||
<div className="mt-3 space-y-2 text-xs">
|
<div className="mt-3 space-y-2 text-xs">
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Rol</span>
|
<span className="text-gray-500 dark:text-gray-400">Rol</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-gray-200">
|
||||||
{o.contact}
|
{o.contact}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Email</span>
|
<span className="text-gray-500 dark:text-gray-400">Email</span>
|
||||||
<span className="font-medium text-gray-800 truncate max-w-[200px]">
|
<span className="font-medium text-gray-800 dark:text-gray-200 truncate max-w-[200px]">
|
||||||
{o.region}
|
{o.region}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Proyectos</span>
|
<span className="text-gray-500 dark:text-gray-400">Proyectos</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-gray-200">
|
||||||
{o.projects}
|
{o.projects}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Medidores</span>
|
<span className="text-gray-500 dark:text-gray-400">Medidores</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-gray-200">
|
||||||
{o.meters}
|
{o.meters}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex justify-between gap-2">
|
||||||
<span className="text-gray-500">Último acceso</span>
|
<span className="text-gray-500 dark:text-gray-400">Último acceso</span>
|
||||||
<span className="font-medium text-gray-800">
|
<span className="font-medium text-gray-800 dark:text-gray-200">
|
||||||
{o.lastSync}
|
{o.lastSync}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -651,14 +654,14 @@ export default function Home({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!loadingUsers && filteredOrganisms.length === 0 && (
|
{!loadingUsers && filteredOrganisms.length === 0 && (
|
||||||
<div className="text-sm text-gray-500 text-center py-10">
|
<div className="text-sm text-gray-500 dark:text-gray-400 text-center py-10">
|
||||||
No se encontraron organismos.
|
No se encontraron organismos.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="p-5 border-t text-xs text-gray-500">
|
<div className="p-5 border-t dark:border-gray-700 text-xs text-gray-500 dark:text-gray-400">
|
||||||
Mostrando {filteredOrganisms.length} organismo{filteredOrganisms.length !== 1 ? 's' : ''} de {users.length} total{users.length !== 1 ? 'es' : ''}
|
Mostrando {filteredOrganisms.length} organismo{filteredOrganisms.length !== 1 ? 's' : ''} de {users.length} total{users.length !== 1 ? 'es' : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -669,10 +672,10 @@ export default function Home({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Gráfica */}
|
{/* Gráfica */}
|
||||||
<div className="bg-white rounded-xl shadow p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-6">
|
||||||
<div className="flex items-center justify-between gap-4 mb-4">
|
<div className="flex items-center justify-between gap-4 mb-4">
|
||||||
<h2 className="text-lg font-semibold">
|
<h2 className="text-lg font-semibold dark:text-white">
|
||||||
Número de Medidores por Proyecto
|
Numero de Medidores por Proyecto
|
||||||
</h2>
|
</h2>
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">
|
||||||
Click en barra para ver tomas
|
Click en barra para ver tomas
|
||||||
@@ -681,20 +684,20 @@ export default function Home({
|
|||||||
|
|
||||||
{chartData.length === 0 && selectedOrganism !== "Todos" ? (
|
{chartData.length === 0 && selectedOrganism !== "Todos" ? (
|
||||||
<div className="h-60 flex flex-col items-center justify-center">
|
<div className="h-60 flex flex-col items-center justify-center">
|
||||||
<p className="text-sm text-gray-500 mb-2">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||||
{selectedUserProjectName
|
{selectedUserProjectName
|
||||||
? "Este organismo no tiene medidores registrados"
|
? "Este organismo no tiene medidores registrados"
|
||||||
: "Este organismo no tiene un proyecto asignado"}
|
: "Este organismo no tiene un proyecto asignado"}
|
||||||
</p>
|
</p>
|
||||||
{selectedUserProjectName && (
|
{selectedUserProjectName && (
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
Proyecto asignado: <span className="font-semibold">{selectedUserProjectName}</span>
|
Proyecto asignado: <span className="font-semibold dark:text-gray-300">{selectedUserProjectName}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : chartData.length === 0 ? (
|
) : chartData.length === 0 ? (
|
||||||
<div className="h-60 flex items-center justify-center">
|
<div className="h-60 flex items-center justify-center">
|
||||||
<p className="text-sm text-gray-500">No hay datos disponibles</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">No hay datos disponibles</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -715,14 +718,14 @@ export default function Home({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedOrganism !== "Todos" && selectedUserProjectName && (
|
{selectedOrganism !== "Todos" && selectedUserProjectName && (
|
||||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Proyecto del organismo:</span>
|
<span className="text-gray-500 dark:text-gray-400">Proyecto del organismo:</span>
|
||||||
<span className="ml-2 font-semibold text-gray-800">{selectedUserProjectName}</span>
|
<span className="ml-2 font-semibold text-gray-800 dark:text-white">{selectedUserProjectName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-500">Total de medidores:</span>
|
<span className="text-gray-500 dark:text-gray-400">Total de medidores:</span>
|
||||||
<span className="ml-2 font-semibold text-blue-600">{filteredMeters.length}</span>
|
<span className="ml-2 font-semibold text-blue-600">{filteredMeters.length}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -733,8 +736,8 @@ export default function Home({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isOperator && (
|
{!isOperator && (
|
||||||
<div className="bg-white rounded-xl shadow p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-6">
|
||||||
<h2 className="text-lg font-semibold mb-4">Historial Reciente de Auditoría</h2>
|
<h2 className="text-lg font-semibold mb-4 dark:text-white">Historial Reciente de Auditoria</h2>
|
||||||
{loadingAuditLogs ? (
|
{loadingAuditLogs ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
@@ -744,12 +747,12 @@ export default function Home({
|
|||||||
No hay registros de auditoría disponibles
|
No hay registros de auditoría disponibles
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-gray-200 max-h-60 overflow-y-auto">
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700 max-h-60 overflow-y-auto">
|
||||||
{history.map((h, i) => (
|
{history.map((h, i) => (
|
||||||
<li key={i} className="py-2 flex items-start gap-3">
|
<li key={i} className="py-2 flex items-start gap-3">
|
||||||
<span className="text-gray-400 mt-1">•</span>
|
<span className="text-gray-400 mt-1">•</span>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm text-gray-700">
|
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
<span className="font-semibold">{h.user}</span> {h.action}{" "}
|
<span className="font-semibold">{h.user}</span> {h.action}{" "}
|
||||||
<span className="font-medium">{h.target}</span>
|
<span className="font-medium">{h.target}</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -763,8 +766,8 @@ export default function Home({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isOperator && (
|
{!isOperator && (
|
||||||
<div className="bg-white rounded-xl shadow p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow p-6">
|
||||||
<h2 className="text-lg font-semibold mb-4">Últimas Alertas</h2>
|
<h2 className="text-lg font-semibold mb-4 dark:text-white">Ultimas Alertas</h2>
|
||||||
{loadingNotifications ? (
|
{loadingNotifications ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
@@ -774,11 +777,11 @@ export default function Home({
|
|||||||
No hay alertas disponibles
|
No hay alertas disponibles
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="divide-y divide-gray-200">
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{alerts.map((a, i) => (
|
{alerts.map((a, i) => (
|
||||||
<li key={i} className="py-2 flex justify-between items-start">
|
<li key={i} className="py-2 flex justify-between items-start">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<span className="text-sm text-gray-700">
|
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
<span className="font-semibold">{a.company}</span> - {a.type}
|
<span className="font-semibold">{a.company}</span> - {a.type}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
41
src/pages/conectores/SHMetersPage.tsx
Normal file
41
src/pages/conectores/SHMetersPage.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Radio } from "lucide-react";
|
||||||
|
|
||||||
|
export default function SHMetersPage() {
|
||||||
|
const [loading] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||||
|
<Radio className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">SH-METERS</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Conector para medidores SH</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Radio className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||||
|
Conector SH-METERS
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||||
|
Configuracion e integracion con medidores SH.
|
||||||
|
Esta seccion esta en desarrollo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/pages/conectores/TTSPage.tsx
Normal file
41
src/pages/conectores/TTSPage.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Wifi } from "lucide-react";
|
||||||
|
|
||||||
|
export default function TTSPage() {
|
||||||
|
const [loading] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
|
||||||
|
<Wifi className="w-6 h-6 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">TTS</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">The Things Stack - Integracion LoRaWAN</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Wifi className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||||
|
Conector TTS (The Things Stack)
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||||
|
Configuracion e integracion con The Things Stack para dispositivos LoRaWAN.
|
||||||
|
Esta seccion esta en desarrollo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/pages/conectores/XMetersPage.tsx
Normal file
41
src/pages/conectores/XMetersPage.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Gauge } from "lucide-react";
|
||||||
|
|
||||||
|
export default function XMetersPage() {
|
||||||
|
const [loading] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||||
|
<Gauge className="w-6 h-6 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">XMETERS</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Conector para medidores X</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Gauge className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||||
|
Conector XMETERS
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||||
|
Configuracion e integracion con medidores X.
|
||||||
|
Esta seccion esta en desarrollo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user