Agregar carga masiva de lecturas y corregir manejo de respuestas paginadas

- Implementar carga masiva de lecturas via Excel (backend y frontend)
- Corregir cliente API para manejar respuestas con paginación
- Eliminar referencias a device_id (columna inexistente)
- Cambiar areaName por meterLocation en lecturas
- Actualizar fetchProjects y fetchConcentrators para paginación
- Agregar documentación del estado actual y cambios

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Exteban08
2026-01-23 21:23:41 +00:00
parent c81a18987f
commit ab97987c6a
14 changed files with 1154 additions and 35 deletions

View File

@@ -1,6 +1,11 @@
import { Request, Response } from 'express';
import multer from 'multer';
import { bulkUploadMeters, generateMeterTemplate } from '../services/bulk-upload.service';
import {
bulkUploadMeters,
generateMeterTemplate,
bulkUploadReadings,
generateReadingTemplate,
} from '../services/bulk-upload.service';
// Configure multer for memory storage
const storage = multer.memoryStorage();
@@ -11,13 +16,17 @@ export const upload = multer({
fileSize: 10 * 1024 * 1024, // 10MB max
},
fileFilter: (_req, file, cb) => {
// Accept Excel files only
// Accept Excel files only - check both MIME type and extension
const allowedMimes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-excel', // .xls
'application/octet-stream', // Generic binary (some systems send this)
];
if (allowedMimes.includes(file.mimetype)) {
const allowedExtensions = ['.xlsx', '.xls'];
const fileExtension = file.originalname.toLowerCase().slice(file.originalname.lastIndexOf('.'));
if (allowedMimes.includes(file.mimetype) || allowedExtensions.includes(fileExtension)) {
cb(null, true);
} else {
cb(new Error('Solo se permiten archivos Excel (.xlsx, .xls)'));
@@ -80,3 +89,59 @@ export async function downloadMeterTemplate(_req: Request, res: Response): Promi
});
}
}
/**
* POST /api/bulk-upload/readings
* Upload Excel file with readings data
*/
export async function uploadReadings(req: Request, res: Response): Promise<void> {
try {
if (!req.file) {
res.status(400).json({
success: false,
error: 'No se proporcionó ningún archivo',
});
return;
}
const result = await bulkUploadReadings(req.file.buffer);
res.status(result.success ? 200 : 207).json({
success: result.success,
data: {
totalRows: result.totalRows,
inserted: result.inserted,
failed: result.errors.length,
errors: result.errors.slice(0, 50), // Limit errors in response
},
});
} catch (err) {
const error = err as Error;
console.error('Error in readings bulk upload:', error);
res.status(500).json({
success: false,
error: error.message || 'Error procesando la carga masiva de lecturas',
});
}
}
/**
* GET /api/bulk-upload/readings/template
* Download Excel template for readings
*/
export async function downloadReadingTemplate(_req: Request, res: Response): Promise<void> {
try {
const buffer = generateReadingTemplate();
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename=plantilla_lecturas.xlsx');
res.send(buffer);
} catch (err) {
const error = err as Error;
console.error('Error generating readings template:', error);
res.status(500).json({
success: false,
error: 'Error generando la plantilla de lecturas',
});
}
}

View File

@@ -11,7 +11,7 @@ import * as readingService from '../services/reading.service';
export async function getAll(req: Request, res: Response): Promise<void> {
try {
const page = parseInt(req.query.page as string, 10) || 1;
const pageSize = Math.min(parseInt(req.query.pageSize as string, 10) || 10, 100);
const pageSize = Math.min(parseInt(req.query.pageSize as string, 10) || 50, 1000);
const filters: meterService.MeterFilters = {};