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:
Exteban08
2026-01-23 23:13:48 +00:00
parent ab97987c6a
commit 6c7d448b2f
6 changed files with 388 additions and 299 deletions

View File

@@ -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) {
return {
data: data.data,
pagination: data.pagination,
} as T;
}
return data.data as T;
}
``` ```
### 2. `src/api/readings.ts` **`src/pages/consumption/ConsumptionPage.tsx` (líneas 133, 213, 432)**
**Cambios en interface `MeterReading`:**
```typescript
// ELIMINADO:
deviceId: string | null;
areaName: string | null;
// AGREGADO:
meterLocation: string | null;
concentratorId: string;
concentratorName: string;
projectName: string;
```
**Cambios en interface `ReadingFilters`:**
```typescript
// ELIMINADO:
areaName?: string;
// AGREGADO:
concentratorId?: string;
```
**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 ```typescript
// ANTES: // ANTES:
const response = await apiClient.get<Record<string, unknown>[]>('/api/projects'); r.readingValue.toFixed(2)
return transformArray<Project>(response); summary?.avgReading.toFixed(1)
reading.readingValue.toFixed(2)
// DESPUÉS: // DESPUÉS:
const response = await apiClient.get<...>('/api/projects'); Number(r.readingValue).toFixed(2)
if (response && typeof response === 'object' && 'data' in response && Array.isArray(response.data)) { summary?.avgReading != null ? Number(summary.avgReading).toFixed(1) : "0"
return transformArray<Project>(response.data); 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 transformArray<Project>(response as Record<string, unknown>[]);
``` ```
### 4. `src/api/concentrators.ts` (función `fetchConcentrators`) Mapeos de columnas adicionales (líneas 65-90):
**Cambio:** Mismo patrón que projects.ts para manejar paginación ```typescript
const mappings: Record<string, string> = {
// Serial number
'device_s/n': 'serial_number',
'device_sn': 'serial_number',
// Name
'device_name': 'name',
'meter_name': 'name',
// Status
'device_status': 'status',
// ... más mapeos
};
```
### 5. `src/pages/consumption/ConsumptionPage.tsx` Normalización de status (líneas 210-225):
**Cambios:** ```typescript
- Línea ~94: `r.areaName``r.meterLocation` (filtro de búsqueda) const statusMappings: Record<string, string> = {
- Línea ~127: `"Área"``"Ubicación"` (header CSV) 'INSTALLED': 'ACTIVE',
- Línea ~132: `r.areaName``r.meterLocation` (datos CSV) 'NEW_LORA': 'ACTIVE',
- Línea ~365: `"Área"``"Ubicación"` (header tabla) 'NEW': 'ACTIVE',
- Línea ~428: `reading.areaName``reading.meterLocation` (celda tabla) 'ENABLED': 'ACTIVE',
'DISABLED': 'INACTIVE',
### 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

View File

@@ -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
```

View File

@@ -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"}`} value={`${summary?.avgReading != null ? Number(summary.avgReading).toFixed(1) : "0"}`}
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>

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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 {