Fase 3: Refactorizado SignalR a un hook reutilizable (useSignalR) y conectado al Dashboard.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
// backend/src/Titulares.Api/Controllers/TitularesController.cs
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Titulares.Api.Data;
|
||||
using Titulares.Api.Hubs;
|
||||
using Titulares.Api.Models;
|
||||
|
||||
namespace Titulares.Api.Controllers;
|
||||
@@ -11,10 +13,20 @@ namespace Titulares.Api.Controllers;
|
||||
public class TitularesController : ControllerBase
|
||||
{
|
||||
private readonly TitularRepositorio _repositorio;
|
||||
private readonly IHubContext<TitularesHub> _hubContext;
|
||||
|
||||
public TitularesController(TitularRepositorio repositorio)
|
||||
public TitularesController(TitularRepositorio repositorio, IHubContext<TitularesHub> hubContext)
|
||||
{
|
||||
_repositorio = repositorio;
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
private async Task NotificarCambios()
|
||||
{
|
||||
var titularesActualizados = await _repositorio.ObtenerTodosAsync();
|
||||
// Enviamos un mensaje llamado "TitularesActualizados" a TODOS los clientes conectados
|
||||
// y les pasamos la lista completa y actualizada.
|
||||
await _hubContext.Clients.All.SendAsync("TitularesActualizados", titularesActualizados);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -27,40 +39,35 @@ public class TitularesController : ControllerBase
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CrearManual([FromBody] CrearTitularDto titularDto)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
var nuevoId = await _repositorio.CrearManualAsync(titularDto);
|
||||
await NotificarCambios(); // Notificamos después de crear
|
||||
return CreatedAtAction(nameof(ObtenerTodos), new { id = nuevoId }, null);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<IActionResult> Actualizar(int id, [FromBody] ActualizarTitularDto titularDto)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
var resultado = await _repositorio.ActualizarTextoAsync(id, titularDto);
|
||||
return resultado ? NoContent() : NotFound();
|
||||
if (!resultado) return NotFound();
|
||||
await NotificarCambios(); // Notificamos después de actualizar
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut("reordenar")]
|
||||
public async Task<IActionResult> Reordenar([FromBody] List<ReordenarTitularDto> ordenes)
|
||||
{
|
||||
if (ordenes == null || !ordenes.Any())
|
||||
{
|
||||
return BadRequest("La lista de órdenes no puede estar vacía.");
|
||||
}
|
||||
var resultado = await _repositorio.ActualizarOrdenAsync(ordenes);
|
||||
return resultado ? Ok() : StatusCode(500, "Error al actualizar el orden.");
|
||||
if (!resultado) return StatusCode(500, "Error al actualizar el orden.");
|
||||
await NotificarCambios(); // Notificamos después de reordenar
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Eliminar(int id)
|
||||
{
|
||||
var resultado = await _repositorio.EliminarAsync(id);
|
||||
return resultado ? NoContent() : NotFound();
|
||||
if (!resultado) return NotFound();
|
||||
await NotificarCambios(); // Notificamos después de eliminar
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
12
backend/src/Titulares.Api/Hubs/TitularesHub.cs
Normal file
12
backend/src/Titulares.Api/Hubs/TitularesHub.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// backend/src/Titulares.Api/Hubs/TitularesHub.cs
|
||||
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Titulares.Api.Hubs;
|
||||
|
||||
// Esta clase es el punto de conexión para los clientes de SignalR.
|
||||
// No necesitamos añadirle métodos personalizados porque solo enviaremos
|
||||
// mensajes desde el servidor hacia los clientes.
|
||||
public class TitularesHub : Hub
|
||||
{
|
||||
}
|
||||
@@ -1,35 +1,34 @@
|
||||
// backend/src/Titulares.Api/Program.cs
|
||||
using Titulares.Api.Data;
|
||||
using Titulares.Api.Hubs; // Añadir este using
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// 1. Añadir servicios al contenedor.
|
||||
// ===================================
|
||||
|
||||
// Añadimos los servicios para los controladores API
|
||||
// Add services to the container.
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// Añadimos nuestro repositorio personalizado
|
||||
builder.Services.AddSingleton<TitularRepositorio>();
|
||||
|
||||
// Añadimos los servicios de autorización (necesario para app.UseAuthorization)
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// Añadimos la política de CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowReactApp", builder =>
|
||||
{
|
||||
builder.WithOrigins("http://localhost:5174")
|
||||
builder.WithOrigins("http://localhost:5173")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
// Añadimos los servicios de autorización (necesario para app.UseAuthorization)
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// 2. Construir la aplicación.
|
||||
// ==========================
|
||||
var app = builder.Build();
|
||||
@@ -43,15 +42,21 @@ if (app.Environment.IsDevelopment())
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
// COMENTAMOS LA REDIRECCIÓN HTTPS PORQUE TRABAJAMOS CON HTTP
|
||||
// app.UseHttpsRedirection();
|
||||
|
||||
// Usamos la política de CORS que definimos
|
||||
// 1. Activa el enrutamiento para que la app sepa a dónde va la petición.
|
||||
app.UseRouting();
|
||||
|
||||
// 2. APLICA LA POLÍTICA DE CORS.
|
||||
app.UseCors("AllowReactApp");
|
||||
|
||||
// Usamos la autorización
|
||||
// 3. Usamos la autorización.
|
||||
app.UseAuthorization();
|
||||
|
||||
// Mapeamos los controladores para que la API responda a las rutas
|
||||
// 4. Mapeamos los endpoints (Controladores y Hubs).
|
||||
app.MapControllers();
|
||||
app.MapHub<TitularesHub>("/titularesHub");
|
||||
|
||||
|
||||
app.Run();
|
||||
182
frontend/package-lock.json
generated
182
frontend/package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@microsoft/signalr": "^9.0.6",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/material": "^7.3.4",
|
||||
"axios": "^1.13.0",
|
||||
@@ -1217,6 +1218,19 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/signalr": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-9.0.6.tgz",
|
||||
"integrity": "sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"eventsource": "^2.0.2",
|
||||
"fetch-cookie": "^2.0.3",
|
||||
"node-fetch": "^2.6.7",
|
||||
"ws": "^7.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.4.tgz",
|
||||
@@ -2214,6 +2228,18 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
@@ -2904,6 +2930,24 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
|
||||
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -2965,6 +3009,16 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-cookie": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz",
|
||||
"integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==",
|
||||
"license": "Unlicense",
|
||||
"dependencies": {
|
||||
"set-cookie-parser": "^2.4.8",
|
||||
"tough-cookie": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -3593,6 +3647,26 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.26",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
|
||||
@@ -3805,16 +3879,33 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/lupomontero"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -3889,6 +3980,12 @@
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
@@ -4011,6 +4108,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -4158,6 +4261,27 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
@@ -4235,6 +4359,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||
@@ -4276,6 +4409,16 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
@@ -4382,6 +4525,22 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -4408,6 +4567,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@microsoft/signalr": "^9.0.6",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/material": "^7.3.4",
|
||||
"axios": "^1.13.0",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// frontend/src/components/Dashboard.tsx
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Box, Button, Typography, Stack } from '@mui/material';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { Box, Button, Typography, Stack, Chip } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import SyncIcon from '@mui/icons-material/Sync';
|
||||
|
||||
import type { Titular } from '../types';
|
||||
import * as api from '../services/apiService';
|
||||
import { useSignalR } from '../hooks/useSignalR';
|
||||
import FormularioConfiguracion from './FormularioConfiguracion';
|
||||
import TablaTitulares from './TablaTitulares';
|
||||
import AddTitularModal from './AddTitularModal';
|
||||
@@ -14,30 +16,36 @@ const Dashboard = () => {
|
||||
const [titulares, setTitulares] = useState<Titular[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const cargarTitulares = async () => {
|
||||
try {
|
||||
const data = await api.obtenerTitulares();
|
||||
setTitulares(data);
|
||||
} catch (error) {
|
||||
console.error("Error al cargar titulares:", error);
|
||||
}
|
||||
};
|
||||
// Usamos useCallback para que la función de callback no se recree en cada render,
|
||||
// evitando que el useEffect del hook se ejecute innecesariamente.
|
||||
const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => {
|
||||
console.log("Datos recibidos desde SignalR:", titularesActualizados);
|
||||
setTitulares(titularesActualizados);
|
||||
}, []); // El array vacío significa que esta función nunca cambiará
|
||||
|
||||
// Usamos nuestro hook y le pasamos el evento que nos interesa escuchar
|
||||
const { connectionStatus } = useSignalR([
|
||||
{ eventName: 'TitularesActualizados', callback: onTitularesActualizados }
|
||||
]);
|
||||
|
||||
// La carga inicial de datos sigue siendo necesaria por si el componente se monta
|
||||
// antes de que llegue la primera notificación de SignalR.
|
||||
useEffect(() => {
|
||||
cargarTitulares();
|
||||
api.obtenerTitulares()
|
||||
.then(setTitulares)
|
||||
.catch(error => console.error("Error al cargar titulares:", error));
|
||||
}, []);
|
||||
|
||||
const handleReorder = async (titularesReordenados: Titular[]) => {
|
||||
setTitulares(titularesReordenados); // Actualización optimista de la UI
|
||||
const payload = titularesReordenados.map((item, index) => ({
|
||||
id: item.id,
|
||||
nuevoOrden: index
|
||||
}));
|
||||
setTitulares(titularesReordenados);
|
||||
const payload = titularesReordenados.map((item, index) => ({ id: item.id, nuevoOrden: index }));
|
||||
try {
|
||||
await api.actualizarOrdenTitulares(payload);
|
||||
// Ya no necesitamos hacer nada más, SignalR notificará a todos los clientes.
|
||||
} catch (err) {
|
||||
console.error("Error al reordenar:", err);
|
||||
cargarTitulares(); // Revertir en caso de error
|
||||
// En caso de error, volvemos a pedir los datos para no tener un estado inconsistente.
|
||||
api.obtenerTitulares().then(setTitulares);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,7 +53,7 @@ const Dashboard = () => {
|
||||
if (window.confirm('¿Estás seguro de que quieres eliminar este titular?')) {
|
||||
try {
|
||||
await api.eliminarTitular(id);
|
||||
setTitulares(titulares.filter(t => t.id !== id)); // Actualizar UI
|
||||
// SignalR se encargará de actualizar el estado.
|
||||
} catch (err) {
|
||||
console.error("Error al eliminar:", err);
|
||||
}
|
||||
@@ -55,18 +63,33 @@ const Dashboard = () => {
|
||||
const handleAdd = async (texto: string) => {
|
||||
try {
|
||||
await api.crearTitularManual(texto);
|
||||
cargarTitulares(); // Recargar la lista para ver el nuevo titular
|
||||
// SignalR se encargará de actualizar el estado.
|
||||
} catch (err) {
|
||||
console.error("Error al añadir titular:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusChip = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'Connected':
|
||||
return <Chip label="Conectado" color="success" size="small" />;
|
||||
case 'Reconnecting':
|
||||
case 'Connecting':
|
||||
return <Chip label="Conectando..." color="warning" size="small" />;
|
||||
default:
|
||||
return <Chip label="Desconectado" color="error" size="small" />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h4" component="h1">
|
||||
Titulares Dashboard
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<Typography variant="h4" component="h1">
|
||||
Titulares Dashboard
|
||||
</Typography>
|
||||
{getStatusChip()}
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Button variant="outlined" startIcon={<SyncIcon />}>
|
||||
Generate CSV
|
||||
@@ -79,7 +102,6 @@ const Dashboard = () => {
|
||||
|
||||
<FormularioConfiguracion />
|
||||
<TablaTitulares titulares={titulares} onReorder={handleReorder} onDelete={handleDelete} />
|
||||
|
||||
<AddTitularModal open={modalOpen} onClose={() => setModalOpen(false)} onAdd={handleAdd} />
|
||||
</>
|
||||
);
|
||||
|
||||
66
frontend/src/hooks/useSignalR.ts
Normal file
66
frontend/src/hooks/useSignalR.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// frontend/src/hooks/useSignalR.ts
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import * as signalR from '@microsoft/signalr';
|
||||
|
||||
const HUB_URL = 'http://localhost:5174/titularesHub';
|
||||
|
||||
// Definimos un tipo para el estado de la conexión para más claridad
|
||||
export type ConnectionStatus = 'Connecting' | 'Connected' | 'Disconnected' | 'Reconnecting';
|
||||
|
||||
// El hook ahora acepta los listeners como argumentos
|
||||
export const useSignalR = (listeners: { eventName: string; callback: (...args: any[]) => void }[]) => {
|
||||
// Usamos useRef para mantener una única instancia de la conexión a través de los re-renders
|
||||
const connectionRef = useRef<signalR.HubConnection | null>(null);
|
||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('Disconnected');
|
||||
|
||||
useEffect(() => {
|
||||
// Si no hay conexión, la creamos.
|
||||
if (!connectionRef.current) {
|
||||
connectionRef.current = new signalR.HubConnectionBuilder()
|
||||
.withUrl(HUB_URL)
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
}
|
||||
|
||||
const connection = connectionRef.current;
|
||||
|
||||
// Registramos los listeners que nos pasaron como argumento
|
||||
listeners.forEach(listener => {
|
||||
connection.on(listener.eventName, listener.callback);
|
||||
});
|
||||
|
||||
// Manejamos los cambios de estado para feedback en la UI
|
||||
connection.onreconnecting(() => setConnectionStatus('Reconnecting'));
|
||||
connection.onreconnected(() => setConnectionStatus('Connected'));
|
||||
connection.onclose(() => setConnectionStatus('Disconnected'));
|
||||
|
||||
// Solo iniciamos la conexión si está desconectada
|
||||
if (connection.state === signalR.HubConnectionState.Disconnected) {
|
||||
setConnectionStatus('Connecting');
|
||||
connection.start()
|
||||
.then(() => {
|
||||
setConnectionStatus('Connected');
|
||||
console.log('SignalR Conectado.');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error de conexión con SignalR: ', err);
|
||||
setConnectionStatus('Disconnected');
|
||||
});
|
||||
}
|
||||
|
||||
// --- FUNCIÓN DE LIMPIEZA ---
|
||||
// Esto se ejecuta cuando el componente que usa el hook se desmonta
|
||||
return () => {
|
||||
// Quitamos los listeners para evitar fugas de memoria
|
||||
listeners.forEach(listener => {
|
||||
connection.off(listener.eventName, listener.callback);
|
||||
});
|
||||
|
||||
// Opcional: podría detener la conexión aquí si solo un componente la usa.
|
||||
// connection.stop();
|
||||
};
|
||||
}, [listeners]); // El efecto se re-ejecutará si la lista de listeners cambia
|
||||
|
||||
return { connectionStatus };
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
import axios from 'axios';
|
||||
import type { Titular } from '../types';
|
||||
|
||||
const API_URL = 'https://localhost:5174/api';
|
||||
const API_URL = 'http://localhost:5174/api';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_URL,
|
||||
|
||||
Reference in New Issue
Block a user