Fix: Corregir pantalla blanca y mejorar carga masiva
- Fix error .toFixed() con valores DECIMAL de PostgreSQL (string vs number) - Fix modal de carga masiva que se cerraba sin mostrar resultados - Validar fechas antes de insertar en BD (evita error con "Installed") - Agregar mapeos de columnas comunes (device_status, device_name, etc.) - Normalizar valores de status (Installed -> ACTIVE, New_LoRa -> ACTIVE) - Actualizar documentación del proyecto Archivos modificados: - src/pages/meters/MetersTable.tsx - src/pages/consumption/ConsumptionPage.tsx - src/pages/meters/MeterPage.tsx - water-api/src/services/bulk-upload.service.ts - ESTADO_ACTUAL.md - CAMBIOS_SESION.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,131 +1,171 @@
|
|||||||
# Cambios Realizados en Esta Sesión
|
# Cambios Realizados - Sesión 2026-01-23
|
||||||
|
|
||||||
**Fecha:** 2026-01-23
|
## Resumen
|
||||||
|
Corrección de errores críticos que causaban pantalla blanca y mejoras en el sistema de carga masiva.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Problema a Resolver
|
## Problema 1: Pantalla Blanca en Water Meters y Consumo
|
||||||
Pantalla en blanco al entrar a "Water Meters" y "Consumo" después de implementar carga masiva de lecturas.
|
|
||||||
|
|
||||||
---
|
### Síntoma
|
||||||
|
Al navegar a "Water Meters" o "Consumo", la página se quedaba en blanco.
|
||||||
|
|
||||||
## Cambios Realizados
|
### Causa
|
||||||
|
PostgreSQL devuelve valores DECIMAL como strings (ej: `"300.0000"`). El código llamaba `.toFixed()` directamente sobre estos strings, pero `.toFixed()` es un método de números, no de strings.
|
||||||
|
|
||||||
### 1. `src/api/client.ts` (línea ~224-237)
|
### Solución
|
||||||
**Cambio:** Modificado `parseResponse` para manejar respuestas con paginación
|
Convertir los valores a número con `Number()` antes de llamar `.toFixed()`.
|
||||||
|
|
||||||
|
### Archivos Modificados
|
||||||
|
|
||||||
|
**`src/pages/meters/MetersTable.tsx` (línea 75)**
|
||||||
```typescript
|
```typescript
|
||||||
// ANTES:
|
// ANTES:
|
||||||
if ('success' in data) {
|
r.lastReadingValue?.toFixed(2)
|
||||||
if (data.success === false) { throw... }
|
|
||||||
return data.data as T; // Solo devolvía data.data
|
|
||||||
}
|
|
||||||
|
|
||||||
// DESPUÉS:
|
// DESPUÉS:
|
||||||
if ('success' in data) {
|
r.lastReadingValue != null ? Number(r.lastReadingValue).toFixed(2) : "-"
|
||||||
if (data.success === false) { throw... }
|
```
|
||||||
// Si hay pagination, devolver objeto completo
|
|
||||||
if ('pagination' in data) {
|
**`src/pages/consumption/ConsumptionPage.tsx` (líneas 133, 213, 432)**
|
||||||
return {
|
```typescript
|
||||||
data: data.data,
|
// ANTES:
|
||||||
pagination: data.pagination,
|
r.readingValue.toFixed(2)
|
||||||
} as T;
|
summary?.avgReading.toFixed(1)
|
||||||
|
reading.readingValue.toFixed(2)
|
||||||
|
|
||||||
|
// DESPUÉS:
|
||||||
|
Number(r.readingValue).toFixed(2)
|
||||||
|
summary?.avgReading != null ? Number(summary.avgReading).toFixed(1) : "0"
|
||||||
|
Number(reading.readingValue).toFixed(2)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problema 2: Modal de Carga Masiva se Cerraba sin Mostrar Resultados
|
||||||
|
|
||||||
|
### Síntoma
|
||||||
|
Al subir un archivo Excel para carga masiva, el modal se cerraba inmediatamente sin mostrar cuántos registros se insertaron o qué errores hubo.
|
||||||
|
|
||||||
|
### Causa
|
||||||
|
El callback `onSuccess` cerraba el modal automáticamente:
|
||||||
|
```typescript
|
||||||
|
onSuccess={() => {
|
||||||
|
m.loadMeters();
|
||||||
|
setShowBulkUpload(false); // ← Cerraba antes de ver resultados
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solución
|
||||||
|
Separar la recarga de datos del cierre del modal. Ahora el modal solo se cierra cuando el usuario hace clic en "Cerrar".
|
||||||
|
|
||||||
|
### Archivo Modificado
|
||||||
|
|
||||||
|
**`src/pages/meters/MeterPage.tsx` (líneas 332-340)**
|
||||||
|
```typescript
|
||||||
|
// ANTES:
|
||||||
|
<MetersBulkUploadModal
|
||||||
|
onClose={() => setShowBulkUpload(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
m.loadMeters();
|
||||||
|
setShowBulkUpload(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// DESPUÉS:
|
||||||
|
<MetersBulkUploadModal
|
||||||
|
onClose={() => {
|
||||||
|
m.loadMeters();
|
||||||
|
setShowBulkUpload(false);
|
||||||
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
m.loadMeters();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problema 3: Error de Fecha Inválida en Carga Masiva
|
||||||
|
|
||||||
|
### Síntoma
|
||||||
|
Al subir medidores, aparecía el error:
|
||||||
|
```
|
||||||
|
Fila X: invalid input syntax for type date: "Installed"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Causa
|
||||||
|
El archivo Excel tenía columnas con valores como "Installed" o "New_LoRa" que el sistema interpretaba como fechas porque no estaban mapeadas correctamente.
|
||||||
|
|
||||||
|
### Solución
|
||||||
|
1. **Validar fechas**: Verificar que `installation_date` sea realmente una fecha válida antes de usarla.
|
||||||
|
2. **Más mapeos de columnas**: Agregar mapeos para columnas comunes como `device_status`, `device_name`, etc.
|
||||||
|
3. **Normalizar status**: Convertir valores como "Installed", "New_LoRa" a "ACTIVE".
|
||||||
|
|
||||||
|
### Archivo Modificado
|
||||||
|
|
||||||
|
**`water-api/src/services/bulk-upload.service.ts`**
|
||||||
|
|
||||||
|
Validación de fechas (líneas 183-195):
|
||||||
|
```typescript
|
||||||
|
let installationDate: string | undefined = undefined;
|
||||||
|
if (row.installation_date) {
|
||||||
|
const dateStr = String(row.installation_date).trim();
|
||||||
|
if (/^\d{4}[-/]\d{1,2}[-/]\d{1,2}/.test(dateStr) || /^\d{1,2}[-/]\d{1,2}[-/]\d{2,4}/.test(dateStr)) {
|
||||||
|
const parsed = new Date(dateStr);
|
||||||
|
if (!isNaN(parsed.getTime())) {
|
||||||
|
installationDate = parsed.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return data.data as T;
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. `src/api/readings.ts`
|
Mapeos de columnas adicionales (líneas 65-90):
|
||||||
**Cambios en interface `MeterReading`:**
|
|
||||||
```typescript
|
```typescript
|
||||||
// ELIMINADO:
|
const mappings: Record<string, string> = {
|
||||||
deviceId: string | null;
|
// Serial number
|
||||||
areaName: string | null;
|
'device_s/n': 'serial_number',
|
||||||
|
'device_sn': 'serial_number',
|
||||||
// AGREGADO:
|
// Name
|
||||||
meterLocation: string | null;
|
'device_name': 'name',
|
||||||
concentratorId: string;
|
'meter_name': 'name',
|
||||||
concentratorName: string;
|
// Status
|
||||||
projectName: string;
|
'device_status': 'status',
|
||||||
|
// ... más mapeos
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Cambios en interface `ReadingFilters`:**
|
Normalización de status (líneas 210-225):
|
||||||
```typescript
|
```typescript
|
||||||
// ELIMINADO:
|
const statusMappings: Record<string, string> = {
|
||||||
areaName?: string;
|
'INSTALLED': 'ACTIVE',
|
||||||
|
'NEW_LORA': 'ACTIVE',
|
||||||
// AGREGADO:
|
'NEW': 'ACTIVE',
|
||||||
concentratorId?: string;
|
'ENABLED': 'ACTIVE',
|
||||||
|
'DISABLED': 'INACTIVE',
|
||||||
|
// ...
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
**Cambios en interface `ReadingInput`:**
|
|
||||||
```typescript
|
|
||||||
// ELIMINADO:
|
|
||||||
deviceId?: string;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cambios en función `createReading`:**
|
|
||||||
```typescript
|
|
||||||
// ELIMINADO de backendData:
|
|
||||||
device_id: data.deviceId,
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. `src/api/projects.ts` (función `fetchProjects`)
|
|
||||||
**Cambio:** Ahora maneja respuestas paginadas
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ANTES:
|
|
||||||
const response = await apiClient.get<Record<string, unknown>[]>('/api/projects');
|
|
||||||
return transformArray<Project>(response);
|
|
||||||
|
|
||||||
// DESPUÉS:
|
|
||||||
const response = await apiClient.get<...>('/api/projects');
|
|
||||||
if (response && typeof response === 'object' && 'data' in response && Array.isArray(response.data)) {
|
|
||||||
return transformArray<Project>(response.data);
|
|
||||||
}
|
|
||||||
return transformArray<Project>(response as Record<string, unknown>[]);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. `src/api/concentrators.ts` (función `fetchConcentrators`)
|
|
||||||
**Cambio:** Mismo patrón que projects.ts para manejar paginación
|
|
||||||
|
|
||||||
### 5. `src/pages/consumption/ConsumptionPage.tsx`
|
|
||||||
**Cambios:**
|
|
||||||
- Línea ~94: `r.areaName` → `r.meterLocation` (filtro de búsqueda)
|
|
||||||
- Línea ~127: `"Área"` → `"Ubicación"` (header CSV)
|
|
||||||
- Línea ~132: `r.areaName` → `r.meterLocation` (datos CSV)
|
|
||||||
- Línea ~365: `"Área"` → `"Ubicación"` (header tabla)
|
|
||||||
- Línea ~428: `reading.areaName` → `reading.meterLocation` (celda tabla)
|
|
||||||
|
|
||||||
### 6. `water-api/src/services/reading.service.ts`
|
|
||||||
**Cambios previos (ya aplicados):**
|
|
||||||
- Eliminadas todas las referencias a `device_id` en queries SQL
|
|
||||||
- La columna `device_id` no existe en la tabla `meter_readings`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Estado de los Servidores
|
## Archivos Modificados en Esta Sesión
|
||||||
|
|
||||||
### Backend (puerto 3000) ✅ Funcionando
|
| Archivo | Cambio |
|
||||||
```bash
|
|---------|--------|
|
||||||
curl http://localhost:3000/api/readings?pageSize=1
|
| `src/pages/meters/MetersTable.tsx` | Fix `.toFixed()` en lastReadingValue |
|
||||||
# Devuelve datos correctamente
|
| `src/pages/consumption/ConsumptionPage.tsx` | Fix `.toFixed()` en readingValue y avgReading |
|
||||||
```
|
| `src/pages/meters/MeterPage.tsx` | Fix modal de carga masiva |
|
||||||
|
| `water-api/src/services/bulk-upload.service.ts` | Validación de fechas, mapeos de columnas, normalización de status |
|
||||||
### Frontend (puerto 5173) ⚠️ Pantalla blanca
|
| `ESTADO_ACTUAL.md` | Documentación actualizada |
|
||||||
- El servidor está corriendo
|
| `CAMBIOS_SESION.md` | Este archivo |
|
||||||
- Las páginas no renderizan correctamente
|
|
||||||
- Se necesita revisar la consola del navegador para ver el error específico
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Para Debug
|
## Verificación
|
||||||
|
|
||||||
1. Abrir http://localhost:5173
|
1. ✅ La página de Water Meters carga correctamente
|
||||||
2. F12 → Console
|
2. ✅ La página de Consumo carga correctamente
|
||||||
3. Navegar a "Water Meters" o "Consumo"
|
3. ✅ El modal de carga masiva muestra resultados
|
||||||
4. Copiar el error de la consola
|
4. ✅ Errores de carga masiva se muestran claramente
|
||||||
|
5. ✅ Valores como "Installed" no causan error de fecha
|
||||||
El error más probable es:
|
|
||||||
- `Cannot read property 'map' of undefined` - si `transformArray` recibe undefined
|
|
||||||
- `TypeError: response.data is undefined` - si la respuesta no tiene la estructura esperada
|
|
||||||
|
|||||||
365
ESTADO_ACTUAL.md
365
ESTADO_ACTUAL.md
@@ -1,7 +1,7 @@
|
|||||||
# Estado Actual del Proyecto Water Project
|
# Estado Actual del Proyecto Water Project GRH
|
||||||
|
|
||||||
**Fecha:** 2026-01-23
|
**Fecha:** 2026-01-23
|
||||||
**Última sesión:** Migración de NocoDB a PostgreSQL + Node.js/Express
|
**Última actualización:** Corrección de errores y mejoras en carga masiva
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,145 +19,165 @@ Projects → Concentrators → Meters → Readings
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Lo que se implementó
|
## Arquitectura del Sistema
|
||||||
|
|
||||||
### 1. Backend API completo (`water-api/`)
|
```
|
||||||
- Autenticación JWT con refresh tokens
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
- CRUD completo para: Projects, Concentrators, Meters, Readings, Users, Roles
|
│ FRONTEND (React) │
|
||||||
- Carga masiva de medidores y lecturas via Excel
|
│ http://localhost:5173 │
|
||||||
- Endpoints de resumen/estadísticas
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ - React 18 + TypeScript + Vite │
|
||||||
|
│ - Tailwind CSS + Material-UI │
|
||||||
|
│ - Recharts para gráficos │
|
||||||
|
│ - Cliente API con JWT automático │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ BACKEND (Node.js) │
|
||||||
|
│ http://localhost:3000 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ - Express + TypeScript │
|
||||||
|
│ - Autenticación JWT con refresh tokens │
|
||||||
|
│ - CRUD completo para todas las entidades │
|
||||||
|
│ - Carga masiva via Excel (xlsx) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ BASE DE DATOS │
|
||||||
|
│ PostgreSQL │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Tablas: users, roles, projects, concentrators, │
|
||||||
|
│ meters, meter_readings, refresh_tokens │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
### 2. Frontend adaptado
|
---
|
||||||
- Cliente API con manejo automático de JWT (`src/api/client.ts`)
|
|
||||||
- Transformación automática snake_case ↔ camelCase
|
|
||||||
- Páginas: Home, Meters, Consumption, Projects, Users, Roles
|
|
||||||
|
|
||||||
### 3. Carga Masiva
|
## Funcionalidades Implementadas
|
||||||
- **Medidores:** Subir Excel con columnas: `serial_number`, `name`, `concentrator_serial`, `location`, `type`, `status`
|
|
||||||
- **Lecturas:** Subir Excel con columnas: `meter_serial`, `reading_value`, `reading_type`, `battery_level`, `signal_strength`, `received_at`
|
|
||||||
|
|
||||||
### 4. Credenciales actualizadas
|
### 1. Autenticación
|
||||||
- **Usuario admin:** Ivan Alcaraz
|
- Login con JWT + refresh tokens
|
||||||
|
- Manejo automático de renovación de tokens
|
||||||
|
- Roles: ADMIN, USER
|
||||||
|
|
||||||
|
### 2. Gestión de Proyectos
|
||||||
|
- CRUD completo
|
||||||
|
- Estados: ACTIVE/INACTIVE
|
||||||
|
|
||||||
|
### 3. Gestión de Concentradores
|
||||||
|
- CRUD completo
|
||||||
|
- Vinculados a proyectos
|
||||||
|
- Tipos: Gateway LoRa/LoRaWAN
|
||||||
|
|
||||||
|
### 4. Gestión de Medidores
|
||||||
|
- CRUD completo
|
||||||
|
- Tipos: LORA, LORAWAN, GRANDES
|
||||||
|
- Estados: ACTIVE, INACTIVE, MAINTENANCE, FAULTY, REPLACED
|
||||||
|
- **Carga masiva via Excel**
|
||||||
|
- Última lectura visible en tabla
|
||||||
|
|
||||||
|
### 5. Gestión de Lecturas (Consumo)
|
||||||
|
- CRUD completo
|
||||||
|
- Tipos: AUTOMATIC, MANUAL, SCHEDULED
|
||||||
|
- **Carga masiva via Excel**
|
||||||
|
- Filtros por proyecto, fecha
|
||||||
|
- Exportación a CSV
|
||||||
|
- Indicadores de batería y señal
|
||||||
|
|
||||||
|
### 6. Dashboard
|
||||||
|
- KPIs: Total lecturas, medidores activos, consumo promedio
|
||||||
|
- Gráficos por proyecto
|
||||||
|
- Últimas alertas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Carga Masiva
|
||||||
|
|
||||||
|
### Medidores (Excel)
|
||||||
|
Columnas requeridas:
|
||||||
|
- `serial_number` - Número de serie del medidor (único)
|
||||||
|
- `name` - Nombre del medidor
|
||||||
|
- `concentrator_serial` - Serial del concentrador existente
|
||||||
|
|
||||||
|
Columnas opcionales:
|
||||||
|
- `meter_id` - ID del medidor
|
||||||
|
- `location` - Ubicación
|
||||||
|
- `type` - LORA, LORAWAN, GRANDES (default: LORA)
|
||||||
|
- `status` - ACTIVE, INACTIVE, etc. (default: ACTIVE)
|
||||||
|
- `installation_date` - Fecha de instalación (YYYY-MM-DD)
|
||||||
|
|
||||||
|
### Lecturas (Excel)
|
||||||
|
Columnas requeridas:
|
||||||
|
- `meter_serial` - Serial del medidor existente
|
||||||
|
- `reading_value` - Valor de la lectura
|
||||||
|
|
||||||
|
Columnas opcionales:
|
||||||
|
- `reading_type` - AUTOMATIC, MANUAL, SCHEDULED (default: MANUAL)
|
||||||
|
- `received_at` - Fecha/hora (default: ahora)
|
||||||
|
- `battery_level` - Nivel de batería (%)
|
||||||
|
- `signal_strength` - Intensidad de señal (dBm)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Credenciales
|
||||||
|
|
||||||
|
### Usuario Admin
|
||||||
|
- **Nombre:** Ivan Alcaraz
|
||||||
- **Email:** ialcarazsalazar@consultoria-as.com
|
- **Email:** ialcarazsalazar@consultoria-as.com
|
||||||
- **Password:** Aasi940812
|
- **Password:** Aasi940812
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Problema Actual: Pantalla en Blanco
|
## Datos Actuales en BD
|
||||||
|
|
||||||
### Síntoma
|
### Proyectos
|
||||||
Al entrar a "Water Meters" o "Consumo", la pantalla se queda en blanco.
|
- ADAMANT
|
||||||
|
- OLE
|
||||||
|
- LUZIA
|
||||||
|
- ATELIER
|
||||||
|
|
||||||
### Causa identificada
|
### Concentradores
|
||||||
El cliente API (`src/api/client.ts`) fue modificado para manejar respuestas paginadas. Cuando el backend devuelve:
|
| Serial | Nombre | Proyecto |
|
||||||
```json
|
|--------|--------|----------|
|
||||||
{
|
| 2024072612 | Adamant | ADAMANT |
|
||||||
"success": true,
|
| 2024030601 | OLE | OLE |
|
||||||
"data": [...],
|
| 2024030402 | LUZIA | LUZIA |
|
||||||
"pagination": {...}
|
| 2024072602 | ATELIER | ATELIER |
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
El cliente ahora devuelve `{ data: [...], pagination: {...} }` en lugar de solo el array.
|
### Medidores
|
||||||
|
- ADAMANT: 201 medidores
|
||||||
### Archivos modificados para manejar esto:
|
- OLE: 5 medidores
|
||||||
1. ✅ `src/api/client.ts` - Detecta si hay `pagination` y devuelve el objeto completo
|
|
||||||
2. ✅ `src/api/meters.ts` - Ya manejaba respuestas paginadas
|
|
||||||
3. ✅ `src/api/readings.ts` - Ya manejaba respuestas paginadas
|
|
||||||
4. ✅ `src/api/projects.ts` - Actualizado para manejar paginación
|
|
||||||
5. ✅ `src/api/concentrators.ts` - Actualizado para manejar paginación
|
|
||||||
|
|
||||||
### Cambios en interfaces de readings.ts:
|
|
||||||
- Eliminado `deviceId` (no existe en BD)
|
|
||||||
- Cambiado `areaName` por `meterLocation`
|
|
||||||
- Agregado `concentratorId`, `concentratorName`, `projectName`
|
|
||||||
|
|
||||||
### Cambios en ConsumptionPage.tsx:
|
|
||||||
- Cambiado `areaName` por `meterLocation` en todas las referencias
|
|
||||||
- Cambiado header de tabla "Área" por "Ubicación"
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Para Continuar Debugging
|
## Correcciones Recientes (2026-01-23)
|
||||||
|
|
||||||
### 1. Verificar que los APIs funcionan
|
### 1. Error `.toFixed()` con valores string
|
||||||
```bash
|
**Problema:** PostgreSQL devuelve DECIMAL como string, causando error al llamar `.toFixed()`.
|
||||||
# Projects
|
**Solución:** Convertir a número con `Number()` antes de llamar `.toFixed()`.
|
||||||
curl http://localhost:3000/api/projects
|
**Archivos:**
|
||||||
|
- `src/pages/meters/MetersTable.tsx:75`
|
||||||
|
- `src/pages/consumption/ConsumptionPage.tsx:133, 213, 432`
|
||||||
|
|
||||||
# Meters (requiere auth)
|
### 2. Modal de carga masiva se cerraba sin mostrar resultados
|
||||||
curl http://localhost:3000/api/meters?pageSize=2
|
**Problema:** El modal se cerraba automáticamente después de la carga.
|
||||||
|
**Solución:** El modal ahora permanece abierto para mostrar resultados y errores.
|
||||||
|
**Archivo:** `src/pages/meters/MeterPage.tsx:332-340`
|
||||||
|
|
||||||
# Readings
|
### 3. Validación de fechas en carga masiva
|
||||||
curl http://localhost:3000/api/readings?pageSize=2
|
**Problema:** Valores como "Installed" en columnas no mapeadas causaban error de fecha inválida.
|
||||||
```
|
**Solución:** Validar que `installation_date` sea realmente una fecha antes de insertarla.
|
||||||
|
**Archivo:** `water-api/src/services/bulk-upload.service.ts:183-195`
|
||||||
|
|
||||||
### 2. Ver errores en el navegador
|
### 4. Mapeo de columnas mejorado
|
||||||
1. Abrir la aplicación en http://localhost:5173
|
**Mejora:** Agregados más mapeos de columnas comunes (device_status, device_name, etc.)
|
||||||
2. Abrir DevTools (F12)
|
**Archivo:** `water-api/src/services/bulk-upload.service.ts:65-90`
|
||||||
3. Ir a la pestaña "Console"
|
|
||||||
4. Navegar a "Water Meters" o "Consumo"
|
|
||||||
5. Copiar el error que aparezca
|
|
||||||
|
|
||||||
### 3. Posibles causas adicionales
|
### 5. Normalización de status
|
||||||
Si el error persiste, verificar:
|
**Mejora:** Valores como "Installed", "New_LoRa" se convierten automáticamente a "ACTIVE".
|
||||||
|
**Archivo:** `water-api/src/services/bulk-upload.service.ts:210-225`
|
||||||
a) **Error de transformación de datos:**
|
|
||||||
- El `transformArray` puede estar recibiendo undefined
|
|
||||||
- Verificar en `src/api/readings.ts` línea ~120
|
|
||||||
|
|
||||||
b) **Error en componentes React:**
|
|
||||||
- Algún componente puede estar accediendo a propiedades que no existen
|
|
||||||
- Verificar `src/pages/consumption/ConsumptionPage.tsx`
|
|
||||||
- Verificar `src/pages/meters/MeterPage.tsx`
|
|
||||||
|
|
||||||
c) **Problema con el cliente API:**
|
|
||||||
- El `parseResponse` en `src/api/client.ts` línea ~211
|
|
||||||
- Verificar que la detección de pagination funcione correctamente
|
|
||||||
|
|
||||||
### 4. Test rápido del cliente
|
|
||||||
Agregar console.log temporal en `src/api/client.ts`:
|
|
||||||
```typescript
|
|
||||||
// En la función parseResponse, después de línea 221
|
|
||||||
console.log('API Response:', data);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Estructura de Archivos Clave
|
|
||||||
|
|
||||||
```
|
|
||||||
water-project/
|
|
||||||
├── src/
|
|
||||||
│ ├── api/
|
|
||||||
│ │ ├── client.ts # Cliente HTTP con JWT
|
|
||||||
│ │ ├── readings.ts # API de lecturas
|
|
||||||
│ │ ├── meters.ts # API de medidores
|
|
||||||
│ │ ├── projects.ts # API de proyectos
|
|
||||||
│ │ └── concentrators.ts # API de concentradores
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ ├── meters/
|
|
||||||
│ │ │ ├── MeterPage.tsx
|
|
||||||
│ │ │ ├── useMeters.ts # Hook para cargar datos
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ └── consumption/
|
|
||||||
│ │ ├── ConsumptionPage.tsx
|
|
||||||
│ │ └── ReadingsBulkUploadModal.tsx
|
|
||||||
│ └── ...
|
|
||||||
├── water-api/
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── controllers/
|
|
||||||
│ │ │ ├── reading.controller.ts
|
|
||||||
│ │ │ ├── meter.controller.ts
|
|
||||||
│ │ │ └── bulk-upload.controller.ts
|
|
||||||
│ │ ├── services/
|
|
||||||
│ │ │ ├── reading.service.ts
|
|
||||||
│ │ │ ├── meter.service.ts
|
|
||||||
│ │ │ └── bulk-upload.service.ts
|
|
||||||
│ │ └── routes/
|
|
||||||
│ └── ...
|
|
||||||
└── ESTADO_ACTUAL.md (este archivo)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -172,83 +192,58 @@ npm run dev
|
|||||||
cd /home/GRH/water-project
|
cd /home/GRH/water-project
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
# Verificar TypeScript (ignorar TS6133 - variables no usadas)
|
# Compilar backend
|
||||||
cd /home/GRH/water-project
|
cd /home/GRH/water-project/water-api
|
||||||
npx tsc --noEmit 2>&1 | grep -v "TS6133"
|
npm run build
|
||||||
|
|
||||||
# Ver logs del backend
|
# Ver logs del backend
|
||||||
tail -f /tmp/water-api.log
|
tail -f /tmp/water-api.log
|
||||||
|
|
||||||
# Conectar a PostgreSQL
|
|
||||||
psql -U postgres -d water_project
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Esquema de Base de Datos Relevante
|
## Estructura de Archivos
|
||||||
|
|
||||||
### Tabla `meter_readings`
|
|
||||||
```sql
|
|
||||||
- id (UUID)
|
|
||||||
- meter_id (UUID, FK → meters)
|
|
||||||
- reading_value (DECIMAL)
|
|
||||||
- reading_type (VARCHAR) -- AUTOMATIC, MANUAL, SCHEDULED
|
|
||||||
- battery_level (SMALLINT, nullable)
|
|
||||||
- signal_strength (SMALLINT, nullable)
|
|
||||||
- raw_payload (TEXT, nullable)
|
|
||||||
- received_at (TIMESTAMP)
|
|
||||||
- created_at (TIMESTAMP)
|
|
||||||
```
|
```
|
||||||
|
water-project/
|
||||||
**NOTA:** La columna `device_id` NO EXISTE en esta tabla. Fue removida del código.
|
├── src/ # Frontend React
|
||||||
|
│ ├── api/ # Cliente API
|
||||||
### Respuesta del API `/api/readings`
|
│ │ ├── client.ts # Cliente HTTP con JWT
|
||||||
```json
|
│ │ ├── meters.ts # API de medidores
|
||||||
{
|
│ │ ├── readings.ts # API de lecturas
|
||||||
"success": true,
|
│ │ ├── projects.ts # API de proyectos
|
||||||
"data": [
|
│ │ └── concentrators.ts # API de concentradores
|
||||||
{
|
│ ├── pages/ # Páginas
|
||||||
"id": "...",
|
│ │ ├── meters/ # Módulo de medidores
|
||||||
"meter_id": "...",
|
│ │ │ ├── MeterPage.tsx
|
||||||
"reading_value": "300.0000",
|
│ │ │ ├── MetersTable.tsx
|
||||||
"reading_type": "MANUAL",
|
│ │ │ ├── MetersModal.tsx
|
||||||
"battery_level": null,
|
│ │ │ ├── MetersSidebar.tsx
|
||||||
"signal_strength": null,
|
│ │ │ ├── MetersBulkUploadModal.tsx
|
||||||
"raw_payload": null,
|
│ │ │ └── useMeters.ts
|
||||||
"received_at": "2026-01-23T21:07:28.726Z",
|
│ │ ├── consumption/ # Módulo de consumo
|
||||||
"created_at": "2026-01-23T21:07:28.726Z",
|
│ │ │ ├── ConsumptionPage.tsx
|
||||||
"meter_serial_number": "24300001",
|
│ │ │ └── ReadingsBulkUploadModal.tsx
|
||||||
"meter_name": "D307_IB-113107",
|
│ │ └── ...
|
||||||
"meter_location": null,
|
│ └── components/ # Componentes reutilizables
|
||||||
"concentrator_id": "...",
|
│
|
||||||
"concentrator_name": "Adamant",
|
└── water-api/ # Backend Node.js
|
||||||
"project_id": "...",
|
├── src/
|
||||||
"project_name": "ADAMANT"
|
│ ├── controllers/ # Controladores REST
|
||||||
}
|
│ ├── services/ # Lógica de negocio
|
||||||
],
|
│ │ ├── bulk-upload.service.ts
|
||||||
"pagination": {
|
│ │ └── ...
|
||||||
"page": 1,
|
│ ├── routes/ # Definición de rutas
|
||||||
"pageSize": 100,
|
│ ├── middleware/ # Middlewares (auth, etc.)
|
||||||
"total": 206,
|
│ └── config/ # Configuración (DB, etc.)
|
||||||
"totalPages": 3
|
└── sql/ # Scripts SQL
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Próximos Pasos Sugeridos
|
## Próximos Pasos Sugeridos
|
||||||
|
|
||||||
1. **Resolver pantalla blanca** - Obtener el error específico de la consola del navegador
|
1. **Integración TTS** - Webhooks para The Things Stack
|
||||||
2. **Probar carga masiva** - Una vez que las páginas funcionen
|
2. **Alertas automáticas** - Notificaciones por consumo anormal
|
||||||
3. **Integración TTS** - Webhooks para The Things Stack (pendiente)
|
3. **Reportes** - Generación de reportes PDF
|
||||||
4. **Despliegue** - Configurar para producción
|
4. **Despliegue** - Configurar para producción
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contacto del Plan Original
|
|
||||||
|
|
||||||
El plan completo de migración está en:
|
|
||||||
```
|
|
||||||
/home/GRH/.claude/plans/peaceful-napping-bumblebee.md
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export default function ConsumptionPage() {
|
|||||||
r.meterName || "—",
|
r.meterName || "—",
|
||||||
r.meterSerialNumber || "—",
|
r.meterSerialNumber || "—",
|
||||||
r.meterLocation || "—",
|
r.meterLocation || "—",
|
||||||
r.readingValue.toFixed(2),
|
Number(r.readingValue).toFixed(2),
|
||||||
r.readingType || "—",
|
r.readingType || "—",
|
||||||
r.batteryLevel !== null ? `${r.batteryLevel}%` : "—",
|
r.batteryLevel !== null ? `${r.batteryLevel}%` : "—",
|
||||||
r.signalStrength !== null ? `${r.signalStrength} dBm` : "—",
|
r.signalStrength !== null ? `${r.signalStrength} dBm` : "—",
|
||||||
@@ -210,7 +210,7 @@ export default function ConsumptionPage() {
|
|||||||
<StatCard
|
<StatCard
|
||||||
icon={<Droplets />}
|
icon={<Droplets />}
|
||||||
label="Consumo Promedio"
|
label="Consumo Promedio"
|
||||||
value={`${summary?.avgReading.toFixed(1) ?? "0"} m³`}
|
value={`${summary?.avgReading != null ? Number(summary.avgReading).toFixed(1) : "0"} m³`}
|
||||||
loading={loadingSummary}
|
loading={loadingSummary}
|
||||||
gradient="from-violet-500 to-purple-600"
|
gradient="from-violet-500 to-purple-600"
|
||||||
/>
|
/>
|
||||||
@@ -429,7 +429,7 @@ export default function ConsumptionPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-3.5 text-right">
|
<td className="px-5 py-3.5 text-right">
|
||||||
<span className="text-sm font-semibold text-slate-800 tabular-nums">
|
<span className="text-sm font-semibold text-slate-800 tabular-nums">
|
||||||
{reading.readingValue.toFixed(2)}
|
{Number(reading.readingValue).toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-slate-400 ml-1">m³</span>
|
<span className="text-xs text-slate-400 ml-1">m³</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -331,11 +331,13 @@ export default function MetersPage({
|
|||||||
|
|
||||||
{showBulkUpload && (
|
{showBulkUpload && (
|
||||||
<MetersBulkUploadModal
|
<MetersBulkUploadModal
|
||||||
onClose={() => setShowBulkUpload(false)}
|
onClose={() => {
|
||||||
onSuccess={() => {
|
|
||||||
m.loadMeters();
|
m.loadMeters();
|
||||||
setShowBulkUpload(false);
|
setShowBulkUpload(false);
|
||||||
}}
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
m.loadMeters();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function MetersTable({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ title: "Concentrador", field: "concentratorName", render: (r: Meter) => r.concentratorName || "-" },
|
{ title: "Concentrador", field: "concentratorName", render: (r: Meter) => r.concentratorName || "-" },
|
||||||
{ title: "Última Lectura", field: "lastReadingValue", render: (r: Meter) => r.lastReadingValue?.toFixed(2) ?? "-" },
|
{ title: "Última Lectura", field: "lastReadingValue", render: (r: Meter) => r.lastReadingValue != null ? Number(r.lastReadingValue).toFixed(2) : "-" },
|
||||||
]}
|
]}
|
||||||
data={disabled ? [] : data}
|
data={disabled ? [] : data}
|
||||||
onRowClick={(_, rowData) => onRowClick(rowData as Meter)}
|
onRowClick={(_, rowData) => onRowClick(rowData as Meter)}
|
||||||
|
|||||||
@@ -63,27 +63,54 @@ function normalizeColumnName(name: string): string {
|
|||||||
|
|
||||||
// Map common variations
|
// Map common variations
|
||||||
const mappings: Record<string, string> = {
|
const mappings: Record<string, string> = {
|
||||||
|
// Serial number
|
||||||
'serial': 'serial_number',
|
'serial': 'serial_number',
|
||||||
'numero_de_serie': 'serial_number',
|
'numero_de_serie': 'serial_number',
|
||||||
'serial_number': 'serial_number',
|
'serial_number': 'serial_number',
|
||||||
|
'device_s/n': 'serial_number',
|
||||||
|
'device_sn': 'serial_number',
|
||||||
|
's/n': 'serial_number',
|
||||||
|
'sn': 'serial_number',
|
||||||
|
// Meter ID
|
||||||
'meter_id': 'meter_id',
|
'meter_id': 'meter_id',
|
||||||
'meterid': 'meter_id',
|
'meterid': 'meter_id',
|
||||||
'id_medidor': 'meter_id',
|
'id_medidor': 'meter_id',
|
||||||
|
// Name
|
||||||
'nombre': 'name',
|
'nombre': 'name',
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
|
'device_name': 'name',
|
||||||
|
'meter_name': 'name',
|
||||||
|
'nombre_medidor': 'name',
|
||||||
|
// Concentrator
|
||||||
'concentrador': 'concentrator_serial',
|
'concentrador': 'concentrator_serial',
|
||||||
'concentrator': 'concentrator_serial',
|
'concentrator': 'concentrator_serial',
|
||||||
'concentrator_serial': 'concentrator_serial',
|
'concentrator_serial': 'concentrator_serial',
|
||||||
'serial_concentrador': 'concentrator_serial',
|
'serial_concentrador': 'concentrator_serial',
|
||||||
|
'gateway': 'concentrator_serial',
|
||||||
|
'gateway_serial': 'concentrator_serial',
|
||||||
|
// Location
|
||||||
'ubicacion': 'location',
|
'ubicacion': 'location',
|
||||||
'location': 'location',
|
'location': 'location',
|
||||||
|
'direccion': 'location',
|
||||||
|
'address': 'location',
|
||||||
|
// Type
|
||||||
'tipo': 'type',
|
'tipo': 'type',
|
||||||
'type': 'type',
|
'type': 'type',
|
||||||
|
'device_type': 'type',
|
||||||
|
'tipo_dispositivo': 'type',
|
||||||
|
'protocol': 'type',
|
||||||
|
'protocolo': 'type',
|
||||||
|
// Status
|
||||||
'estado': 'status',
|
'estado': 'status',
|
||||||
'status': 'status',
|
'status': 'status',
|
||||||
|
'device_status': 'status',
|
||||||
|
'estado_dispositivo': 'status',
|
||||||
|
// Installation date
|
||||||
'fecha_instalacion': 'installation_date',
|
'fecha_instalacion': 'installation_date',
|
||||||
'installation_date': 'installation_date',
|
'installation_date': 'installation_date',
|
||||||
'fecha_de_instalacion': 'installation_date',
|
'fecha_de_instalacion': 'installation_date',
|
||||||
|
'installed_time': 'installation_date',
|
||||||
|
'installed_date': 'installation_date',
|
||||||
};
|
};
|
||||||
|
|
||||||
return mappings[normalized] || normalized;
|
return mappings[normalized] || normalized;
|
||||||
@@ -181,6 +208,19 @@ export async function bulkUploadMeters(buffer: Buffer): Promise<BulkUploadResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare meter data
|
// Prepare meter data
|
||||||
|
// Validate installation_date is actually a valid date
|
||||||
|
let installationDate: string | undefined = undefined;
|
||||||
|
if (row.installation_date) {
|
||||||
|
const dateStr = String(row.installation_date).trim();
|
||||||
|
// Check if it looks like a date (contains numbers and possibly dashes/slashes)
|
||||||
|
if (/^\d{4}[-/]\d{1,2}[-/]\d{1,2}/.test(dateStr) || /^\d{1,2}[-/]\d{1,2}[-/]\d{2,4}/.test(dateStr)) {
|
||||||
|
const parsed = new Date(dateStr);
|
||||||
|
if (!isNaN(parsed.getTime())) {
|
||||||
|
installationDate = parsed.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const meterData: MeterRow = {
|
const meterData: MeterRow = {
|
||||||
serial_number: String(row.serial_number).trim(),
|
serial_number: String(row.serial_number).trim(),
|
||||||
meter_id: row.meter_id ? String(row.meter_id).trim() : undefined,
|
meter_id: row.meter_id ? String(row.meter_id).trim() : undefined,
|
||||||
@@ -189,7 +229,7 @@ export async function bulkUploadMeters(buffer: Buffer): Promise<BulkUploadResult
|
|||||||
location: row.location ? String(row.location).trim() : undefined,
|
location: row.location ? String(row.location).trim() : undefined,
|
||||||
type: row.type ? String(row.type).trim().toUpperCase() : 'LORA',
|
type: row.type ? String(row.type).trim().toUpperCase() : 'LORA',
|
||||||
status: row.status ? String(row.status).trim().toUpperCase() : 'ACTIVE',
|
status: row.status ? String(row.status).trim().toUpperCase() : 'ACTIVE',
|
||||||
installation_date: row.installation_date ? String(row.installation_date).trim() : undefined,
|
installation_date: installationDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate type
|
// Validate type
|
||||||
@@ -198,11 +238,23 @@ export async function bulkUploadMeters(buffer: Buffer): Promise<BulkUploadResult
|
|||||||
meterData.type = 'LORA';
|
meterData.type = 'LORA';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate status
|
// Validate and normalize status
|
||||||
const validStatuses = ['ACTIVE', 'INACTIVE', 'MAINTENANCE', 'FAULTY', 'REPLACED'];
|
const statusMappings: Record<string, string> = {
|
||||||
if (!validStatuses.includes(meterData.status!)) {
|
'ACTIVE': 'ACTIVE',
|
||||||
meterData.status = 'ACTIVE';
|
'INACTIVE': 'INACTIVE',
|
||||||
}
|
'MAINTENANCE': 'MAINTENANCE',
|
||||||
|
'FAULTY': 'FAULTY',
|
||||||
|
'REPLACED': 'REPLACED',
|
||||||
|
'INSTALLED': 'ACTIVE',
|
||||||
|
'NEW_LORA': 'ACTIVE',
|
||||||
|
'NEW': 'ACTIVE',
|
||||||
|
'ENABLED': 'ACTIVE',
|
||||||
|
'DISABLED': 'INACTIVE',
|
||||||
|
'OFFLINE': 'INACTIVE',
|
||||||
|
'ONLINE': 'ACTIVE',
|
||||||
|
};
|
||||||
|
const normalizedStatus = meterData.status?.toUpperCase().replace(/\s+/g, '_') || 'ACTIVE';
|
||||||
|
meterData.status = statusMappings[normalizedStatus] || 'ACTIVE';
|
||||||
|
|
||||||
// Insert meter
|
// Insert meter
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user