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

@@ -3,6 +3,7 @@ import {
fetchConcentrators,
type Concentrator,
} from "../../api/concentrators";
import { fetchProjects, type Project } from "../../api/projects";
import type { ProjectCard, SampleView } from "./ConcentratorsPage";
type User = {
@@ -16,6 +17,7 @@ export function useConcentrators(currentUser: User) {
const [loadingProjects, setLoadingProjects] = useState(true);
const [loadingConcentrators, setLoadingConcentrators] = useState(true);
const [projects, setProjects] = useState<Project[]>([]);
const [allProjects, setAllProjects] = useState<string[]>([]);
const [selectedProject, setSelectedProject] = useState("");
@@ -51,58 +53,49 @@ export function useConcentrators(currentUser: User) {
[allProjects, currentUser.role, currentUser.project]
);
const loadConcentrators = async () => {
if (!isGeneral) return;
setLoadingConcentrators(true);
const loadProjects = async () => {
setLoadingProjects(true);
try {
const raw = await fetchConcentrators();
const normalized = raw.map((c: any) => {
const preferredName =
c["Device Alias"] ||
c["Device Label"] ||
c["Device Display Name"] ||
c.deviceName ||
c.name ||
c["Device Name"] ||
"";
return {
...c,
"Device Name": preferredName,
};
});
const projectsArray = [
...new Set(normalized.map((r: any) => r["Area Name"])),
].filter(Boolean) as string[];
setAllProjects(projectsArray);
setConcentrators(normalized);
const projectsData = await fetchProjects();
setProjects(projectsData);
const projectIds = projectsData.map((p) => p.id);
setAllProjects(projectIds);
setSelectedProject((prev) => {
if (prev) return prev;
if (currentUser.role !== "SUPER_ADMIN" && currentUser.project) {
return currentUser.project;
}
return projectsArray[0] ?? "";
return projectIds[0] ?? "";
});
} catch (err) {
console.error("Error loading concentrators:", err);
console.error("Error loading projects:", err);
setProjects([]);
setAllProjects([]);
setConcentrators([]);
setSelectedProject("");
} finally {
setLoadingConcentrators(false);
setLoadingProjects(false);
}
};
// init
const loadConcentrators = async () => {
if (!isGeneral) return;
setLoadingConcentrators(true);
try {
const data = await fetchConcentrators();
setConcentrators(data);
} catch (err) {
console.error("Error loading concentrators:", err);
setConcentrators([]);
} finally {
setLoadingConcentrators(false);
}
};
// init - load projects and concentrators
useEffect(() => {
loadProjects();
loadConcentrators();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -110,6 +103,7 @@ export function useConcentrators(currentUser: User) {
// view changes
useEffect(() => {
if (isGeneral) {
loadProjects();
loadConcentrators();
} else {
setLoadingProjects(false);
@@ -136,7 +130,7 @@ export function useConcentrators(currentUser: User) {
if (selectedProject) {
setFilteredConcentrators(
concentrators.filter((c) => c["Area Name"] === selectedProject)
concentrators.filter((c) => c.projectId === selectedProject)
);
} else {
setFilteredConcentrators(concentrators);
@@ -146,8 +140,13 @@ export function useConcentrators(currentUser: User) {
// sidebar cards (general)
const projectsDataGeneral: ProjectCard[] = useMemo(() => {
const counts = concentrators.reduce<Record<string, number>>((acc, c) => {
const area = c["Area Name"] ?? "SIN PROYECTO";
acc[area] = (acc[area] ?? 0) + 1;
const project = c.projectId ?? "SIN PROYECTO";
acc[project] = (acc[project] ?? 0) + 1;
return acc;
}, {});
const projectNameMap = projects.reduce<Record<string, string>>((acc, p) => {
acc[p.id] = p.name;
return acc;
}, {});
@@ -155,17 +154,18 @@ export function useConcentrators(currentUser: User) {
const baseContact = "Operaciones";
const baseLastSync = "Hace 1 h";
return visibleProjects.map((name) => ({
name,
return visibleProjects.map((projectId) => ({
id: projectId,
name: projectNameMap[projectId] ?? projectId,
region: baseRegion,
projects: 1,
concentrators: counts[name] ?? 0,
concentrators: counts[projectId] ?? 0,
activeAlerts: 0,
lastSync: baseLastSync,
contact: baseContact,
status: "ACTIVO",
status: "ACTIVO" as const,
}));
}, [concentrators, visibleProjects]);
}, [concentrators, visibleProjects, projects]);
// sidebar cards (mock)
const projectsDataMock: Record<Exclude<SampleView, "GENERAL">, ProjectCard[]> =
@@ -173,6 +173,7 @@ export function useConcentrators(currentUser: User) {
() => ({
LORA: [
{
id: "mock-lora-centro",
name: "LoRa - Zona Centro",
region: "Baja California",
projects: 1,
@@ -183,6 +184,7 @@ export function useConcentrators(currentUser: User) {
status: "ACTIVO",
},
{
id: "mock-lora-este",
name: "LoRa - Zona Este",
region: "Baja California",
projects: 1,
@@ -195,6 +197,7 @@ export function useConcentrators(currentUser: User) {
],
LORAWAN: [
{
id: "mock-lorawan-industrial",
name: "LoRaWAN - Industrial",
region: "Baja California",
projects: 1,
@@ -207,6 +210,7 @@ export function useConcentrators(currentUser: User) {
],
GRANDES: [
{
id: "mock-grandes-convenios",
name: "Grandes - Convenios",
region: "Baja California",
projects: 1,