Migrar backend a PostgreSQL + Node.js/Express con nuevas funcionalidades

Backend (water-api/):
- Crear API REST completa con Express + TypeScript
- Implementar autenticación JWT con refresh tokens
- CRUD completo para: projects, concentrators, meters, gateways, devices, users, roles
- Agregar validación con Zod para todas las entidades
- Implementar webhooks para The Things Stack (LoRaWAN)
- Agregar endpoint de lecturas con filtros y resumen de consumo
- Implementar carga masiva de medidores via Excel (.xlsx)

Frontend:
- Crear cliente HTTP con manejo automático de JWT y refresh
- Actualizar todas las APIs para usar nuevo backend
- Agregar sistema de autenticación real (login, logout, me)
- Agregar selector de tipo (LORA, LoRaWAN, Grandes) en concentradores y medidores
- Agregar campo Meter ID en medidores
- Crear modal de carga masiva para medidores
- Agregar página de consumo con gráficas y filtros
- Corregir carga de proyectos independiente de datos existentes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Exteban08
2026-01-23 10:13:26 +00:00
parent 2b5735d78d
commit c81a18987f
92 changed files with 14088 additions and 1866 deletions

View File

@@ -8,6 +8,7 @@ import ConcentratorsPage from "./pages/concentrators/ConcentratorsPage";
import ProjectsPage from "./pages/projects/ProjectsPage";
import UsersPage from "./pages/UsersPage";
import RolesPage from "./pages/RolesPage";
import ConsumptionPage from "./pages/consumption/ConsumptionPage";
import ProfileModal from "./components/layout/common/ProfileModal";
import { updateMyProfile } from "./api/me";
@@ -18,7 +19,15 @@ import SettingsModal, {
import LoginPage from "./pages/LoginPage";
// ✅ NUEVO
// Auth imports
import {
isAuthenticated,
getMe,
logout as authLogout,
clearAuth,
type AuthUser,
} from "./api/auth";
import ConfirmModal from "./components/layout/common/ConfirmModal";
import Watermark from "./components/layout/common/Watermark";
@@ -27,28 +36,59 @@ export type Page =
| "projects"
| "meters"
| "concentrators"
| "consumption"
| "users"
| "roles";
const AUTH_KEY = "grh_auth";
export default function App() {
const [isAuth, setIsAuth] = useState<boolean>(() => {
return Boolean(localStorage.getItem(AUTH_KEY));
});
const [isAuth, setIsAuth] = useState<boolean>(false);
const [user, setUser] = useState<AuthUser | null>(null);
const [authLoading, setAuthLoading] = useState(true);
const handleLogin = (payload?: { token?: string }) => {
localStorage.setItem(
AUTH_KEY,
JSON.stringify({ token: payload?.token ?? "demo", ts: Date.now() })
);
setIsAuth(true);
// Check authentication on mount
useEffect(() => {
const checkAuth = async () => {
if (isAuthenticated()) {
try {
const userData = await getMe();
setUser(userData);
setIsAuth(true);
} catch {
clearAuth();
setIsAuth(false);
setUser(null);
}
}
setAuthLoading(false);
};
checkAuth();
}, []);
const handleLogin = () => {
// After successful login, fetch user data
const fetchUser = async () => {
try {
const userData = await getMe();
setUser(userData);
setIsAuth(true);
} catch {
clearAuth();
setIsAuth(false);
}
};
fetchUser();
};
const handleLogout = () => {
localStorage.removeItem(AUTH_KEY);
const handleLogout = async () => {
try {
await authLogout();
} catch {
// Ignore logout errors
}
clearAuth();
setUser(null);
setIsAuth(false);
// opcional: reset de navegación
// Reset navigation
setPage("home");
setSubPage("default");
setSelectedProject("");
@@ -65,13 +105,6 @@ export default function App() {
const [profileOpen, setProfileOpen] = useState(false);
const [savingProfile, setSavingProfile] = useState(false);
const [user, setUser] = useState({
name: "CESPT Admin",
email: "admin@cespt.gob.mx",
avatarUrl: null as string | null,
organismName: "CESPT",
});
const [settingsOpen, setSettingsOpen] = useState(false);
const [settings, setSettings] = useState<AppSettings>(() => loadSettings());
@@ -84,7 +117,7 @@ export default function App() {
const handleUploadAvatar = async (file: File) => {
const base64 = await fileToBase64(file);
localStorage.setItem("mock_avatar", base64);
setUser((prev) => ({ ...prev, avatarUrl: base64 }));
setUser((prev) => prev ? { ...prev, avatar_url: base64 } : null);
};
function fileToBase64(file: File) {
@@ -101,18 +134,17 @@ export default function App() {
email: string;
organismName?: string;
}) => {
if (!user) return;
setSavingProfile(true);
try {
const updated = await updateMyProfile(next);
setUser((prev) => ({
setUser((prev) => prev ? ({
...prev,
name: updated.name ?? next.name ?? prev.name,
email: updated.email ?? next.email ?? prev.email,
avatarUrl: updated.avatarUrl ?? prev.avatarUrl,
organismName:
updated.organismName ?? next.organismName ?? prev.organismName,
}));
avatar_url: updated.avatarUrl ?? prev.avatar_url,
}) : null);
setProfileOpen(false);
} finally {
@@ -141,6 +173,8 @@ export default function App() {
return <MetersPage selectedProject={selectedProject} />;
case "concentrators":
return <ConcentratorsPage />;
case "consumption":
return <ConsumptionPage />;
case "users":
return <UsersPage />;
case "roles":
@@ -159,6 +193,15 @@ export default function App() {
}
};
// Show loading while checking authentication
if (authLoading) {
return (
<div className="flex h-screen w-full items-center justify-center bg-slate-50">
<div className="text-slate-500">Cargando...</div>
</div>
);
}
if (!isAuth) {
return <LoginPage onSuccess={handleLogin} />;
}
@@ -186,11 +229,10 @@ export default function App() {
page={page}
subPage={subPage}
setSubPage={setSubPage}
userName={user.name}
userEmail={user.email}
avatarUrl={user.avatarUrl}
userName={user?.name ?? "Usuario"}
userEmail={user?.email ?? ""}
avatarUrl={user?.avatar_url ?? null}
onOpenProfile={() => setProfileOpen(true)}
// ✅ en vez de cerrar, abrimos confirm modal
onRequestLogout={() => setLogoutOpen(true)}
/>
</div>
@@ -212,11 +254,11 @@ export default function App() {
<ProfileModal
open={profileOpen}
loading={savingProfile}
avatarUrl={user.avatarUrl}
avatarUrl={user?.avatar_url ?? null}
initial={{
name: user.name,
email: user.email,
organismName: user.organismName,
name: user?.name ?? "",
email: user?.email ?? "",
organismName: "",
}}
onClose={() => setProfileOpen(false)}
onSave={handleSaveProfile}
@@ -236,7 +278,7 @@ export default function App() {
onConfirm={async () => {
setLoggingOut(true);
try {
handleLogout();
await handleLogout();
setLogoutOpen(false);
} finally {
setLoggingOut(false);