Feat: Edición y Manejo de Titulares, entre otros.

This commit is contained in:
2025-10-28 14:12:05 -03:00
parent 3c12a89f76
commit 5b3dede4d5
12 changed files with 216 additions and 99 deletions

View File

@@ -47,9 +47,13 @@ public class TitularesController : ControllerBase
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<IActionResult> Actualizar(int id, [FromBody] ActualizarTitularDto titularDto) public async Task<IActionResult> Actualizar(int id, [FromBody] ActualizarTitularDto titularDto)
{ {
var resultado = await _repositorio.ActualizarTextoAsync(id, titularDto); if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var resultado = await _repositorio.ActualizarTitularAsync(id, titularDto);
if (!resultado) return NotFound(); if (!resultado) return NotFound();
await NotificarCambios(); // Notificamos después de actualizar await NotificarCambios();
return NoContent(); return NoContent();
} }

View File

@@ -39,15 +39,25 @@ public class TitularRepositorio
return await connection.ExecuteScalarAsync<int>(sql, new { titularDto.Texto, OrdenVisual = nuevoOrden }); return await connection.ExecuteScalarAsync<int>(sql, new { titularDto.Texto, OrdenVisual = nuevoOrden });
} }
public async Task<bool> ActualizarTextoAsync(int id, ActualizarTitularDto titularDto) public async Task<bool> ActualizarTitularAsync(int id, ActualizarTitularDto titularDto)
{ {
using var connection = CreateConnection(); using var connection = CreateConnection();
// La consulta SQL actualiza Texto, Viñeta y las banderas de estado
var sql = @" var sql = @"
UPDATE Titulares UPDATE Titulares
SET Texto = @Texto, ModificadoPorUsuario = 1, Tipo = 'Edited' SET
Texto = @Texto,
Viñeta = @Viñeta,
ModificadoPorUsuario = 1,
Tipo = 'Edited'
WHERE Id = @Id; WHERE Id = @Id;
"; ";
var affectedRows = await connection.ExecuteAsync(sql, new { titularDto.Texto, Id = id }); var affectedRows = await connection.ExecuteAsync(sql, new
{
titularDto.Texto,
titularDto.Viñeta,
Id = id
});
return affectedRows > 0; return affectedRows > 0;
} }
@@ -83,7 +93,7 @@ public class TitularRepositorio
} }
} }
public async Task SincronizarDesdeScraping(List<ArticuloScrapeado> articulosScrapeados, int limiteTotal) public async Task SincronizarDesdeScraping(List<ArticuloScrapeado> articulosScrapeados, int cantidadALimitar)
{ {
using var connection = CreateConnection(); using var connection = CreateConnection();
await connection.OpenAsync(); await connection.OpenAsync();
@@ -91,12 +101,11 @@ public class TitularRepositorio
try try
{ {
// 1. Obtener las URLs que ya tenemos en la base de datos // --- PARTE 1: AÑADIR NUEVOS TITULARES ---
var urlsEnDb = (await connection.QueryAsync<string>( var urlsEnDb = (await connection.QueryAsync<string>(
"SELECT UrlFuente FROM Titulares WHERE UrlFuente IS NOT NULL", transaction: transaction)) "SELECT UrlFuente FROM Titulares WHERE UrlFuente IS NOT NULL", transaction: transaction))
.ToHashSet(); .ToHashSet();
// 2. Filtrar para quedarnos solo con los artículos que son realmente nuevos
var articulosNuevos = articulosScrapeados var articulosNuevos = articulosScrapeados
.Where(a => !urlsEnDb.Contains(a.UrlFuente)) .Where(a => !urlsEnDb.Contains(a.UrlFuente))
.ToList(); .ToList();
@@ -104,48 +113,45 @@ public class TitularRepositorio
if (articulosNuevos.Any()) if (articulosNuevos.Any())
{ {
var cantidadNuevos = articulosNuevos.Count; var cantidadNuevos = articulosNuevos.Count;
// 3. Hacer espacio para los nuevos: empujamos todos los existentes hacia abajo
await connection.ExecuteAsync( await connection.ExecuteAsync(
"UPDATE Titulares SET OrdenVisual = OrdenVisual + @cantidadNuevos", "UPDATE Titulares SET OrdenVisual = OrdenVisual + @cantidadNuevos",
new { cantidadNuevos }, new { cantidadNuevos },
transaction: transaction); transaction: transaction);
// 4. Insertar los nuevos artículos al principio (OrdenVisual 0, 1, 2...)
for (int i = 0; i < cantidadNuevos; i++) for (int i = 0; i < cantidadNuevos; i++)
{ {
var articulo = articulosNuevos[i]; var articulo = articulosNuevos[i];
var sqlInsert = @" var sqlInsert = @"
INSERT INTO Titulares (Texto, UrlFuente, ModificadoPorUsuario, EsEntradaManual, OrdenVisual, Tipo, Fuente) INSERT INTO Titulares (Texto, UrlFuente, OrdenVisual, Tipo, Fuente)
VALUES (@Texto, @UrlFuente, 0, 0, @OrdenVisual, 'Scraped', 'eldia.com'); VALUES (@Texto, @UrlFuente, @OrdenVisual, 'Scraped', 'eldia.com');
"; ";
await connection.ExecuteAsync(sqlInsert, new await connection.ExecuteAsync(sqlInsert, new { articulo.Texto, articulo.UrlFuente, OrdenVisual = i }, transaction: transaction);
{ }
articulo.Texto,
articulo.UrlFuente,
OrdenVisual = i
}, transaction: transaction);
} }
// 5. Purgar los más antiguos si superamos el límite, ignorando los manuales // --- PARTE 2: PURGAR LOS SOBRANTES ---
var sqlDelete = @" var sqlDelete = @"
DELETE FROM Titulares DELETE FROM Titulares
WHERE Id IN ( WHERE Id IN (
SELECT Id FROM Titulares -- Seleccionamos los IDs de los titulares que queremos borrar.
-- Son aquellos que no son manuales, ordenados por antigüedad (los más nuevos primero),
-- y nos saltamos los primeros 'N' que queremos conservar.
SELECT Id
FROM Titulares
WHERE EsEntradaManual = 0 WHERE EsEntradaManual = 0
ORDER BY OrdenVisual DESC ORDER BY OrdenVisual ASC
OFFSET @limiteTotal ROWS OFFSET @Limite ROWS
); );
"; ";
await connection.ExecuteAsync(sqlDelete, new { limiteTotal }, transaction: transaction); // Usamos cantidadALimitar como el límite de cuántos conservar.
} await connection.ExecuteAsync(sqlDelete, new { Limite = cantidadALimitar }, transaction: transaction);
transaction.Commit(); transaction.Commit();
} }
catch catch
{ {
transaction.Rollback(); transaction.Rollback();
throw; // Relanzamos la excepción para que el worker sepa que algo falló throw;
} }
} }
} }

View File

@@ -7,5 +7,5 @@ public class ConfiguracionApp
public string RutaCsv { get; set; } = "C:\\temp\\titulares.csv"; public string RutaCsv { get; set; } = "C:\\temp\\titulares.csv";
public int IntervaloMinutos { get; set; } = 5; public int IntervaloMinutos { get; set; } = 5;
public int CantidadTitularesAScrapear { get; set; } = 20; public int CantidadTitularesAScrapear { get; set; } = 20;
public int LimiteTotalEnDb { get; set; } = 50; //public int LimiteTotalEnDb { get; set; } = 50;
} }

View File

@@ -27,6 +27,7 @@ public class CrearTitularDto
public class ActualizarTitularDto public class ActualizarTitularDto
{ {
public required string Texto { get; set; } public required string Texto { get; set; }
public string? Viñeta { get; set; }
} }
// DTO para el reordenamiento // DTO para el reordenamiento

View File

@@ -26,53 +26,35 @@ public class CsvService
try try
{ {
// La cláusula Where asegura que los Encabezados no sean nulos o vacíos. var directorio = Path.GetDirectoryName(rutaArchivo);
var grupos = titulares if (directorio != null)
.Where(t => !string.IsNullOrEmpty(t.Encabezado)) {
.GroupBy(t => t.Encabezado); Directory.CreateDirectory(directorio);
}
var config = new CsvConfiguration(CultureInfo.InvariantCulture) var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{ {
Delimiter = ";", Delimiter = ";",
HasHeaderRecord = false,
// Esta linea es para que nunca se ponga comillas en ningún campo.
ShouldQuote = args => false,
}; };
await using var writer = new StreamWriter(rutaArchivo); await using var writer = new StreamWriter(rutaArchivo);
await using var csv = new CsvWriter(writer, config); await using var csv = new CsvWriter(writer, config);
// Escribimos la cabecera principal
csv.WriteField("Text"); csv.WriteField("Text");
csv.WriteField("Heading");
csv.WriteField("Value");
csv.WriteField("Up");
csv.WriteField("Down");
csv.WriteField("Change");
csv.WriteField("Bullet"); csv.WriteField("Bullet");
await csv.NextRecordAsync(); await csv.NextRecordAsync();
foreach (var grupo in grupos) foreach (var titular in titulares)
{
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(titular.Texto);
csv.WriteField(""); // Heading
csv.WriteField(""); // Value
csv.WriteField(""); // Up
csv.WriteField(""); // Down
csv.WriteField(""); // Change
csv.WriteField(titular.Viñeta ?? "•"); csv.WriteField(titular.Viñeta ?? "•");
await csv.NextRecordAsync(); await csv.NextRecordAsync();
} }
await csv.NextRecordAsync(); _logger.LogInformation("CSV generado exitosamente con formato simplificado.");
}
_logger.LogInformation("CSV generado exitosamente.");
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -9,17 +9,27 @@ using Titulares.Api.Models;
namespace Titulares.Api.Workers; namespace Titulares.Api.Workers;
public class ProcesoScrapingWorker : BackgroundService public class ProcesoScrapingWorker : BackgroundService, IDisposable
{ {
private readonly ILogger<ProcesoScrapingWorker> _logger; private readonly ILogger<ProcesoScrapingWorker> _logger;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IOptionsMonitor<ConfiguracionApp> _configuracion; private readonly IOptionsMonitor<ConfiguracionApp> _configuracion;
private readonly IDisposable? _optionsReloadToken;
private CancellationTokenSource? _delayCts;
public ProcesoScrapingWorker(ILogger<ProcesoScrapingWorker> logger, IServiceProvider serviceProvider, IOptionsMonitor<ConfiguracionApp> configuracion) public ProcesoScrapingWorker(ILogger<ProcesoScrapingWorker> logger, IServiceProvider serviceProvider, IOptionsMonitor<ConfiguracionApp> configuracion)
{ {
_logger = logger; _logger = logger;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_configuracion = configuracion; _configuracion = configuracion;
_optionsReloadToken = _configuracion.OnChange(OnConfigurationChanged);
}
private void OnConfigurationChanged(ConfiguracionApp newConfig)
{
_logger.LogInformation("La configuración ha cambiado. Interrumpiendo la espera actual para aplicar los nuevos ajustes.");
_delayCts?.Cancel();
} }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -27,43 +37,27 @@ public class ProcesoScrapingWorker : BackgroundService
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
var configActual = _configuracion.CurrentValue; var configActual = _configuracion.CurrentValue;
_logger.LogInformation("Iniciando ciclo de scraping con cantidad: {cantidad}", configActual.CantidadTitularesAScrapear); _logger.LogInformation("Iniciando ciclo de scraping con cantidad: {cantidad}", configActual.CantidadTitularesAScrapear);
try try
{ {
// Creamos un 'scope' para obtener instancias 'scoped' de nuestros servicios.
// Es la práctica correcta en servicios de larga duración.
using (var scope = _serviceProvider.CreateScope()) using (var scope = _serviceProvider.CreateScope())
{ {
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>(); var csvService = scope.ServiceProvider.GetRequiredService<CsvService>();
int cantidadTitulares = configActual.CantidadTitularesAScrapear;
var articulosScrapeados = await scrapingService.ObtenerUltimosTitulares(cantidadTitulares);
// Obtener estos valores desde la configuración await repositorio.SincronizarDesdeScraping(articulosScrapeados, cantidadTitulares);
int cantidadAObtener = configActual.CantidadTitularesAScrapear;
int limiteTotalEnDb = configActual.LimiteTotalEnDb;
// 1. Obtener los últimos titulares de la web
var articulosScrapeados = await scrapingService.ObtenerUltimosTitulares(cantidadAObtener);
if (articulosScrapeados.Any())
{
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.");
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); await csvService.GenerarCsvAsync(titularesActualizados);
} }
else
{
_logger.LogWarning("No se encontraron artículos en el scraping.");
}
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -72,7 +66,29 @@ public class ProcesoScrapingWorker : BackgroundService
var intervaloEnMinutos = configActual.IntervaloMinutos; var intervaloEnMinutos = configActual.IntervaloMinutos;
_logger.LogInformation("Proceso en espera por {minutos} minutos.", intervaloEnMinutos); _logger.LogInformation("Proceso en espera por {minutos} minutos.", intervaloEnMinutos);
await Task.Delay(TimeSpan.FromMinutes(intervaloEnMinutos), stoppingToken);
try
{
_delayCts = new CancellationTokenSource();
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, _delayCts.Token);
await Task.Delay(TimeSpan.FromMinutes(intervaloEnMinutos), linkedCts.Token);
} }
catch (TaskCanceledException)
{
_logger.LogInformation("La espera fue interrumpida. Reiniciando el ciclo.");
}
finally
{
_delayCts?.Dispose();
_delayCts = null;
}
}
}
public override void Dispose()
{
_optionsReloadToken?.Dispose();
_delayCts?.Dispose();
base.Dispose();
} }
} }

View File

@@ -1,6 +1,5 @@
{ {
"RutaCsv": "C:\\temp\\titulares.csv", "RutaCsv": "C:\\temp\\titulares.csv",
"IntervaloMinutos": 5, "IntervaloMinutos": 5,
"CantidadTitularesAScrapear": 5, "CantidadTitularesAScrapear": 5
"LimiteTotalEnDb": 50
} }

View File

@@ -11,11 +11,13 @@ import { useSignalR } from '../hooks/useSignalR';
import FormularioConfiguracion from './FormularioConfiguracion'; import FormularioConfiguracion from './FormularioConfiguracion';
import TablaTitulares from './TablaTitulares'; import TablaTitulares from './TablaTitulares';
import AddTitularModal from './AddTitularModal'; import AddTitularModal from './AddTitularModal';
import EditarTitularModal from './EditarTitularModal';
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); const [isGeneratingCsv, setIsGeneratingCsv] = useState(false);
const [titularAEditar, setTitularAEditar] = useState<Titular | null>(null);
// 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.
@@ -95,6 +97,15 @@ const Dashboard = () => {
} }
}; };
const handleSaveEdit = async (id: number, texto: string, viñeta: string) => {
try {
await api.actualizarTitular(id, { texto, viñeta: viñeta || null });
// SignalR se encargará de actualizar la UI
} catch (err) {
console.error("Error al guardar cambios:", err);
}
};
return ( return (
<> <>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
@@ -120,8 +131,25 @@ const Dashboard = () => {
</Box> </Box>
<FormularioConfiguracion /> <FormularioConfiguracion />
<TablaTitulares titulares={titulares} onReorder={handleReorder} onDelete={handleDelete} /> <TablaTitulares
<AddTitularModal open={modalOpen} onClose={() => setModalOpen(false)} onAdd={handleAdd} /> titulares={titulares}
onReorder={handleReorder}
onDelete={handleDelete}
onEdit={(titular) => setTitularAEditar(titular)}
/>
<AddTitularModal
open={modalOpen}
onClose={() => setModalOpen(false)}
onAdd={handleAdd}
/>
<EditarTitularModal
open={titularAEditar !== null}
onClose={() => setTitularAEditar(null)}
onSave={handleSaveEdit}
titular={titularAEditar}
/>
</> </>
); );
}; };

View File

@@ -0,0 +1,66 @@
// frontend/src/components/EditarTitularModal.tsx
import { useEffect, useState } from 'react';
import { Modal, Box, Typography, TextField, Button } from '@mui/material';
import type { Titular } from '../types';
const style = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
};
interface Props {
open: boolean;
onClose: () => void;
onSave: (id: number, texto: string, viñeta: string) => void;
titular: Titular | null;
}
const EditarTitularModal = ({ open, onClose, onSave, titular }: Props) => {
const [texto, setTexto] = useState('');
const [viñeta, setViñeta] = useState('');
// Este efecto actualiza el estado del formulario cuando se selecciona un titular para editar
useEffect(() => {
if (titular) {
setTexto(titular.texto);
setViñeta(titular.viñeta ?? '•'); // Default a '•' si es nulo
}
}, [titular]);
const handleSave = () => {
if (titular && texto.trim()) {
onSave(titular.id, texto.trim(), viñeta.trim());
onClose();
}
};
return (
<Modal open={open} onClose={onClose}>
<Box sx={style}>
<Typography variant="h6" component="h2">Editar Titular</Typography>
<TextField
fullWidth autoFocus margin="normal" label="Texto del titular"
value={texto} onChange={(e) => setTexto(e.target.value)}
/>
<TextField
fullWidth margin="normal" label="Viñeta (ej: •, !, o dejar vacío)"
value={viñeta} onChange={(e) => setViñeta(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
/>
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={onClose} sx={{ mr: 1 }}>Cancelar</Button>
<Button variant="contained" onClick={handleSave}>Guardar Cambios</Button>
</Box>
</Box>
</Modal>
);
};
export default EditarTitularModal;

View File

@@ -9,14 +9,16 @@ import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type D
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import type { Titular } from '../types'; import type { Titular } from '../types';
import EditIcon from '@mui/icons-material/Edit';
// La prop `onDelete` se añade para comunicar el evento al componente padre // La prop `onDelete` se añade para comunicar el evento al componente padre
interface SortableRowProps { interface SortableRowProps {
titular: Titular; titular: Titular;
onDelete: (id: number) => void; onDelete: (id: number) => void;
onEdit: (titular: Titular) => void;
} }
const SortableRow = ({ titular, onDelete }: SortableRowProps) => { const SortableRow = ({ titular, onDelete, onEdit }: SortableRowProps) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: titular.id }); const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: titular.id });
const style = { const style = {
@@ -42,7 +44,9 @@ const SortableRow = ({ titular, onDelete }: SortableRowProps) => {
</TableCell> </TableCell>
<TableCell>{titular.fuente}</TableCell> <TableCell>{titular.fuente}</TableCell>
<TableCell> <TableCell>
{/* Usamos un stopPropagation para que el clic no active el arrastre */} <IconButton size="small" onClick={(e) => { e.stopPropagation(); onEdit(titular); }}>
<EditIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(titular.id); }}> <IconButton size="small" onClick={(e) => { e.stopPropagation(); onDelete(titular.id); }}>
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
@@ -55,9 +59,10 @@ interface TablaTitularesProps {
titulares: Titular[]; titulares: Titular[];
onReorder: (titulares: Titular[]) => void; onReorder: (titulares: Titular[]) => void;
onDelete: (id: number) => void; onDelete: (id: number) => void;
onEdit: (titular: Titular) => void;
} }
const TablaTitulares = ({ titulares, onReorder, onDelete }: TablaTitularesProps) => { const TablaTitulares = ({ titulares, onReorder, onDelete, onEdit }: TablaTitularesProps) => {
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } })); // Evita activar el drag con un simple clic const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } })); // Evita activar el drag con un simple clic
const handleDragEnd = (event: DragEndEvent) => { const handleDragEnd = (event: DragEndEvent) => {
@@ -95,7 +100,7 @@ const TablaTitulares = ({ titulares, onReorder, onDelete }: TablaTitularesProps)
</TableHead> </TableHead>
<TableBody> <TableBody>
{titulares.map((titular) => ( {titulares.map((titular) => (
<SortableRow key={titular.id} titular={titular} onDelete={onDelete} /> <SortableRow key={titular.id} titular={titular} onDelete={onDelete} onEdit={onEdit} />
))} ))}
</TableBody> </TableBody>
</Table> </Table>

View File

@@ -10,6 +10,11 @@ const apiClient = axios.create({
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}); });
export interface ActualizarTitularPayload {
texto: string;
viñeta: string | null;
}
export const obtenerTitulares = async (): Promise<Titular[]> => { export const obtenerTitulares = async (): Promise<Titular[]> => {
const response = await apiClient.get('/titulares'); const response = await apiClient.get('/titulares');
return response.data; return response.data;
@@ -44,3 +49,7 @@ export const guardarConfiguracion = async (config: Configuracion): Promise<void>
export const generarCsvManual = async (): Promise<void> => { export const generarCsvManual = async (): Promise<void> => {
await apiClient.post('/acciones/generar-csv'); await apiClient.post('/acciones/generar-csv');
}; };
export const actualizarTitular = async (id: number, payload: ActualizarTitularPayload): Promise<void> => {
await apiClient.put(`/titulares/${id}`, payload);
};

View File

@@ -10,11 +10,12 @@ export interface Titular {
fechaCreacion: string; fechaCreacion: string;
tipo: 'Scraped' | 'Edited' | 'Manual' | null; tipo: 'Scraped' | 'Edited' | 'Manual' | null;
fuente: string | null; fuente: string | null;
viñeta: string | null;
} }
export interface Configuracion { export interface Configuracion {
rutaCsv: string; rutaCsv: string;
intervaloMinutos: number; intervaloMinutos: number;
cantidadTitularesAScrapear: number; cantidadTitularesAScrapear: number;
limiteTotalEnDb: number; //limiteTotalEnDb: number;
} }