Fase 5 Completa: Implementada la generación de CSV automática y manual.
This commit is contained in:
36
backend/src/Titulares.Api/Controllers/AccionesController.cs
Normal file
36
backend/src/Titulares.Api/Controllers/AccionesController.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// backend/src/Titulares.Api/Controllers/AccionesController.cs
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Titulares.Api.Data;
|
||||||
|
using Titulares.Api.Services;
|
||||||
|
|
||||||
|
namespace Titulares.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AccionesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly TitularRepositorio _repositorio;
|
||||||
|
private readonly CsvService _csvService;
|
||||||
|
|
||||||
|
public AccionesController(TitularRepositorio repositorio, CsvService csvService)
|
||||||
|
{
|
||||||
|
_repositorio = repositorio;
|
||||||
|
_csvService = csvService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("generar-csv")]
|
||||||
|
public async Task<IActionResult> GenerarCsvManual()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var titulares = await _repositorio.ObtenerTodosAsync();
|
||||||
|
await _csvService.GenerarCsvAsync(titulares);
|
||||||
|
return Ok(new { message = "CSV generado manualmente con éxito." });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, $"Error al generar el CSV: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ public class Titular
|
|||||||
public string? Encabezado { get; set; }
|
public string? Encabezado { get; set; }
|
||||||
public string? Tipo { get; set; }
|
public string? Tipo { get; set; }
|
||||||
public string? Fuente { get; set; }
|
public string? Fuente { get; set; }
|
||||||
|
public string? Viñeta { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// DTO (Data Transfer Object) para la creación de un titular manual
|
// DTO (Data Transfer Object) para la creación de un titular manual
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ builder.Services.Configure<ConfiguracionApp>(builder.Configuration);
|
|||||||
// Añadimos nuestro repositorio personalizado
|
// Añadimos nuestro repositorio personalizado
|
||||||
builder.Services.AddSingleton<TitularRepositorio>();
|
builder.Services.AddSingleton<TitularRepositorio>();
|
||||||
builder.Services.AddScoped<ScrapingService>();
|
builder.Services.AddScoped<ScrapingService>();
|
||||||
|
builder.Services.AddScoped<CsvService>();
|
||||||
|
|
||||||
// Añadimos la política de CORS
|
// Añadimos la política de CORS
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
|
|||||||
83
backend/src/Titulares.Api/Services/CsvService.cs
Normal file
83
backend/src/Titulares.Api/Services/CsvService.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// backend/src/Titulares.Api/Services/CsvService.cs
|
||||||
|
|
||||||
|
using CsvHelper;
|
||||||
|
using CsvHelper.Configuration;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Globalization;
|
||||||
|
using Titulares.Api.Models;
|
||||||
|
|
||||||
|
namespace Titulares.Api.Services;
|
||||||
|
|
||||||
|
public class CsvService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CsvService> _logger;
|
||||||
|
private readonly IOptionsMonitor<ConfiguracionApp> _configuracion;
|
||||||
|
|
||||||
|
public CsvService(ILogger<CsvService> logger, IOptionsMonitor<ConfiguracionApp> configuracion)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuracion = configuracion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task GenerarCsvAsync(IEnumerable<Titular> titulares)
|
||||||
|
{
|
||||||
|
var rutaArchivo = _configuracion.CurrentValue.RutaCsv;
|
||||||
|
_logger.LogInformation("Iniciando generación de CSV en: {Ruta}", rutaArchivo);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// La cláusula Where asegura que los Encabezados no sean nulos o vacíos.
|
||||||
|
var grupos = titulares
|
||||||
|
.Where(t => !string.IsNullOrEmpty(t.Encabezado))
|
||||||
|
.GroupBy(t => t.Encabezado);
|
||||||
|
|
||||||
|
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
|
||||||
|
{
|
||||||
|
Delimiter = ";",
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var writer = new StreamWriter(rutaArchivo);
|
||||||
|
await using var csv = new CsvWriter(writer, config);
|
||||||
|
|
||||||
|
// Escribimos la cabecera principal
|
||||||
|
csv.WriteField("Text");
|
||||||
|
csv.WriteField("Heading");
|
||||||
|
csv.WriteField("Value");
|
||||||
|
csv.WriteField("Up");
|
||||||
|
csv.WriteField("Down");
|
||||||
|
csv.WriteField("Change");
|
||||||
|
csv.WriteField("Bullet");
|
||||||
|
await csv.NextRecordAsync();
|
||||||
|
|
||||||
|
foreach (var grupo in grupos)
|
||||||
|
{
|
||||||
|
csv.WriteField("");
|
||||||
|
csv.WriteField($" {grupo.Key!.ToUpper()} ");
|
||||||
|
csv.WriteField("");
|
||||||
|
await csv.NextRecordAsync();
|
||||||
|
|
||||||
|
await csv.NextRecordAsync();
|
||||||
|
|
||||||
|
foreach (var titular in grupo)
|
||||||
|
{
|
||||||
|
csv.WriteField(titular.Texto);
|
||||||
|
csv.WriteField(""); // Heading
|
||||||
|
csv.WriteField(""); // Value
|
||||||
|
csv.WriteField(""); // Up
|
||||||
|
csv.WriteField(""); // Down
|
||||||
|
csv.WriteField(""); // Change
|
||||||
|
csv.WriteField(titular.Viñeta ?? "•");
|
||||||
|
await csv.NextRecordAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await csv.NextRecordAsync();
|
||||||
|
}
|
||||||
|
_logger.LogInformation("CSV generado exitosamente.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error al generar el archivo CSV.");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ public class ProcesoScrapingWorker : BackgroundService
|
|||||||
var repositorio = scope.ServiceProvider.GetRequiredService<TitularRepositorio>();
|
var repositorio = scope.ServiceProvider.GetRequiredService<TitularRepositorio>();
|
||||||
var scrapingService = scope.ServiceProvider.GetRequiredService<ScrapingService>();
|
var scrapingService = scope.ServiceProvider.GetRequiredService<ScrapingService>();
|
||||||
var hubContext = scope.ServiceProvider.GetRequiredService<IHubContext<TitularesHub>>();
|
var hubContext = scope.ServiceProvider.GetRequiredService<IHubContext<TitularesHub>>();
|
||||||
|
var csvService = scope.ServiceProvider.GetRequiredService<CsvService>();
|
||||||
|
|
||||||
// Obtener estos valores desde la configuración
|
// Obtener estos valores desde la configuración
|
||||||
int cantidadAObtener = configActual.CantidadTitularesAScrapear;
|
int cantidadAObtener = configActual.CantidadTitularesAScrapear;
|
||||||
@@ -49,14 +50,14 @@ public class ProcesoScrapingWorker : BackgroundService
|
|||||||
|
|
||||||
if (articulosScrapeados.Any())
|
if (articulosScrapeados.Any())
|
||||||
{
|
{
|
||||||
// 2. Sincronizar con la base de datos
|
|
||||||
await repositorio.SincronizarDesdeScraping(articulosScrapeados, limiteTotalEnDb);
|
await repositorio.SincronizarDesdeScraping(articulosScrapeados, limiteTotalEnDb);
|
||||||
_logger.LogInformation("Sincronización con la base de datos completada.");
|
_logger.LogInformation("Sincronización con la base de datos completada.");
|
||||||
|
|
||||||
// 3. Notificar a todos los clientes a través de SignalR
|
|
||||||
var titularesActualizados = await repositorio.ObtenerTodosAsync();
|
var titularesActualizados = await repositorio.ObtenerTodosAsync();
|
||||||
await hubContext.Clients.All.SendAsync("TitularesActualizados", titularesActualizados, stoppingToken);
|
await hubContext.Clients.All.SendAsync("TitularesActualizados", titularesActualizados, stoppingToken);
|
||||||
_logger.LogInformation("Notificación enviada a los clientes.");
|
_logger.LogInformation("Notificación enviada a los clientes.");
|
||||||
|
|
||||||
|
await csvService.GenerarCsvAsync(titularesActualizados);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// frontend/src/components/Dashboard.tsx
|
// frontend/src/components/Dashboard.tsx
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { Box, Button, Typography, Stack, Chip } from '@mui/material';
|
import { Box, Button, Typography, Stack, Chip, CircularProgress } from '@mui/material';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import SyncIcon from '@mui/icons-material/Sync';
|
import SyncIcon from '@mui/icons-material/Sync';
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ import AddTitularModal from './AddTitularModal';
|
|||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [titulares, setTitulares] = useState<Titular[]>([]);
|
const [titulares, setTitulares] = useState<Titular[]>([]);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [isGeneratingCsv, setIsGeneratingCsv] = useState(false);
|
||||||
|
|
||||||
// Usamos useCallback para que la función de callback no se recree en cada render,
|
// Usamos useCallback para que la función de callback no se recree en cada render,
|
||||||
// evitando que el useEffect del hook se ejecute innecesariamente.
|
// evitando que el useEffect del hook se ejecute innecesariamente.
|
||||||
@@ -81,6 +82,19 @@ const Dashboard = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGenerateCsv = async () => {
|
||||||
|
setIsGeneratingCsv(true);
|
||||||
|
try {
|
||||||
|
await api.generarCsvManual();
|
||||||
|
// Opcional: mostrar una notificación de éxito
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error al generar CSV manualmente", error);
|
||||||
|
// Opcional: mostrar una notificación de error
|
||||||
|
} finally {
|
||||||
|
setIsGeneratingCsv(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
@@ -91,8 +105,13 @@ const Dashboard = () => {
|
|||||||
{getStatusChip()}
|
{getStatusChip()}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction="row" spacing={2}>
|
<Stack direction="row" spacing={2}>
|
||||||
<Button variant="outlined" startIcon={<SyncIcon />}>
|
<Button
|
||||||
Generate CSV
|
variant="outlined"
|
||||||
|
startIcon={isGeneratingCsv ? <CircularProgress size={20} /> : <SyncIcon />}
|
||||||
|
onClick={handleGenerateCsv}
|
||||||
|
disabled={isGeneratingCsv}
|
||||||
|
>
|
||||||
|
{isGeneratingCsv ? 'Generando...' : 'Generate CSV'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setModalOpen(true)}>
|
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setModalOpen(true)}>
|
||||||
Add Manual
|
Add Manual
|
||||||
|
|||||||
@@ -39,4 +39,8 @@ export const obtenerConfiguracion = async (): Promise<Configuracion> => {
|
|||||||
|
|
||||||
export const guardarConfiguracion = async (config: Configuracion): Promise<void> => {
|
export const guardarConfiguracion = async (config: Configuracion): Promise<void> => {
|
||||||
await apiClient.post('/configuracion', config);
|
await apiClient.post('/configuracion', config);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generarCsvManual = async (): Promise<void> => {
|
||||||
|
await apiClient.post('/acciones/generar-csv');
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user