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

434
water-api/sql/schema.sql Normal file
View File

@@ -0,0 +1,434 @@
-- ============================================================================
-- Water Project Database Schema
-- PostgreSQL Migration Script
-- ============================================================================
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================================================
-- TRIGGER FUNCTION: Auto-update updated_at timestamp
-- ============================================================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- ============================================================================
-- ENUM TYPES
-- ============================================================================
CREATE TYPE role_name AS ENUM ('ADMIN', 'OPERATOR', 'VIEWER');
CREATE TYPE project_status AS ENUM ('ACTIVE', 'INACTIVE', 'COMPLETED');
CREATE TYPE device_status AS ENUM ('ACTIVE', 'INACTIVE', 'OFFLINE', 'MAINTENANCE', 'ERROR');
CREATE TYPE meter_type AS ENUM ('WATER', 'GAS', 'ELECTRIC');
CREATE TYPE reading_type AS ENUM ('AUTOMATIC', 'MANUAL', 'SCHEDULED');
-- ============================================================================
-- TABLE 1: roles
-- ============================================================================
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name role_name NOT NULL UNIQUE,
description TEXT,
permissions JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_roles_name ON roles(name);
CREATE TRIGGER trigger_roles_updated_at
BEFORE UPDATE ON roles
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON TABLE roles IS 'User roles with associated permissions';
-- ============================================================================
-- TABLE 2: users
-- ============================================================================
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
avatar_url TEXT,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
last_login TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role_id ON users(role_id);
CREATE INDEX idx_users_is_active ON users(is_active);
CREATE TRIGGER trigger_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON TABLE users IS 'Application users with authentication credentials';
-- ============================================================================
-- TABLE 3: projects
-- ============================================================================
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
area_name VARCHAR(255),
location TEXT,
status project_status NOT NULL DEFAULT 'ACTIVE',
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_projects_status ON projects(status);
CREATE INDEX idx_projects_created_by ON projects(created_by);
CREATE INDEX idx_projects_name ON projects(name);
CREATE TRIGGER trigger_projects_updated_at
BEFORE UPDATE ON projects
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON TABLE projects IS 'Water monitoring projects';
-- ============================================================================
-- TABLE 4: concentrators
-- ============================================================================
CREATE TABLE concentrators (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
serial_number VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
location TEXT,
status device_status NOT NULL DEFAULT 'ACTIVE',
ip_address INET,
firmware_version VARCHAR(50),
last_communication TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_concentrators_serial_number ON concentrators(serial_number);
CREATE INDEX idx_concentrators_project_id ON concentrators(project_id);
CREATE INDEX idx_concentrators_status ON concentrators(status);
CREATE TRIGGER trigger_concentrators_updated_at
BEFORE UPDATE ON concentrators
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON TABLE concentrators IS 'Data concentrators that aggregate gateway communications';
-- ============================================================================
-- TABLE 5: gateways
-- ============================================================================
CREATE TABLE gateways (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
gateway_id VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
concentrator_id UUID REFERENCES concentrators(id) ON DELETE SET NULL,
location TEXT,
status device_status NOT NULL DEFAULT 'ACTIVE',
tts_gateway_id VARCHAR(255),
tts_status VARCHAR(50),
tts_last_seen TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_gateways_gateway_id ON gateways(gateway_id);
CREATE INDEX idx_gateways_project_id ON gateways(project_id);
CREATE INDEX idx_gateways_concentrator_id ON gateways(concentrator_id);
CREATE INDEX idx_gateways_status ON gateways(status);
CREATE TRIGGER trigger_gateways_updated_at
BEFORE UPDATE ON gateways
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON TABLE gateways IS 'LoRaWAN gateways for device communication';
-- ============================================================================
-- TABLE 6: devices
-- ============================================================================
CREATE TABLE devices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
dev_eui VARCHAR(16) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
device_type VARCHAR(100),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
gateway_id UUID REFERENCES gateways(id) ON DELETE SET NULL,
status device_status NOT NULL DEFAULT 'ACTIVE',
tts_device_id VARCHAR(255),
tts_status VARCHAR(50),
tts_last_seen TIMESTAMP WITH TIME ZONE,
app_key VARCHAR(32),
join_eui VARCHAR(16),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_devices_dev_eui ON devices(dev_eui);
CREATE INDEX idx_devices_project_id ON devices(project_id);
CREATE INDEX idx_devices_gateway_id ON devices(gateway_id);
CREATE INDEX idx_devices_status ON devices(status);
CREATE INDEX idx_devices_device_type ON devices(device_type);
CREATE TRIGGER trigger_devices_updated_at
BEFORE UPDATE ON devices
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON TABLE devices IS 'LoRaWAN end devices (sensors/transmitters)';
-- ============================================================================
-- TABLE 7: meters
-- ============================================================================
CREATE TABLE meters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
serial_number VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
device_id UUID REFERENCES devices(id) ON DELETE SET NULL,
area_name VARCHAR(255),
location TEXT,
meter_type meter_type NOT NULL DEFAULT 'WATER',
status device_status NOT NULL DEFAULT 'ACTIVE',
last_reading_value NUMERIC(15, 4),
last_reading_at TIMESTAMP WITH TIME ZONE,
installation_date DATE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_meters_serial_number ON meters(serial_number);
CREATE INDEX idx_meters_project_id ON meters(project_id);
CREATE INDEX idx_meters_device_id ON meters(device_id);
CREATE INDEX idx_meters_status ON meters(status);
CREATE INDEX idx_meters_meter_type ON meters(meter_type);
CREATE INDEX idx_meters_area_name ON meters(area_name);
CREATE TRIGGER trigger_meters_updated_at
BEFORE UPDATE ON meters
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMENT ON TABLE meters IS 'Physical water meters associated with devices';
-- ============================================================================
-- TABLE 8: meter_readings
-- ============================================================================
CREATE TABLE meter_readings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
meter_id UUID NOT NULL REFERENCES meters(id) ON DELETE CASCADE,
device_id UUID REFERENCES devices(id) ON DELETE SET NULL,
reading_value NUMERIC(15, 4) NOT NULL,
reading_type reading_type NOT NULL DEFAULT 'AUTOMATIC',
battery_level SMALLINT,
signal_strength SMALLINT,
raw_payload TEXT,
received_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_meter_readings_meter_id ON meter_readings(meter_id);
CREATE INDEX idx_meter_readings_device_id ON meter_readings(device_id);
CREATE INDEX idx_meter_readings_received_at ON meter_readings(received_at);
CREATE INDEX idx_meter_readings_meter_id_received_at ON meter_readings(meter_id, received_at DESC);
COMMENT ON TABLE meter_readings IS 'Historical meter reading values';
-- ============================================================================
-- TABLE 9: tts_uplink_logs
-- ============================================================================
CREATE TABLE tts_uplink_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
device_id UUID REFERENCES devices(id) ON DELETE SET NULL,
dev_eui VARCHAR(16) NOT NULL,
raw_payload JSONB NOT NULL,
decoded_payload JSONB,
gateway_ids TEXT[],
rssi SMALLINT,
snr NUMERIC(5, 2),
processed BOOLEAN NOT NULL DEFAULT FALSE,
error_message TEXT,
received_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_tts_uplink_logs_device_id ON tts_uplink_logs(device_id);
CREATE INDEX idx_tts_uplink_logs_dev_eui ON tts_uplink_logs(dev_eui);
CREATE INDEX idx_tts_uplink_logs_received_at ON tts_uplink_logs(received_at);
CREATE INDEX idx_tts_uplink_logs_processed ON tts_uplink_logs(processed);
CREATE INDEX idx_tts_uplink_logs_raw_payload ON tts_uplink_logs USING GIN (raw_payload);
COMMENT ON TABLE tts_uplink_logs IS 'The Things Stack uplink message logs';
-- ============================================================================
-- TABLE 10: refresh_tokens
-- ============================================================================
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
revoked_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at);
COMMENT ON TABLE refresh_tokens IS 'JWT refresh tokens for user sessions';
-- ============================================================================
-- VIEW: meter_stats_by_project
-- ============================================================================
CREATE OR REPLACE VIEW meter_stats_by_project AS
SELECT
p.id AS project_id,
p.name AS project_name,
p.status AS project_status,
COUNT(m.id) AS total_meters,
COUNT(CASE WHEN m.status = 'ACTIVE' THEN 1 END) AS active_meters,
COUNT(CASE WHEN m.status = 'INACTIVE' THEN 1 END) AS inactive_meters,
COUNT(CASE WHEN m.status = 'OFFLINE' THEN 1 END) AS offline_meters,
COUNT(CASE WHEN m.status = 'MAINTENANCE' THEN 1 END) AS maintenance_meters,
COUNT(CASE WHEN m.status = 'ERROR' THEN 1 END) AS error_meters,
ROUND(AVG(m.last_reading_value)::NUMERIC, 2) AS avg_last_reading,
MAX(m.last_reading_at) AS most_recent_reading,
COUNT(DISTINCT m.area_name) AS unique_areas
FROM projects p
LEFT JOIN meters m ON p.id = m.project_id
GROUP BY p.id, p.name, p.status;
COMMENT ON VIEW meter_stats_by_project IS 'Aggregated meter statistics per project';
-- ============================================================================
-- VIEW: device_status_summary
-- ============================================================================
CREATE OR REPLACE VIEW device_status_summary AS
SELECT
p.id AS project_id,
p.name AS project_name,
'concentrator' AS device_category,
c.status,
COUNT(*) AS count
FROM projects p
LEFT JOIN concentrators c ON p.id = c.project_id
WHERE c.id IS NOT NULL
GROUP BY p.id, p.name, c.status
UNION ALL
SELECT
p.id AS project_id,
p.name AS project_name,
'gateway' AS device_category,
g.status,
COUNT(*) AS count
FROM projects p
LEFT JOIN gateways g ON p.id = g.project_id
WHERE g.id IS NOT NULL
GROUP BY p.id, p.name, g.status
UNION ALL
SELECT
p.id AS project_id,
p.name AS project_name,
'device' AS device_category,
d.status,
COUNT(*) AS count
FROM projects p
LEFT JOIN devices d ON p.id = d.project_id
WHERE d.id IS NOT NULL
GROUP BY p.id, p.name, d.status
UNION ALL
SELECT
p.id AS project_id,
p.name AS project_name,
'meter' AS device_category,
m.status,
COUNT(*) AS count
FROM projects p
LEFT JOIN meters m ON p.id = m.project_id
WHERE m.id IS NOT NULL
GROUP BY p.id, p.name, m.status;
COMMENT ON VIEW device_status_summary IS 'Summary of device statuses across all device types per project';
-- ============================================================================
-- SEED DATA: Default Roles
-- ============================================================================
INSERT INTO roles (name, description, permissions) VALUES
(
'ADMIN',
'Full system administrator with all permissions',
'{
"users": {"create": true, "read": true, "update": true, "delete": true},
"projects": {"create": true, "read": true, "update": true, "delete": true},
"devices": {"create": true, "read": true, "update": true, "delete": true},
"meters": {"create": true, "read": true, "update": true, "delete": true},
"readings": {"create": true, "read": true, "update": true, "delete": true},
"settings": {"create": true, "read": true, "update": true, "delete": true},
"reports": {"create": true, "read": true, "export": true}
}'::JSONB
),
(
'OPERATOR',
'Operator with management permissions but no system settings',
'{
"users": {"create": false, "read": true, "update": false, "delete": false},
"projects": {"create": true, "read": true, "update": true, "delete": false},
"devices": {"create": true, "read": true, "update": true, "delete": false},
"meters": {"create": true, "read": true, "update": true, "delete": false},
"readings": {"create": true, "read": true, "update": false, "delete": false},
"settings": {"create": false, "read": true, "update": false, "delete": false},
"reports": {"create": true, "read": true, "export": true}
}'::JSONB
),
(
'VIEWER',
'Read-only access to view data and reports',
'{
"users": {"create": false, "read": false, "update": false, "delete": false},
"projects": {"create": false, "read": true, "update": false, "delete": false},
"devices": {"create": false, "read": true, "update": false, "delete": false},
"meters": {"create": false, "read": true, "update": false, "delete": false},
"readings": {"create": false, "read": true, "update": false, "delete": false},
"settings": {"create": false, "read": false, "update": false, "delete": false},
"reports": {"create": false, "read": true, "export": false}
}'::JSONB
);
-- ============================================================================
-- SEED DATA: Default Admin User
-- Password: admin123 (bcrypt hashed)
-- ============================================================================
INSERT INTO users (email, password_hash, name, role_id, is_active)
SELECT
'admin@waterproject.com',
'$2b$12$RrlEdRsUiiQYxtUmjOjX.uZU/IpXUFsXsWxDcMny1RUl6RFc.etDm',
'System Administrator',
r.id,
TRUE
FROM roles r
WHERE r.name = 'ADMIN';
-- ============================================================================
-- END OF SCHEMA
-- ============================================================================