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:
116
src/App.tsx
116
src/App.tsx
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user