From 09a1523e9d60b2ec93008c9b646868f1d9a81c4f Mon Sep 17 00:00:00 2001 From: adalberto Date: Wed, 31 Dec 2025 13:36:41 -0600 Subject: [PATCH] Version Stable 001 --- .../Components/MenuViewComponent.cs | 6 +- .../Controllers/AntecedentesController.cs | 2 + .../Controllers/AsistenciaController.cs | 2 + .../Controllers/CargoColaboradorController.cs | 105 ++++++ .../ColaboradorAsistenciaController.cs | 86 ++++- .../Controllers/ColaboradorController.cs | 14 +- .../Controllers/ConfiguracionController.cs | 2 +- .../Controllers/ExpedienteController.cs | 2 + .../Controllers/HomeController.cs | 2 + .../Controllers/ModuloController.cs | 105 ++++++ .../Controllers/PermisoController.cs | 21 +- .../Controllers/RolController.cs | 2 +- .../Controllers/UsuarioController.cs | 2 +- .../Data/ApplicationDbContext.cs | 12 + .../Filters/DynamicAuthorizationFilter.cs | 61 ++++ .../Migrations/Scripts/MigrateCargos.sql | 34 ++ .../Migrations/Scripts/MigrateModules.sql | 38 ++ .../Scripts/UpdateBuscarPersonas.sql | 298 ++++++++++++++++ foundation_system/Models/CargoColaborador.cs | 28 ++ foundation_system/Models/Colaborador.cs | 8 +- foundation_system/Models/Modulo.cs | 33 ++ foundation_system/Models/Permiso.cs | 8 +- .../Models/ViewModels/ColaboradorViewModel.cs | 2 +- foundation_system/Program.cs | 1 + .../Views/CargoColaborador/Create.cshtml | 54 +++ .../Views/CargoColaborador/Edit.cshtml | 56 +++ .../Views/CargoColaborador/Index.cshtml | 84 +++++ .../Views/Colaborador/Create.cshtml | 8 +- .../Views/Colaborador/Edit.cshtml | 8 +- .../Views/Colaborador/Index.cshtml | 2 +- .../ImprimirAsistencia.cshtml | 98 ++++++ .../Views/ColaboradorAsistencia/Index.cshtml | 50 ++- .../ReporteAsistencia.cshtml | 327 ++++++++++++++++++ foundation_system/Views/Modulo/Create.cshtml | 73 ++++ foundation_system/Views/Modulo/Edit.cshtml | 75 ++++ foundation_system/Views/Modulo/Index.cshtml | 86 +++++ foundation_system/Views/Permiso/Create.cshtml | 9 +- foundation_system/Views/Permiso/Edit.cshtml | 9 +- foundation_system/Views/Permiso/Index.cshtml | 9 +- .../Shared/Components/Menu/Default.cshtml | 8 +- foundation_system/wwwroot/css/site.css | 280 +++++++-------- 41 files changed, 1929 insertions(+), 181 deletions(-) create mode 100644 foundation_system/Controllers/CargoColaboradorController.cs create mode 100644 foundation_system/Controllers/ModuloController.cs create mode 100644 foundation_system/Filters/DynamicAuthorizationFilter.cs create mode 100644 foundation_system/Migrations/Scripts/MigrateCargos.sql create mode 100644 foundation_system/Migrations/Scripts/MigrateModules.sql create mode 100644 foundation_system/Migrations/Scripts/UpdateBuscarPersonas.sql create mode 100644 foundation_system/Models/CargoColaborador.cs create mode 100644 foundation_system/Models/Modulo.cs create mode 100644 foundation_system/Views/CargoColaborador/Create.cshtml create mode 100644 foundation_system/Views/CargoColaborador/Edit.cshtml create mode 100644 foundation_system/Views/CargoColaborador/Index.cshtml create mode 100644 foundation_system/Views/ColaboradorAsistencia/ImprimirAsistencia.cshtml create mode 100644 foundation_system/Views/ColaboradorAsistencia/ReporteAsistencia.cshtml create mode 100644 foundation_system/Views/Modulo/Create.cshtml create mode 100644 foundation_system/Views/Modulo/Edit.cshtml create mode 100644 foundation_system/Views/Modulo/Index.cshtml diff --git a/foundation_system/Components/MenuViewComponent.cs b/foundation_system/Components/MenuViewComponent.cs index 4f8e706..c90c5c8 100644 --- a/foundation_system/Components/MenuViewComponent.cs +++ b/foundation_system/Components/MenuViewComponent.cs @@ -29,8 +29,9 @@ public class MenuViewComponent : ViewComponent if (isRoot) { menuItems = await _context.Permisos + .Include(p => p.Modulo) .Where(p => p.EsMenu) - .OrderBy(p => p.Modulo) + .OrderBy(p => p.Modulo!.Orden) .ThenBy(p => p.Orden) .ToListAsync(); } @@ -40,8 +41,9 @@ public class MenuViewComponent : ViewComponent .Where(ru => ru.UsuarioId == userId) .Join(_context.RolesPermisos, ru => ru.RolId, rp => rp.RolId, (ru, rp) => rp) .Join(_context.Permisos, rp => rp.PermisoId, p => p.Id, (rp, p) => p) + .Include(p => p.Modulo) .Where(p => p.EsMenu) - .OrderBy(p => p.Modulo) + .OrderBy(p => p.Modulo!.Orden) .ThenBy(p => p.Orden) .Distinct() .ToListAsync(); diff --git a/foundation_system/Controllers/AntecedentesController.cs b/foundation_system/Controllers/AntecedentesController.cs index c77ecac..a39b679 100644 --- a/foundation_system/Controllers/AntecedentesController.cs +++ b/foundation_system/Controllers/AntecedentesController.cs @@ -2,11 +2,13 @@ using foundation_system.Data; using foundation_system.Models; using foundation_system.Models.ViewModels; using foundation_system.Services; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace foundation_system.Controllers; +[Authorize] public class AntecedentesController : Controller { private readonly ApplicationDbContext _context; diff --git a/foundation_system/Controllers/AsistenciaController.cs b/foundation_system/Controllers/AsistenciaController.cs index 39bb2bd..3c2155b 100644 --- a/foundation_system/Controllers/AsistenciaController.cs +++ b/foundation_system/Controllers/AsistenciaController.cs @@ -3,9 +3,11 @@ using Microsoft.EntityFrameworkCore; using foundation_system.Data; using foundation_system.Models; using foundation_system.Models.ViewModels; +using Microsoft.AspNetCore.Authorization; namespace foundation_system.Controllers; +[Authorize] public class AsistenciaController : Controller { private readonly ApplicationDbContext _context; diff --git a/foundation_system/Controllers/CargoColaboradorController.cs b/foundation_system/Controllers/CargoColaboradorController.cs new file mode 100644 index 0000000..0882b46 --- /dev/null +++ b/foundation_system/Controllers/CargoColaboradorController.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using foundation_system.Data; +using foundation_system.Models; + +namespace foundation_system.Controllers; + +[Authorize] +public class CargoColaboradorController : Controller +{ + private readonly ApplicationDbContext _context; + + public CargoColaboradorController(ApplicationDbContext context) + { + _context = context; + } + + // GET: CargoColaborador + public async Task Index() + { + return View(await _context.CargosColaboradores.OrderBy(c => c.Nombre).ToListAsync()); + } + + // GET: CargoColaborador/Create + public IActionResult Create() + { + return View(); + } + + // POST: CargoColaborador/Create + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create([Bind("Nombre,Descripcion,Activo")] CargoColaborador cargo) + { + if (ModelState.IsValid) + { + cargo.CreadoEn = DateTime.UtcNow; + _context.Add(cargo); + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + return View(cargo); + } + + // GET: CargoColaborador/Edit/5 + public async Task Edit(long? id) + { + if (id == null) return NotFound(); + + var cargo = await _context.CargosColaboradores.FindAsync(id); + if (cargo == null) return NotFound(); + return View(cargo); + } + + // POST: CargoColaborador/Edit/5 + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(long id, [Bind("Id,Nombre,Descripcion,Activo,CreadoEn")] CargoColaborador cargo) + { + if (id != cargo.Id) return NotFound(); + + if (ModelState.IsValid) + { + try + { + _context.Update(cargo); + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!CargoExists(cargo.Id)) return NotFound(); + else throw; + } + return RedirectToAction(nameof(Index)); + } + return View(cargo); + } + + // POST: CargoColaborador/Delete/5 + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Delete(long id) + { + var cargo = await _context.CargosColaboradores.FindAsync(id); + if (cargo != null) + { + var isUsed = await _context.Colaboradores.AnyAsync(c => c.CargoId == id); + if (isUsed) + { + TempData["ErrorMessage"] = "No se puede eliminar porque hay colaboradores asignados a este cargo."; + return RedirectToAction(nameof(Index)); + } + + _context.CargosColaboradores.Remove(cargo); + await _context.SaveChangesAsync(); + } + return RedirectToAction(nameof(Index)); + } + + private bool CargoExists(long id) + { + return _context.CargosColaboradores.Any(e => e.Id == id); + } +} diff --git a/foundation_system/Controllers/ColaboradorAsistenciaController.cs b/foundation_system/Controllers/ColaboradorAsistenciaController.cs index adb5121..416a404 100644 --- a/foundation_system/Controllers/ColaboradorAsistenciaController.cs +++ b/foundation_system/Controllers/ColaboradorAsistenciaController.cs @@ -20,10 +20,15 @@ public class ColaboradorAsistenciaController : Controller public async Task Index(DateOnly? fecha) { var selectedDate = fecha ?? DateOnly.FromDateTime(DateTime.Today); + if (fecha.HasValue) + { + TempData["InfoMessage"] = $"Mostrando asistencia del {selectedDate:dd/MM/yyyy}"; + } ViewBag.SelectedDate = selectedDate; var colaboradores = await _context.Colaboradores .Include(c => c.Persona) + .Include(c => c.Cargo) .Where(c => c.Activo) .OrderBy(c => c.Persona.Apellidos) .ToListAsync(); @@ -63,7 +68,86 @@ public class ColaboradorAsistenciaController : Controller asistencia.Observaciones = observaciones; } - await _context.SaveChangesAsync(); + await _context.SaveChangesAsync(); return Json(new { success = true }); } + + // GET: ColaboradorAsistencia/ImprimirAsistencia + public async Task ImprimirAsistencia(DateOnly? fecha) + { + var selectedDate = fecha ?? DateOnly.FromDateTime(DateTime.Today); + ViewBag.SelectedDate = selectedDate; + + var colaboradores = await _context.Colaboradores + .Include(c => c.Persona) + .Include(c => c.Cargo) + .Where(c => c.Activo) + .OrderBy(c => c.Persona.Apellidos) + .ToListAsync(); + + var asistencias = await _context.AsistenciasColaboradores + .Where(a => a.Fecha == selectedDate) + .ToDictionaryAsync(a => a.ColaboradorId); + + ViewBag.Asistencias = asistencias; + + return View(colaboradores); + } + + // GET: ColaboradorAsistencia/ReporteAsistencia + public async Task ReporteAsistencia(long? colaboradorId, DateOnly? inicio, DateOnly? fin) + { + var startDate = inicio ?? DateOnly.FromDateTime(DateTime.Today.AddDays(-30)); + var endDate = fin ?? DateOnly.FromDateTime(DateTime.Today); + + ViewBag.Inicio = startDate; + ViewBag.Fin = endDate; + ViewBag.ColaboradorId = colaboradorId; + + ViewBag.Colaboradores = await _context.Colaboradores + .Include(c => c.Persona) + .Include(c => c.Cargo) + .Where(c => c.Activo) + .OrderBy(c => c.Persona.Apellidos) + .ToListAsync(); + + if (colaboradorId.HasValue) + { + var asistencias = await _context.AsistenciasColaboradores + .Include(a => a.Colaborador) + .ThenInclude(c => c.Persona) + .Where(a => a.ColaboradorId == colaboradorId && a.Fecha >= startDate && a.Fecha <= endDate) + .OrderByDescending(a => a.Fecha) + .ToListAsync(); + + return View(asistencias); + } + + return View(new List()); + } + + // GET: ColaboradorAsistencia/SearchColaboradores + [HttpGet] + public async Task SearchColaboradores(string term) + { + if (string.IsNullOrWhiteSpace(term) || term.Length < 2) + { + return Json(new List()); + } + + var results = await _context.Database + .SqlQueryRaw( + "SELECT id, text, score FROM buscar_personas_v2(@p0, 'COLABORADOR')", + term) + .ToListAsync(); + + return Json(results); + } +} + +public class SearchResult +{ + public long Id { get; set; } + public string Text { get; set; } = string.Empty; + public double Score { get; set; } } diff --git a/foundation_system/Controllers/ColaboradorController.cs b/foundation_system/Controllers/ColaboradorController.cs index 13c9911..20d27db 100644 --- a/foundation_system/Controllers/ColaboradorController.cs +++ b/foundation_system/Controllers/ColaboradorController.cs @@ -22,14 +22,16 @@ public class ColaboradorController : Controller { var colaboradores = await _context.Colaboradores .Include(c => c.Persona) + .Include(c => c.Cargo) .OrderBy(c => c.Persona.Apellidos) .ToListAsync(); return View(colaboradores); } // GET: Colaborador/Create - public IActionResult Create() + public async Task Create() { + ViewBag.Cargos = await _context.CargosColaboradores.Where(c => c.Activo).OrderBy(c => c.Nombre).ToListAsync(); return View(new ColaboradorViewModel()); } @@ -63,7 +65,7 @@ public class ColaboradorController : Controller var colaborador = new Colaborador { PersonaId = persona.Id, - Cargo = model.Cargo, + CargoId = model.CargoId, TipoColaborador = model.TipoColaborador, FechaIngreso = model.FechaIngreso, HorarioEntrada = model.HorarioEntrada, @@ -84,6 +86,7 @@ public class ColaboradorController : Controller ModelState.AddModelError("", "Ocurrió un error al guardar el colaborador."); } } + ViewBag.Cargos = await _context.CargosColaboradores.Where(c => c.Activo).OrderBy(c => c.Nombre).ToListAsync(); return View(model); } @@ -94,6 +97,7 @@ public class ColaboradorController : Controller var colaborador = await _context.Colaboradores .Include(c => c.Persona) + .Include(c => c.Cargo) .FirstOrDefaultAsync(c => c.Id == id); if (colaborador == null) return NotFound(); @@ -111,7 +115,7 @@ public class ColaboradorController : Controller Email = colaborador.Persona.Email, Telefono = colaborador.Persona.Telefono, Direccion = colaborador.Persona.Direccion, - Cargo = colaborador.Cargo, + CargoId = colaborador.CargoId, TipoColaborador = colaborador.TipoColaborador, FechaIngreso = colaborador.FechaIngreso, HorarioEntrada = colaborador.HorarioEntrada, @@ -119,6 +123,7 @@ public class ColaboradorController : Controller Activo = colaborador.Activo }; + ViewBag.Cargos = await _context.CargosColaboradores.Where(c => c.Activo).OrderBy(c => c.Nombre).ToListAsync(); return View(model); } @@ -152,7 +157,7 @@ public class ColaboradorController : Controller colaborador.Persona.Direccion = model.Direccion; // Update Colaborador - colaborador.Cargo = model.Cargo; + colaborador.CargoId = model.CargoId; colaborador.TipoColaborador = model.TipoColaborador; colaborador.FechaIngreso = model.FechaIngreso; colaborador.HorarioEntrada = model.HorarioEntrada; @@ -172,6 +177,7 @@ public class ColaboradorController : Controller ModelState.AddModelError("", "Ocurrió un error al actualizar el colaborador."); } } + ViewBag.Cargos = await _context.CargosColaboradores.Where(c => c.Activo).OrderBy(c => c.Nombre).ToListAsync(); return View(model); } diff --git a/foundation_system/Controllers/ConfiguracionController.cs b/foundation_system/Controllers/ConfiguracionController.cs index 8c2d1e0..9913b35 100644 --- a/foundation_system/Controllers/ConfiguracionController.cs +++ b/foundation_system/Controllers/ConfiguracionController.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Authorization; namespace foundation_system.Controllers; -[Authorize(Roles = "ROOT,SUPERADMIN")] +[Authorize] public class ConfiguracionController : Controller { private readonly ApplicationDbContext _context; diff --git a/foundation_system/Controllers/ExpedienteController.cs b/foundation_system/Controllers/ExpedienteController.cs index e38924e..cb71b4e 100644 --- a/foundation_system/Controllers/ExpedienteController.cs +++ b/foundation_system/Controllers/ExpedienteController.cs @@ -4,9 +4,11 @@ using foundation_system.Data; using foundation_system.Models; using foundation_system.Models.ViewModels; using foundation_system.Services; +using Microsoft.AspNetCore.Authorization; namespace foundation_system.Controllers; +[Authorize] public class ExpedienteController : Controller { private readonly ApplicationDbContext _context; diff --git a/foundation_system/Controllers/HomeController.cs b/foundation_system/Controllers/HomeController.cs index ca6e8da..65e83ac 100644 --- a/foundation_system/Controllers/HomeController.cs +++ b/foundation_system/Controllers/HomeController.cs @@ -3,9 +3,11 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using foundation_system.Data; using foundation_system.Models; +using Microsoft.AspNetCore.Authorization; namespace foundation_system.Controllers; +[Authorize] public class HomeController : Controller { private readonly ILogger _logger; diff --git a/foundation_system/Controllers/ModuloController.cs b/foundation_system/Controllers/ModuloController.cs new file mode 100644 index 0000000..38d8fc5 --- /dev/null +++ b/foundation_system/Controllers/ModuloController.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using foundation_system.Data; +using foundation_system.Models; + +namespace foundation_system.Controllers; + +[Authorize] +public class ModuloController : Controller +{ + private readonly ApplicationDbContext _context; + + public ModuloController(ApplicationDbContext context) + { + _context = context; + } + + // GET: Modulo + public async Task Index() + { + return View(await _context.Modulos.OrderBy(m => m.Orden).ToListAsync()); + } + + // GET: Modulo/Create + public IActionResult Create() + { + return View(); + } + + // POST: Modulo/Create + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create([Bind("Nombre,Icono,Orden,Activo")] Modulo modulo) + { + if (ModelState.IsValid) + { + modulo.CreadoEn = DateTime.UtcNow; + _context.Add(modulo); + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + return View(modulo); + } + + // GET: Modulo/Edit/5 + public async Task Edit(int? id) + { + if (id == null) return NotFound(); + + var modulo = await _context.Modulos.FindAsync(id); + if (modulo == null) return NotFound(); + return View(modulo); + } + + // POST: Modulo/Edit/5 + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(int id, [Bind("Id,Nombre,Icono,Orden,Activo")] Modulo modulo) + { + if (id != modulo.Id) return NotFound(); + + if (ModelState.IsValid) + { + try + { + _context.Update(modulo); + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!ModuloExists(modulo.Id)) return NotFound(); + else throw; + } + return RedirectToAction(nameof(Index)); + } + return View(modulo); + } + + // POST: Modulo/Delete/5 + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Delete(int id) + { + var modulo = await _context.Modulos.FindAsync(id); + if (modulo != null) + { + var isUsed = await _context.Permisos.AnyAsync(p => p.ModuloId == id); + if (isUsed) + { + TempData["ErrorMessage"] = "No se puede eliminar porque tiene permisos asociados."; + return RedirectToAction(nameof(Index)); + } + + _context.Modulos.Remove(modulo); + await _context.SaveChangesAsync(); + } + return RedirectToAction(nameof(Index)); + } + + private bool ModuloExists(int id) + { + return _context.Modulos.Any(e => e.Id == id); + } +} diff --git a/foundation_system/Controllers/PermisoController.cs b/foundation_system/Controllers/PermisoController.cs index b86ac16..52f1ffc 100644 --- a/foundation_system/Controllers/PermisoController.cs +++ b/foundation_system/Controllers/PermisoController.cs @@ -6,7 +6,7 @@ using foundation_system.Models; namespace foundation_system.Controllers; -[Authorize(Roles = "ROOT")] +[Authorize] public class PermisoController : Controller { private readonly ApplicationDbContext _context; @@ -19,25 +19,32 @@ public class PermisoController : Controller // GET: Permiso public async Task Index() { - return View(await _context.Permisos.OrderBy(p => p.Modulo).ThenBy(p => p.Orden).ToListAsync()); + var permisos = await _context.Permisos + .Include(p => p.Modulo) + .OrderBy(p => p.Modulo!.Orden) + .ThenBy(p => p.Orden) + .ToListAsync(); + return View(permisos); } // GET: Permiso/Create - public IActionResult Create() + public async Task Create() { + ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync(); return View(); } // POST: Permiso/Create [HttpPost] [ValidateAntiForgeryToken] - public async Task Create([Bind("Modulo,Codigo,Nombre,Descripcion,Url,Icono,Orden,EsMenu")] Permiso permiso) + public async Task Create([Bind("ModuloId,Codigo,Nombre,Descripcion,Url,Icono,Orden,EsMenu")] Permiso permiso) { if (ModelState.IsValid) { if (await _context.Permisos.AnyAsync(p => p.Codigo == permiso.Codigo)) { ModelState.AddModelError("Codigo", "El código ya existe."); + ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync(); return View(permiso); } @@ -46,6 +53,7 @@ public class PermisoController : Controller await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } + ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync(); return View(permiso); } @@ -56,13 +64,15 @@ public class PermisoController : Controller var permiso = await _context.Permisos.FindAsync(id); if (permiso == null) return NotFound(); + + ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync(); return View(permiso); } // POST: Permiso/Edit/5 [HttpPost] [ValidateAntiForgeryToken] - public async Task Edit(int id, [Bind("Id,Modulo,Codigo,Nombre,Descripcion,Url,Icono,Orden,EsMenu")] Permiso permiso) + public async Task Edit(int id, [Bind("Id,ModuloId,Codigo,Nombre,Descripcion,Url,Icono,Orden,EsMenu")] Permiso permiso) { if (id != permiso.Id) return NotFound(); @@ -80,6 +90,7 @@ public class PermisoController : Controller } return RedirectToAction(nameof(Index)); } + ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync(); return View(permiso); } diff --git a/foundation_system/Controllers/RolController.cs b/foundation_system/Controllers/RolController.cs index 2c37d86..1913263 100644 --- a/foundation_system/Controllers/RolController.cs +++ b/foundation_system/Controllers/RolController.cs @@ -6,7 +6,7 @@ using foundation_system.Models; namespace foundation_system.Controllers; -[Authorize(Roles = "ROOT")] +[Authorize] public class RolController : Controller { private readonly ApplicationDbContext _context; diff --git a/foundation_system/Controllers/UsuarioController.cs b/foundation_system/Controllers/UsuarioController.cs index c713098..5bd754e 100644 --- a/foundation_system/Controllers/UsuarioController.cs +++ b/foundation_system/Controllers/UsuarioController.cs @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Authorization; namespace foundation_system.Controllers; -[Authorize(Roles = "ROOT,SUPERADMIN")] +[Authorize] public class UsuarioController : Controller { private readonly ApplicationDbContext _context; diff --git a/foundation_system/Data/ApplicationDbContext.cs b/foundation_system/Data/ApplicationDbContext.cs index 6f0fc73..9de8244 100644 --- a/foundation_system/Data/ApplicationDbContext.cs +++ b/foundation_system/Data/ApplicationDbContext.cs @@ -15,8 +15,10 @@ public class ApplicationDbContext : DbContext public DbSet RolesSistema { get; set; } public DbSet RolesUsuario { get; set; } public DbSet Permisos { get; set; } + public DbSet Modulos { get; set; } public DbSet RolesPermisos { get; set; } public DbSet Colaboradores { get; set; } + public DbSet CargosColaboradores { get; set; } public DbSet AsistenciasColaboradores { get; set; } public DbSet Ninos { get; set; } public DbSet Asistencias { get; set; } @@ -56,12 +58,22 @@ public class ApplicationDbContext : DbContext .HasOne(rp => rp.Permiso) .WithMany() .HasForeignKey(rp => rp.PermisoId); + + modelBuilder.Entity() + .HasOne(p => p.Modulo) + .WithMany(m => m.Permisos) + .HasForeignKey(p => p.ModuloId); modelBuilder.Entity() .HasOne(u => u.Persona) .WithMany() .HasForeignKey(u => u.PersonaId); + modelBuilder.Entity() + .HasOne(c => c.Cargo) + .WithMany(cc => cc.Colaboradores) + .HasForeignKey(c => c.CargoId); + // Global configuration: Convert all dates to UTC when saving foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { diff --git a/foundation_system/Filters/DynamicAuthorizationFilter.cs b/foundation_system/Filters/DynamicAuthorizationFilter.cs new file mode 100644 index 0000000..9423c23 --- /dev/null +++ b/foundation_system/Filters/DynamicAuthorizationFilter.cs @@ -0,0 +1,61 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace foundation_system.Filters; + +public class DynamicAuthorizationFilter : IAsyncAuthorizationFilter +{ + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + // Skip if user is not authenticated + if (context.HttpContext.User.Identity?.IsAuthenticated != true) + { + return; + } + + // Get the controller action descriptor + if (context.ActionDescriptor is not ControllerActionDescriptor descriptor) + { + return; + } + + // Allow access to Account and Home controllers by default for authenticated users + var controllerName = descriptor.ControllerName; + if (controllerName.Equals("Account", StringComparison.OrdinalIgnoreCase) || + controllerName.Equals("Home", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + // Check for AllowAnonymous attribute + if (descriptor.MethodInfo.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any() || + descriptor.ControllerTypeInfo.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any()) + { + return; + } + + var user = context.HttpContext.User; + + // ROOT role always has access + if (user.IsInRole("ROOT")) + { + return; + } + + // Check if user has permission for this controller + // The permission code is expected to match the Controller Name (e.g., "Usuario", "Rol", "Colaborador") + // In AccountController, we added claims of type "Permission" with the permission code + var hasPermission = user.HasClaim(c => c.Type == "Permission" && + c.Value.Equals(controllerName, StringComparison.OrdinalIgnoreCase)); + + if (!hasPermission) + { + context.Result = new ForbidResult(); + } + + await Task.CompletedTask; + } +} diff --git a/foundation_system/Migrations/Scripts/MigrateCargos.sql b/foundation_system/Migrations/Scripts/MigrateCargos.sql new file mode 100644 index 0000000..119ea47 --- /dev/null +++ b/foundation_system/Migrations/Scripts/MigrateCargos.sql @@ -0,0 +1,34 @@ +-- 1. Create cargos_colaboradores table +CREATE TABLE IF NOT EXISTS public.cargos_colaboradores +( + id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ), + nombre character varying(100) COLLATE pg_catalog."default" NOT NULL, + descripcion text COLLATE pg_catalog."default", + activo boolean NOT NULL DEFAULT true, + creado_en timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT cargos_colaboradores_pkey PRIMARY KEY (id) +); + +-- 2. Insert unique cargos from colaboradores into cargos_colaboradores +INSERT INTO public.cargos_colaboradores (nombre) +SELECT DISTINCT cargo FROM public.colaboradores +WHERE cargo IS NOT NULL AND cargo <> ''; + +-- 3. Add cargo_id column to colaboradores +ALTER TABLE public.colaboradores ADD COLUMN IF NOT EXISTS cargo_id bigint; + +-- 4. Update colaboradores.cargo_id based on matching names +UPDATE public.colaboradores c +SET cargo_id = cc.id +FROM public.cargos_colaboradores cc +WHERE c.cargo = cc.nombre; + +-- 5. Add foreign key constraint +ALTER TABLE public.colaboradores + ADD CONSTRAINT fk_colaboradores_cargos FOREIGN KEY (cargo_id) + REFERENCES public.cargos_colaboradores (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE; + +-- 6. Drop old cargo column +ALTER TABLE public.colaboradores DROP COLUMN IF EXISTS cargo; diff --git a/foundation_system/Migrations/Scripts/MigrateModules.sql b/foundation_system/Migrations/Scripts/MigrateModules.sql new file mode 100644 index 0000000..7487922 --- /dev/null +++ b/foundation_system/Migrations/Scripts/MigrateModules.sql @@ -0,0 +1,38 @@ +-- 1. Create modulos table +CREATE TABLE IF NOT EXISTS public.modulos +( + id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), + nombre character varying(100) COLLATE pg_catalog."default" NOT NULL, + icono character varying(50) COLLATE pg_catalog."default", + orden integer NOT NULL DEFAULT 0, + activo boolean NOT NULL DEFAULT true, + creado_en timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT modulos_pkey PRIMARY KEY (id) +); + +-- 2. Insert unique module names from permisos into modulos +INSERT INTO public.modulos (nombre) +SELECT DISTINCT modulo FROM public.permisos +WHERE modulo IS NOT NULL AND modulo <> ''; + +-- 3. Add modulo_id column to permisos +ALTER TABLE public.permisos ADD COLUMN IF NOT EXISTS modulo_id integer; + +-- 4. Update permisos.modulo_id based on matching names +UPDATE public.permisos p +SET modulo_id = m.id +FROM public.modulos m +WHERE p.modulo = m.nombre; + +-- 5. Make modulo_id NOT NULL (after ensuring all are updated) +-- ALTER TABLE public.permisos ALTER COLUMN modulo_id SET NOT NULL; + +-- 6. Add foreign key constraint +ALTER TABLE public.permisos + ADD CONSTRAINT fk_permisos_modulos FOREIGN KEY (modulo_id) + REFERENCES public.modulos (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE; + +-- 7. Drop old modulo column +ALTER TABLE public.permisos DROP COLUMN IF EXISTS modulo; diff --git a/foundation_system/Migrations/Scripts/UpdateBuscarPersonas.sql b/foundation_system/Migrations/Scripts/UpdateBuscarPersonas.sql new file mode 100644 index 0000000..d529c61 --- /dev/null +++ b/foundation_system/Migrations/Scripts/UpdateBuscarPersonas.sql @@ -0,0 +1,298 @@ +-- FUNCTION: public.buscar_personas_v2(text, text, integer) + +CREATE OR REPLACE FUNCTION public.buscar_personas_v2( + p_termino text, + p_tipo text, + p_limite integer DEFAULT 10) + RETURNS TABLE(id bigint, text text, score double precision, match_type text) + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE + ROWS 1000 + +AS $BODY$ +DECLARE + v_termino_norm TEXT; + v_tokens TEXT[]; + v_sim_threshold DOUBLE PRECISION := 0.15; -- Más permisivo + v_lev_threshold INTEGER := 3; -- Permite más errores + + -- Pesos para ranking (ajusta según importancia) + v_peso_nombre DOUBLE PRECISION := 1.0; + v_peso_apellido DOUBLE PRECISION := 0.8; + v_peso_codigo DOUBLE PRECISION := 0.6; + v_peso_dui DOUBLE PRECISION := 0.5; +BEGIN + -- Normalizar el término de búsqueda + v_termino_norm := normalizar_texto(p_termino); + + -- Tokenizar (separar por palabras) + v_tokens := string_to_array(v_termino_norm, ' '); + + -- ======================================================================== + -- BÚSQUEDA PARA NIÑOS + -- ======================================================================== + IF upper(p_tipo) = 'NINO' THEN + RETURN QUERY + WITH candidatos AS ( + -- FASE 1: Filtro rápido por índice (operador %) + SELECT + n.id, + p.nombres, + p.apellidos, + n.codigo_inscripcion, + normalizar_texto(p.nombres) AS nombres_norm, + normalizar_texto(p.apellidos) AS apellidos_norm, + normalizar_texto(n.codigo_inscripcion) AS codigo_norm + FROM ninos n + JOIN personas p ON p.id = n.persona_id + WHERE n.activo = true + AND p.activo = true + AND ( + -- Búsqueda rápida con operador % (usa índice GIN) + normalizar_texto(p.nombres || ' ' || p.apellidos) % v_termino_norm + OR normalizar_texto(n.codigo_inscripcion) % v_termino_norm + OR normalizar_texto(p.nombres) % ANY(v_tokens) + OR normalizar_texto(p.apellidos) % ANY(v_tokens) + ) + ), + scoring AS ( + -- FASE 2: Cálculo preciso de scores + SELECT + c.id, + (c.nombres || ' ' || c.apellidos || ' | Código: ' || c.codigo_inscripcion)::TEXT AS display_text, + + -- Score por similitud de nombre completo + (similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) * v_peso_nombre) AS score_nombre_completo, + + -- Score por similitud de nombres individuales + (similarity(c.nombres_norm, v_termino_norm) * v_peso_nombre) AS score_nombres, + + -- Score por similitud de apellidos + (similarity(c.apellidos_norm, v_termino_norm) * v_peso_apellido) AS score_apellidos, + + -- Score por código + (similarity(c.codigo_norm, v_termino_norm) * v_peso_codigo) AS score_codigo, + + -- Score por Levenshtein (nombres) + (1.0 - (levenshtein(c.nombres_norm, v_termino_norm)::DOUBLE PRECISION + / GREATEST(length(c.nombres_norm), length(v_termino_norm), 1))) * v_peso_nombre AS score_lev_nombres, + + -- Score por tokens (cada palabra individual) + ( + SELECT COALESCE(MAX( + GREATEST( + similarity(c.nombres_norm, tok), + similarity(c.apellidos_norm, tok) + ) + ), 0) + FROM unnest(v_tokens) tok + ) * 0.9 AS score_tokens, + + -- Determinar tipo de match + CASE + WHEN similarity(c.codigo_norm, v_termino_norm) >= v_sim_threshold THEN 'codigo' + WHEN similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) >= v_sim_threshold THEN 'nombre_completo' + WHEN levenshtein(c.nombres_norm, v_termino_norm) <= v_lev_threshold THEN 'fuzzy' + ELSE 'token' + END AS match_type + + FROM candidatos c + ) + SELECT + s.id, + s.display_text, + -- Score final: tomar el máximo de todos los métodos + GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_codigo, + s.score_lev_nombres, + s.score_tokens + ) AS score, + s.match_type + FROM scoring s + WHERE GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_codigo, + s.score_lev_nombres, + s.score_tokens + ) > 0.1 -- Umbral mínimo para aparecer en resultados + ORDER BY score DESC + LIMIT p_limite; + + -- ======================================================================== + -- BÚSQUEDA PARA ENCARGADOS + -- ======================================================================== + ELSIF upper(p_tipo) = 'ENCARGADO' THEN + RETURN QUERY + WITH candidatos AS ( + SELECT + p.id, + p.nombres, + p.apellidos, + COALESCE(p.dui, '') AS dui, + en.parentesco, + normalizar_texto(p.nombres) AS nombres_norm, + normalizar_texto(p.apellidos) AS apellidos_norm, + normalizar_texto(COALESCE(p.dui, '')) AS dui_norm + FROM encargados_nino en + JOIN personas p ON p.id = en.persona_id + WHERE p.activo = true + AND ( + normalizar_texto(p.nombres || ' ' || p.apellidos) % v_termino_norm + OR normalizar_texto(COALESCE(p.dui, '')) % v_termino_norm + OR normalizar_texto(p.nombres) % ANY(v_tokens) + OR normalizar_texto(p.apellidos) % ANY(v_tokens) + ) + ), + scoring AS ( + SELECT + c.id, + (c.nombres || ' ' || c.apellidos || ' (' || c.parentesco || ')')::TEXT AS display_text, + + (similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) * v_peso_nombre) AS score_nombre_completo, + (similarity(c.nombres_norm, v_termino_norm) * v_peso_nombre) AS score_nombres, + (similarity(c.apellidos_norm, v_termino_norm) * v_peso_apellido) AS score_apellidos, + (similarity(c.dui_norm, v_termino_norm) * v_peso_dui) AS score_dui, + + (1.0 - (levenshtein(c.nombres_norm, v_termino_norm)::DOUBLE PRECISION + / GREATEST(length(c.nombres_norm), length(v_termino_norm), 1))) * v_peso_nombre AS score_lev_nombres, + + ( + SELECT COALESCE(MAX( + GREATEST( + similarity(c.nombres_norm, tok), + similarity(c.apellidos_norm, tok) + ) + ), 0) + FROM unnest(v_tokens) tok + ) * 0.9 AS score_tokens, + + CASE + WHEN similarity(c.dui_norm, v_termino_norm) >= v_sim_threshold THEN 'dui' + WHEN similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) >= v_sim_threshold THEN 'nombre_completo' + WHEN levenshtein(c.nombres_norm, v_termino_norm) <= v_lev_threshold THEN 'fuzzy' + ELSE 'token' + END AS match_type + + FROM candidatos c + ) + SELECT + s.id, + s.display_text, + GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_dui, + s.score_lev_nombres, + s.score_tokens + ) AS score, + s.match_type + FROM scoring s + WHERE GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_dui, + s.score_lev_nombres, + s.score_tokens + ) > 0.1 + ORDER BY score DESC + LIMIT p_limite; + + -- ======================================================================== + -- BÚSQUEDA PARA COLABORADORES + -- ======================================================================== + ELSIF upper(p_tipo) = 'COLABORADOR' THEN + RETURN QUERY + WITH candidatos AS ( + SELECT + c.id, + p.nombres, + p.apellidos, + COALESCE(p.dui, '') AS dui, + cc.nombre as cargo, + normalizar_texto(p.nombres) AS nombres_norm, + normalizar_texto(p.apellidos) AS apellidos_norm, + normalizar_texto(COALESCE(p.dui, '')) AS dui_norm + FROM colaboradores c + JOIN personas p ON p.id = c.persona_id + LEFT JOIN cargos_colaboradores cc ON cc.id = c.cargo_id + WHERE c.activo = true + AND p.activo = true + AND ( + normalizar_texto(p.nombres || ' ' || p.apellidos) % v_termino_norm + OR normalizar_texto(COALESCE(p.dui, '')) % v_termino_norm + OR normalizar_texto(p.nombres) % ANY(v_tokens) + OR normalizar_texto(p.apellidos) % ANY(v_tokens) + ) + ), + scoring AS ( + SELECT + c.id, + (c.nombres || ' ' || c.apellidos || ' (' || COALESCE(c.cargo, 'Sin Cargo') || ')')::TEXT AS display_text, + + (similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) * v_peso_nombre) AS score_nombre_completo, + (similarity(c.nombres_norm, v_termino_norm) * v_peso_nombre) AS score_nombres, + (similarity(c.apellidos_norm, v_termino_norm) * v_peso_apellido) AS score_apellidos, + (similarity(c.dui_norm, v_termino_norm) * v_peso_dui) AS score_dui, + + (1.0 - (levenshtein(c.nombres_norm, v_termino_norm)::DOUBLE PRECISION + / GREATEST(length(c.nombres_norm), length(v_termino_norm), 1))) * v_peso_nombre AS score_lev_nombres, + + ( + SELECT COALESCE(MAX( + GREATEST( + similarity(c.nombres_norm, tok), + similarity(c.apellidos_norm, tok) + ) + ), 0) + FROM unnest(v_tokens) tok + ) * 0.9 AS score_tokens, + + CASE + WHEN similarity(c.dui_norm, v_termino_norm) >= v_sim_threshold THEN 'dui' + WHEN similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) >= v_sim_threshold THEN 'nombre_completo' + WHEN levenshtein(c.nombres_norm, v_termino_norm) <= v_lev_threshold THEN 'fuzzy' + ELSE 'token' + END AS match_type + + FROM candidatos c + ) + SELECT + s.id, + s.display_text, + GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_dui, + s.score_lev_nombres, + s.score_tokens + ) AS score, + s.match_type + FROM scoring s + WHERE GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_dui, + s.score_lev_nombres, + s.score_tokens + ) > 0.1 + ORDER BY score DESC + LIMIT p_limite; + + ELSE + RAISE EXCEPTION 'Tipo no soportado: %. Use NINO, ENCARGADO o COLABORADOR', p_tipo; + END IF; +END; +$BODY$; + +ALTER FUNCTION public.buscar_personas_v2(text, text, integer) + OWNER TO ecclesia; diff --git a/foundation_system/Models/CargoColaborador.cs b/foundation_system/Models/CargoColaborador.cs new file mode 100644 index 0000000..5c43a8c --- /dev/null +++ b/foundation_system/Models/CargoColaborador.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace foundation_system.Models; + +[Table("cargos_colaboradores", Schema = "public")] +public class CargoColaborador +{ + [Key] + [Column("id")] + public long Id { get; set; } + + [Required] + [MaxLength(100)] + [Column("nombre")] + public string Nombre { get; set; } = string.Empty; + + [Column("descripcion")] + public string? Descripcion { get; set; } + + [Column("activo")] + public bool Activo { get; set; } = true; + + [Column("creado_en")] + public DateTime CreadoEn { get; set; } = DateTime.UtcNow; + + public virtual ICollection Colaboradores { get; set; } = new List(); +} diff --git a/foundation_system/Models/Colaborador.cs b/foundation_system/Models/Colaborador.cs index 15e78dc..ac75ff2 100644 --- a/foundation_system/Models/Colaborador.cs +++ b/foundation_system/Models/Colaborador.cs @@ -16,10 +16,12 @@ public class Colaborador [ForeignKey("PersonaId")] public virtual Persona Persona { get; set; } = null!; + [Column("cargo_id")] [Required] - [MaxLength(100)] - [Column("cargo")] - public string Cargo { get; set; } = string.Empty; + public long CargoId { get; set; } + + [ForeignKey("CargoId")] + public virtual CargoColaborador? Cargo { get; set; } [Required] [MaxLength(50)] diff --git a/foundation_system/Models/Modulo.cs b/foundation_system/Models/Modulo.cs new file mode 100644 index 0000000..837912d --- /dev/null +++ b/foundation_system/Models/Modulo.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace foundation_system.Models; + +[Table("modulos")] +public class Modulo +{ + [Key] + [Column("id")] + public int Id { get; set; } + + [Column("nombre")] + [Required] + [StringLength(100)] + public string Nombre { get; set; } = string.Empty; + + [Column("icono")] + [StringLength(50)] + public string? Icono { get; set; } + + [Column("orden")] + public int Orden { get; set; } = 0; + + [Column("activo")] + public bool Activo { get; set; } = true; + + [Column("creado_en")] + public DateTime CreadoEn { get; set; } = DateTime.UtcNow; + + // Navigation property + public virtual ICollection Permisos { get; set; } = new List(); +} diff --git a/foundation_system/Models/Permiso.cs b/foundation_system/Models/Permiso.cs index 11c3b57..479495b 100644 --- a/foundation_system/Models/Permiso.cs +++ b/foundation_system/Models/Permiso.cs @@ -10,10 +10,12 @@ public class Permiso [Column("id")] public int Id { get; set; } - [Column("modulo")] + [Column("modulo_id")] [Required] - [StringLength(50)] - public string Modulo { get; set; } = string.Empty; + public int ModuloId { get; set; } + + [ForeignKey("ModuloId")] + public virtual Modulo? Modulo { get; set; } [Column("codigo")] [Required] diff --git a/foundation_system/Models/ViewModels/ColaboradorViewModel.cs b/foundation_system/Models/ViewModels/ColaboradorViewModel.cs index 1647ec1..ee8b08e 100644 --- a/foundation_system/Models/ViewModels/ColaboradorViewModel.cs +++ b/foundation_system/Models/ViewModels/ColaboradorViewModel.cs @@ -39,7 +39,7 @@ public class ColaboradorViewModel [Required(ErrorMessage = "El cargo es requerido")] [Display(Name = "Cargo")] - public string Cargo { get; set; } = string.Empty; + public long CargoId { get; set; } [Required(ErrorMessage = "El tipo de colaborador es requerido")] [Display(Name = "Tipo de Colaborador")] diff --git a/foundation_system/Program.cs b/foundation_system/Program.cs index ddf9e8b..612f378 100644 --- a/foundation_system/Program.cs +++ b/foundation_system/Program.cs @@ -38,6 +38,7 @@ builder.Services.AddControllersWithViews(options => .RequireAuthenticatedUser() .Build(); options.Filters.Add(new Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter(policy)); + options.Filters.Add(new foundation_system.Filters.DynamicAuthorizationFilter()); }); var app = builder.Build(); diff --git a/foundation_system/Views/CargoColaborador/Create.cshtml b/foundation_system/Views/CargoColaborador/Create.cshtml new file mode 100644 index 0000000..1bf3d80 --- /dev/null +++ b/foundation_system/Views/CargoColaborador/Create.cshtml @@ -0,0 +1,54 @@ +@model foundation_system.Models.CargoColaborador + +@{ + ViewData["Title"] = "Nuevo Cargo"; +} + +
+
+
+
+
+
@ViewData["Title"]
+ + Volver + +
+
+
+
+ +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+
+ +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/foundation_system/Views/CargoColaborador/Edit.cshtml b/foundation_system/Views/CargoColaborador/Edit.cshtml new file mode 100644 index 0000000..15c079b --- /dev/null +++ b/foundation_system/Views/CargoColaborador/Edit.cshtml @@ -0,0 +1,56 @@ +@model foundation_system.Models.CargoColaborador + +@{ + ViewData["Title"] = "Editar Cargo"; +} + +
+
+
+
+
+
@ViewData["Title"]: @Model.Nombre
+ + Volver + +
+
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+
+ +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/foundation_system/Views/CargoColaborador/Index.cshtml b/foundation_system/Views/CargoColaborador/Index.cshtml new file mode 100644 index 0000000..87d537b --- /dev/null +++ b/foundation_system/Views/CargoColaborador/Index.cshtml @@ -0,0 +1,84 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Gestión de Cargos de Colaboradores"; +} + +
+
+

@ViewData["Title"]

+ + Nuevo Cargo + +
+ + @if (TempData["ErrorMessage"] != null) + { + + } + +
+
+
+ + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + } + +
NombreDescripciónEstadoAcciones
@item.Nombre@item.Descripcion + @if (item.Activo) + { + + } + else + { + + } + +
+ + + + +
+
+
+
+
+
+ + + +@section Scripts { + +} diff --git a/foundation_system/Views/Colaborador/Create.cshtml b/foundation_system/Views/Colaborador/Create.cshtml index 3c2ea40..c95e615 100644 --- a/foundation_system/Views/Colaborador/Create.cshtml +++ b/foundation_system/Views/Colaborador/Create.cshtml @@ -72,9 +72,11 @@
Información Laboral
- - - + + +
diff --git a/foundation_system/Views/Colaborador/Edit.cshtml b/foundation_system/Views/Colaborador/Edit.cshtml index 0c44b07..4e7d377 100644 --- a/foundation_system/Views/Colaborador/Edit.cshtml +++ b/foundation_system/Views/Colaborador/Edit.cshtml @@ -73,9 +73,11 @@
Información Laboral
- - - + + +
diff --git a/foundation_system/Views/Colaborador/Index.cshtml b/foundation_system/Views/Colaborador/Index.cshtml index 953ca74..7649b14 100644 --- a/foundation_system/Views/Colaborador/Index.cshtml +++ b/foundation_system/Views/Colaborador/Index.cshtml @@ -42,7 +42,7 @@
- @item.Cargo + @(item.Cargo?.Nombre ?? "Sin Cargo") @item.TipoColaborador @item.Persona.Dui @item.Persona.Telefono diff --git a/foundation_system/Views/ColaboradorAsistencia/ImprimirAsistencia.cshtml b/foundation_system/Views/ColaboradorAsistencia/ImprimirAsistencia.cshtml new file mode 100644 index 0000000..cbc6463 --- /dev/null +++ b/foundation_system/Views/ColaboradorAsistencia/ImprimirAsistencia.cshtml @@ -0,0 +1,98 @@ +@model IEnumerable + +@{ + Layout = null; + var selectedDate = (DateOnly)ViewBag.SelectedDate; + var asistencias = (Dictionary)ViewBag.Asistencias; +} + + + + + + + Asistencia - @selectedDate.ToString("dd/MM/yyyy") + + + + + +
+
+ Volver + +
+ +
+

Misión Esperanza (MIES)

+

Control de Asistencia de Colaboradores

+

Fecha: @selectedDate.ToString("dddd, dd de MMMM de yyyy")

+
+ + + + + + + + + + + + + @{ int i = 1; } + @foreach (var item in Model) + { + var asistencia = asistencias.ContainsKey(item.Id) ? asistencias[item.Id] : null; + + + + + + + + } + +
#ColaboradorCargoEstadoObservaciones
@i++ + @item.Persona.Apellidos, @item.Persona.Nombres + @(item.Cargo?.Nombre ?? "Sin Cargo") + @if (asistencia != null) + { + @asistencia.Estado + } + else + { + SIN REGISTRO + } + @(asistencia?.Observaciones ?? "-")
+ +
+
+
+

Firma Responsable

+
+
+
+

Sello Institucional

+
+
+ +
+ Generado el @DateTime.Now.ToString("dd/MM/yyyy HH:mm") +
+
+ + diff --git a/foundation_system/Views/ColaboradorAsistencia/Index.cshtml b/foundation_system/Views/ColaboradorAsistencia/Index.cshtml index 524a8f6..1442c9f 100644 --- a/foundation_system/Views/ColaboradorAsistencia/Index.cshtml +++ b/foundation_system/Views/ColaboradorAsistencia/Index.cshtml @@ -10,10 +10,18 @@
@ViewData["Title"]
-
@@ -38,7 +46,7 @@
@item.Persona.Nombres @item.Persona.Apellidos
@item.HorarioEntrada?.ToString(@"hh\:mm") - @item.HorarioSalida?.ToString(@"hh\:mm") - @item.Cargo + @(item.Cargo?.Nombre ?? "Sin Cargo")
+ $(document).ready(function() { + // Configuración global de toastr + toastr.options = { + "closeButton": true, + "progressBar": true, + "positionClass": "toast-top-right", + "timeOut": "3000" + }; + + @if (TempData["InfoMessage"] != null) + { + toastr.info('@TempData["InfoMessage"]'); + } + @if (TempData["SuccessMessage"] != null) + { + toastr.success('@TempData["SuccessMessage"]'); + } + @if (TempData["ErrorMessage"] != null) + { + toastr.error('@TempData["ErrorMessage"]'); + } + }); + function changeDate(fecha) { window.location.href = '@Url.Action("Index")?fecha=' + fecha; } @@ -105,9 +136,14 @@ .then(response => response.json()) .then(data => { if (data.success) { - // Opcional: Mostrar feedback visual sutil - console.log('Guardado'); + toastr.success('Asistencia guardada correctamente'); + } else { + toastr.error('Error al guardar la asistencia'); } + }) + .catch(error => { + console.error('Error:', error); + toastr.error('Ocurrió un error al procesar la solicitud'); }); } diff --git a/foundation_system/Views/ColaboradorAsistencia/ReporteAsistencia.cshtml b/foundation_system/Views/ColaboradorAsistencia/ReporteAsistencia.cshtml new file mode 100644 index 0000000..a6d77ad --- /dev/null +++ b/foundation_system/Views/ColaboradorAsistencia/ReporteAsistencia.cshtml @@ -0,0 +1,327 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Reporte de Asistencia por Colaborador"; + var colaboradores = (IEnumerable)ViewBag.Colaboradores; + var inicio = (DateOnly)ViewBag.Inicio; + var fin = (DateOnly)ViewBag.Fin; + var selectedColaboradorId = (long?)ViewBag.ColaboradorId; +} + +
+
+

@ViewData["Title"]

+
+ + + Volver + +
+
+ +
+
+
Filtros de Reporte
+
+
+
+
+ +
+ + c.Id == selectedColaboradorId).Persona.Apellidos}, {colaboradores.First(c => c.Id == selectedColaboradorId).Persona.Nombres}" : "")" + readonly placeholder="Seleccione un colaborador..." required /> + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + @if (selectedColaboradorId.HasValue) + { + var selectedColaborador = colaboradores.FirstOrDefault(c => c.Id == selectedColaboradorId); + var stats = Model.GroupBy(a => a.Estado).ToDictionary(g => g.Key, g => g.Count()); + + @if (selectedColaborador != null) + { + + } + + + +
+
+
+ + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + } + @if (!Model.Any()) + { + + + + } + +
FechaDíaEstadoObservaciones
@item.Fecha.ToString("dd/MM/yyyy")@item.Fecha.ToString("dddd") + + @item.Estado + + @item.Observaciones
No se encontraron registros en este rango de fechas.
+
+
+
+ } + else + { +
+ +

Seleccione un colaborador y un rango de fechas para generar el reporte.

+
+ } +
+ + + + +@section Scripts { + +} + +@section Styles { + +} diff --git a/foundation_system/Views/Modulo/Create.cshtml b/foundation_system/Views/Modulo/Create.cshtml new file mode 100644 index 0000000..37b7a06 --- /dev/null +++ b/foundation_system/Views/Modulo/Create.cshtml @@ -0,0 +1,73 @@ +@model foundation_system.Models.Modulo + +@{ + ViewData["Title"] = "Nuevo Módulo"; +} + +
+
+
+
+
+
@ViewData["Title"]
+ + Volver + +
+
+
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ +
+ + +
+ Use nombres de Bootstrap Icons + +
+
+ +
+ + +
+
+
+ +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} + +} diff --git a/foundation_system/Views/Modulo/Edit.cshtml b/foundation_system/Views/Modulo/Edit.cshtml new file mode 100644 index 0000000..33f16cb --- /dev/null +++ b/foundation_system/Views/Modulo/Edit.cshtml @@ -0,0 +1,75 @@ +@model foundation_system.Models.Modulo + +@{ + ViewData["Title"] = "Editar Módulo"; +} + +
+
+
+
+
+
@ViewData["Title"]: @Model.Nombre
+ + Volver + +
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ +
+ + +
+ Use nombres de Bootstrap Icons + +
+
+ +
+ + +
+
+
+ +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} + +} diff --git a/foundation_system/Views/Modulo/Index.cshtml b/foundation_system/Views/Modulo/Index.cshtml new file mode 100644 index 0000000..545cc9f --- /dev/null +++ b/foundation_system/Views/Modulo/Index.cshtml @@ -0,0 +1,86 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Gestión de Módulos / Secciones"; +} + +
+
+

@ViewData["Title"]

+ + Nuevo Módulo + +
+ + @if (TempData["ErrorMessage"] != null) + { + + } + +
+
+
+ + + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + + } + +
OrdenNombreIconoActivoAcciones
@item.Orden@item.Nombre@item.Icono + @if (item.Activo) + { + + } + else + { + + } + +
+ + + + +
+
+
+
+
+
+ + + +@section Scripts { + +} diff --git a/foundation_system/Views/Permiso/Create.cshtml b/foundation_system/Views/Permiso/Create.cshtml index 811906b..1317927 100644 --- a/foundation_system/Views/Permiso/Create.cshtml +++ b/foundation_system/Views/Permiso/Create.cshtml @@ -17,9 +17,12 @@
- - - + + +
diff --git a/foundation_system/Views/Permiso/Edit.cshtml b/foundation_system/Views/Permiso/Edit.cshtml index 928a810..52700c9 100644 --- a/foundation_system/Views/Permiso/Edit.cshtml +++ b/foundation_system/Views/Permiso/Edit.cshtml @@ -18,9 +18,12 @@
- - - + + +
diff --git a/foundation_system/Views/Permiso/Index.cshtml b/foundation_system/Views/Permiso/Index.cshtml index 8fde6ab..3e720fe 100644 --- a/foundation_system/Views/Permiso/Index.cshtml +++ b/foundation_system/Views/Permiso/Index.cshtml @@ -26,8 +26,8 @@ - + @@ -39,8 +39,13 @@ @foreach (var item in Model) { + - diff --git a/foundation_system/Views/Shared/Components/Menu/Default.cshtml b/foundation_system/Views/Shared/Components/Menu/Default.cshtml index 8d99ec6..100a39a 100644 --- a/foundation_system/Views/Shared/Components/Menu/Default.cshtml +++ b/foundation_system/Views/Shared/Components/Menu/Default.cshtml @@ -12,7 +12,13 @@ @foreach (var group in groupedMenu) { - + @foreach (var item in group) { diff --git a/foundation_system/wwwroot/css/site.css b/foundation_system/wwwroot/css/site.css index 64c447d..9fc6cf7 100644 --- a/foundation_system/wwwroot/css/site.css +++ b/foundation_system/wwwroot/css/site.css @@ -1,219 +1,227 @@ :root { - --sidebar-width: 260px; - --sidebar-bg: #1e293b; - --sidebar-hover: #334155; - --sidebar-active: #3b82f6; - --sidebar-text: #f1f5f9; - --sidebar-text-muted: #94a3b8; - - --header-height: 60px; - --header-bg: #ffffff; - - --content-bg: #f8fafc; - - --primary-color: #2563eb; - --primary-hover: #1d4ed8; - - --text-main: #0f172a; - --text-muted: #64748b; - - --border-color: #e2e8f0; - --card-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --sidebar-width: 260px; + --sidebar-bg: #1e293b; + --sidebar-hover: #334155; + --sidebar-active: #3b82f6; + --sidebar-text: #f1f5f9; + --sidebar-text-muted: #94a3b8; + + --header-height: 60px; + --header-bg: #ffffff; + + --content-bg: #f8fafc; + + --primary-color: #2563eb; + --primary-hover: #1d4ed8; + + --text-main: #0f172a; + --text-muted: #64748b; + + --border-color: #e2e8f0; + --card-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); } body { - font-family: 'Inter', system-ui, -apple-system, sans-serif; - background-color: var(--content-bg); - color: var(--text-main); - margin: 0; - padding: 0; - overflow-x: hidden; + font-family: "Inter", system-ui, -apple-system, sans-serif; + background-color: var(--content-bg); + color: var(--text-main); + margin: 0; + padding: 0; + overflow-x: hidden; } /* Layout Structure */ .app-wrapper { - display: flex; - min-height: 100vh; + display: flex; + min-height: 100vh; } /* Sidebar */ .sidebar { - width: var(--sidebar-width); - background-color: var(--sidebar-bg); - color: var(--sidebar-text); - display: flex; - flex-direction: column; - position: fixed; - height: 100vh; - z-index: 1000; - transition: all 0.3s ease; + width: var(--sidebar-width); + background-color: var(--sidebar-bg); + color: var(--sidebar-text); + display: flex; + flex-direction: column; + position: fixed; + height: 100vh; + z-index: 1000; + transition: all 0.3s ease; } .sidebar-header { - height: var(--header-height); - display: flex; - align-items: center; - padding: 0 1.5rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); + height: var(--header-height); + display: flex; + align-items: center; + padding: 0 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .sidebar-brand { - font-size: 1.25rem; - font-weight: 700; - color: white; - text-decoration: none; - letter-spacing: 1px; + font-size: 1.25rem; + font-weight: 700; + color: white; + text-decoration: none; + letter-spacing: 1px; } .sidebar-nav { - flex: 1; - padding: 1rem 0; - overflow-y: auto; + flex: 1; + padding: 1rem 0; + overflow-y: auto; } .nav-section-title { - padding: 0.5rem 1.5rem; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - color: var(--sidebar-text-muted); - letter-spacing: 0.05em; + padding: 1.25rem 1.5rem 0.5rem; + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + color: white; + letter-spacing: 0.08em; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + margin-bottom: 0.25rem; + opacity: 0.9; } .nav-link-custom { - display: flex; - align-items: center; - padding: 0.75rem 1.5rem; - color: var(--sidebar-text-muted); - text-decoration: none; - transition: all 0.2s; - gap: 0.75rem; - font-size: 0.9375rem; + display: flex; + align-items: center; + padding: 0.75rem 1.5rem; + color: var(--sidebar-text-muted); + text-decoration: none; + transition: all 0.2s; + gap: 0.75rem; + font-size: 0.9375rem; } .nav-link-custom:hover { - background-color: var(--sidebar-hover); - color: white; + background-color: var(--sidebar-hover); + color: white; } .nav-link-custom.active { - background-color: var(--sidebar-active); - color: white; + background-color: var(--sidebar-active); + color: white; } .nav-link-custom i { - font-size: 1.1rem; - width: 20px; - text-align: center; + font-size: 1.1rem; + width: 20px; + text-align: center; } /* Main Content */ .main-content { - flex: 1; - margin-left: var(--sidebar-width); - display: flex; - flex-direction: column; - min-width: 0; + flex: 1; + margin-left: var(--sidebar-width); + display: flex; + flex-direction: column; + min-width: 0; } .top-header { - height: var(--header-height); - background-color: var(--header-bg); - border-bottom: 1px solid var(--border-color); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 2rem; - position: sticky; - top: 0; - z-index: 900; + height: var(--header-height); + background-color: var(--header-bg); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 2rem; + position: sticky; + top: 0; + z-index: 900; } .page-container { - padding: 2rem; - flex: 1; + padding: 2rem; + flex: 1; } /* Components */ .card-custom { - background: white; - border-radius: 0.5rem; - border: 1px solid var(--border-color); - box-shadow: var(--card-shadow); - padding: 1.5rem; - margin-bottom: 1.5rem; + background: white; + border-radius: 0.5rem; + border: 1px solid var(--border-color); + box-shadow: var(--card-shadow); + padding: 1.5rem; + margin-bottom: 1.5rem; } .btn-primary-custom { - background-color: var(--primary-color); - border-color: var(--primary-color); - color: white; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-weight: 500; - transition: all 0.2s; + background-color: var(--primary-color); + border-color: var(--primary-color); + color: white; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + transition: all 0.2s; } .btn-primary-custom:hover { - background-color: var(--primary-hover); - border-color: var(--primary-hover); + background-color: var(--primary-hover); + border-color: var(--primary-hover); } /* Typography */ -h1, h2, h3, h4, h5, h6 { - font-weight: 600; - color: var(--text-main); +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 600; + color: var(--text-main); } .text-muted-custom { - color: var(--text-muted); + color: var(--text-muted); } /* Tables */ .table-custom { - width: 100%; - border-collapse: collapse; + width: 100%; + border-collapse: collapse; } .table-custom th { - background-color: #f8fafc; - padding: 0.75rem 1rem; - text-align: left; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - color: var(--text-muted); - border-bottom: 1px solid var(--border-color); + background-color: #f8fafc; + padding: 0.75rem 1rem; + text-align: left; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: var(--text-muted); + border-bottom: 1px solid var(--border-color); } .table-custom td { - padding: 1rem; - border-bottom: 1px solid var(--border-color); - font-size: 0.875rem; + padding: 1rem; + border-bottom: 1px solid var(--border-color); + font-size: 0.875rem; } .table-custom tr:hover { - background-color: #f1f5f9; + background-color: #f1f5f9; } /* Footer */ .footer { - background-color: var(--header-bg); - border-top: 1px solid var(--border-color); - padding: 1rem 2rem; - margin-top: auto; - width: 100%; + background-color: var(--header-bg); + border-top: 1px solid var(--border-color); + padding: 1rem 2rem; + margin-top: auto; + width: 100%; } /* Responsive */ @media (max-width: 768px) { - .sidebar { - transform: translateX(-100%); - } - .main-content { - margin-left: 0; - } - .sidebar.open { - transform: translateX(0); - } -} \ No newline at end of file + .sidebar { + transform: translateX(-100%); + } + .main-content { + margin-left: 0; + } + .sidebar.open { + transform: translateX(0); + } +}
Orden MóduloOrden Nombre Ruta (URL) Icono
+ + + @(item.Modulo?.Nombre ?? "Sin Módulo") + + @item.Orden@item.Modulo @item.Nombre @item.Url @item.Icono