diff --git a/.gitignore b/.gitignore index ef96c12..4f873f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,17 @@ -# Backend -backend/bin/ -backend/obj/ -backend/[Bb]in/ -backend/[Oo]bj/ -backend/appsettings.Development.json +# Ignore Visual Studio Code settings +.vscode/ -# Frontend +# Ignore common OS-generated files +.DS_Store +*.swp + +# Ignore Frontend dependencies and build output frontend/node_modules/ frontend/dist/ frontend/.env.local -frontend/.DS_Store -# IDEs -.vscode/ -.idea/ \ No newline at end of file +# Ignore .NET build artifacts and user-specific files +**/[Bb]in/ +**/[Oo]bj/ +*.user +*.suo \ No newline at end of file diff --git a/TitularesApp.sln b/TitularesApp.sln new file mode 100644 index 0000000..161c9ea --- /dev/null +++ b/TitularesApp.sln @@ -0,0 +1,32 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "backend", "backend", "{1AE8ACA6-933B-BF2A-3671-3E2EAC007D16}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0F9113EE-888A-26D2-68B0-4A7D0A2A8745}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Titulares.Api", "backend\src\Titulares.Api\Titulares.Api.csproj", "{5F7CE525-ABE6-287E-9D86-8E30836C37A7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5F7CE525-ABE6-287E-9D86-8E30836C37A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F7CE525-ABE6-287E-9D86-8E30836C37A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F7CE525-ABE6-287E-9D86-8E30836C37A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F7CE525-ABE6-287E-9D86-8E30836C37A7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0F9113EE-888A-26D2-68B0-4A7D0A2A8745} = {1AE8ACA6-933B-BF2A-3671-3E2EAC007D16} + {5F7CE525-ABE6-287E-9D86-8E30836C37A7} = {0F9113EE-888A-26D2-68B0-4A7D0A2A8745} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5B89267C-BBAA-48DE-B03A-C2E83EC18490} + EndGlobalSection +EndGlobal diff --git a/backend/src/Titulares.Api/Controllers/TitularesController.cs b/backend/src/Titulares.Api/Controllers/TitularesController.cs new file mode 100644 index 0000000..6aea4ca --- /dev/null +++ b/backend/src/Titulares.Api/Controllers/TitularesController.cs @@ -0,0 +1,66 @@ +// backend/src/Titulares.Api/Controllers/TitularesController.cs + +using Microsoft.AspNetCore.Mvc; +using Titulares.Api.Data; +using Titulares.Api.Models; + +namespace Titulares.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class TitularesController : ControllerBase +{ + private readonly TitularRepositorio _repositorio; + + public TitularesController(TitularRepositorio repositorio) + { + _repositorio = repositorio; + } + + [HttpGet] + public async Task ObtenerTodos() + { + var titulares = await _repositorio.ObtenerTodosAsync(); + return Ok(titulares); + } + + [HttpPost] + public async Task CrearManual([FromBody] CrearTitularDto titularDto) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + var nuevoId = await _repositorio.CrearManualAsync(titularDto); + return CreatedAtAction(nameof(ObtenerTodos), new { id = nuevoId }, null); + } + + [HttpPut("{id}")] + public async Task Actualizar(int id, [FromBody] ActualizarTitularDto titularDto) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + var resultado = await _repositorio.ActualizarTextoAsync(id, titularDto); + return resultado ? NoContent() : NotFound(); + } + + [HttpPut("reordenar")] + public async Task Reordenar([FromBody] List 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."); + } + + [HttpDelete("{id}")] + public async Task Eliminar(int id) + { + var resultado = await _repositorio.EliminarAsync(id); + return resultado ? NoContent() : NotFound(); + } +} \ No newline at end of file diff --git a/backend/src/Titulares.Api/Data/TitularRepositorio.cs b/backend/src/Titulares.Api/Data/TitularRepositorio.cs new file mode 100644 index 0000000..483e5cc --- /dev/null +++ b/backend/src/Titulares.Api/Data/TitularRepositorio.cs @@ -0,0 +1,85 @@ +// backend/src/Titulares.Api/Data/TitularRepositorio.cs + +using Dapper; +using Microsoft.Data.SqlClient; +using Titulares.Api.Models; + +namespace Titulares.Api.Data; + +public class TitularRepositorio +{ + private readonly string _connectionString; + + public TitularRepositorio(IConfiguration configuration) + { + _connectionString = configuration.GetConnectionString("DefaultConnection")!; + } + + private SqlConnection CreateConnection() => new SqlConnection(_connectionString); + + public async Task> ObtenerTodosAsync() + { + using var connection = CreateConnection(); + // Siempre los obtenemos en el orden correcto + return await connection.QueryAsync("SELECT * FROM Titulares ORDER BY OrdenVisual"); + } + + public async Task CrearManualAsync(CrearTitularDto titularDto) + { + using var connection = CreateConnection(); + // Obtenemos el OrdenVisual más bajo (el primero) y restamos 1 para ponerlo al inicio. + var minOrdenVisual = await connection.ExecuteScalarAsync("SELECT MIN(OrdenVisual) FROM Titulares") ?? 1; + var nuevoOrden = minOrdenVisual - 1; + + var sql = @" + INSERT INTO Titulares (Texto, ModificadoPorUsuario, EsEntradaManual, OrdenVisual, Tipo, Fuente) + VALUES (@Texto, 0, 1, @OrdenVisual, 'Manual', 'Usuario'); + SELECT CAST(SCOPE_IDENTITY() as int); + "; + return await connection.ExecuteScalarAsync(sql, new { titularDto.Texto, OrdenVisual = nuevoOrden }); + } + + public async Task ActualizarTextoAsync(int id, ActualizarTitularDto titularDto) + { + using var connection = CreateConnection(); + var sql = @" + UPDATE Titulares + SET Texto = @Texto, ModificadoPorUsuario = 1, Tipo = 'Edited' + WHERE Id = @Id; + "; + var affectedRows = await connection.ExecuteAsync(sql, new { titularDto.Texto, Id = id }); + return affectedRows > 0; + } + + public async Task EliminarAsync(int id) + { + using var connection = CreateConnection(); + var affectedRows = await connection.ExecuteAsync("DELETE FROM Titulares WHERE Id = @Id", new { Id = id }); + return affectedRows > 0; + } + + public async Task ActualizarOrdenAsync(List ordenes) + { + using var connection = CreateConnection(); + await connection.OpenAsync(); + using var transaction = connection.BeginTransaction(); + + try + { + foreach (var item in ordenes) + { + await connection.ExecuteAsync( + "UPDATE Titulares SET OrdenVisual = @NuevoOrden WHERE Id = @Id", + item, + transaction: transaction); + } + transaction.Commit(); + return true; + } + catch + { + transaction.Rollback(); + return false; + } + } +} \ No newline at end of file diff --git a/backend/src/Titulares.Api/Models/Titular.cs b/backend/src/Titulares.Api/Models/Titular.cs new file mode 100644 index 0000000..8d42d90 --- /dev/null +++ b/backend/src/Titulares.Api/Models/Titular.cs @@ -0,0 +1,36 @@ +// backend/src/Titulares.Api/Models/Titular.cs + +namespace Titulares.Api.Models; + +public class Titular +{ + public int Id { get; set; } + public required string Texto { get; set; } + public string? UrlFuente { get; set; } + public bool ModificadoPorUsuario { get; set; } + public bool EsEntradaManual { get; set; } + public int OrdenVisual { get; set; } + public DateTime FechaCreacion { get; set; } + public string? Encabezado { get; set; } + public string? Tipo { get; set; } + public string? Fuente { get; set; } +} + +// DTO (Data Transfer Object) para la creación de un titular manual +public class CrearTitularDto +{ + public required string Texto { get; set; } +} + +// DTO para la actualización de un titular +public class ActualizarTitularDto +{ + public required string Texto { get; set; } +} + +// DTO para el reordenamiento +public class ReordenarTitularDto +{ + public int Id { get; set; } + public int NuevoOrden { get; set; } +} \ No newline at end of file diff --git a/backend/src/Titulares.Api/Program.cs b/backend/src/Titulares.Api/Program.cs new file mode 100644 index 0000000..1edd4d2 --- /dev/null +++ b/backend/src/Titulares.Api/Program.cs @@ -0,0 +1,57 @@ +// backend/src/Titulares.Api/Program.cs +using Titulares.Api.Data; + +var builder = WebApplication.CreateBuilder(args); + +// 1. Añadir servicios al contenedor. +// =================================== + +// Añadimos los servicios para los controladores API +builder.Services.AddControllers(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// Añadimos nuestro repositorio personalizado +builder.Services.AddSingleton(); + +// 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:5173") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + }); +}); + +// 2. Construir la aplicación. +// ========================== +var app = builder.Build(); + +// 3. Configurar el pipeline de peticiones HTTP. +// ============================================ + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +// Usamos la política de CORS que definimos +app.UseCors("AllowReactApp"); + +// Usamos la autorización +app.UseAuthorization(); + +// Mapeamos los controladores para que la API responda a las rutas +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/backend/src/Titulares.Api/Properties/launchSettings.json b/backend/src/Titulares.Api/Properties/launchSettings.json new file mode 100644 index 0000000..f1ac1eb --- /dev/null +++ b/backend/src/Titulares.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5173", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7000;http://localhost:5173", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/src/Titulares.Api/Titulares.Api.csproj b/backend/src/Titulares.Api/Titulares.Api.csproj new file mode 100644 index 0000000..d4d48a8 --- /dev/null +++ b/backend/src/Titulares.Api/Titulares.Api.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + diff --git a/backend/src/Titulares.Api/Titulares.Api.http b/backend/src/Titulares.Api/Titulares.Api.http new file mode 100644 index 0000000..7a45bc1 --- /dev/null +++ b/backend/src/Titulares.Api/Titulares.Api.http @@ -0,0 +1,6 @@ +@Titulares.Api_HostAddress = http://localhost:5098 + +GET {{Titulares.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/backend/src/Titulares.Api/appsettings.Development.json b/backend/src/Titulares.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/backend/src/Titulares.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/backend/src/Titulares.Api/appsettings.json b/backend/src/Titulares.Api/appsettings.json new file mode 100644 index 0000000..6de4d08 --- /dev/null +++ b/backend/src/Titulares.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=TECNICA3;Database=TitularesDB;User Id=titularesApi;Password=PTP847Titulares;Trusted_Connection=True;TrustServerCertificate=True;" + } +} \ No newline at end of file