Version Stable 001
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
105
foundation_system/Controllers/CargoColaboradorController.cs
Normal file
105
foundation_system/Controllers/CargoColaboradorController.cs
Normal file
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,15 @@ public class ColaboradorAsistenciaController : Controller
|
||||
public async Task<IActionResult> 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();
|
||||
@@ -66,4 +71,83 @@ public class ColaboradorAsistenciaController : Controller
|
||||
await _context.SaveChangesAsync();
|
||||
return Json(new { success = true });
|
||||
}
|
||||
|
||||
// GET: ColaboradorAsistencia/ImprimirAsistencia
|
||||
public async Task<IActionResult> 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<IActionResult> 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<AsistenciaColaborador>());
|
||||
}
|
||||
|
||||
// GET: ColaboradorAsistencia/SearchColaboradores
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> SearchColaboradores(string term)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(term) || term.Length < 2)
|
||||
{
|
||||
return Json(new List<object>());
|
||||
}
|
||||
|
||||
var results = await _context.Database
|
||||
.SqlQueryRaw<SearchResult>(
|
||||
"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; }
|
||||
}
|
||||
|
||||
@@ -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<IActionResult> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<HomeController> _logger;
|
||||
|
||||
105
foundation_system/Controllers/ModuloController.cs
Normal file
105
foundation_system/Controllers/ModuloController.cs
Normal file
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<IActionResult> 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<IActionResult> Create()
|
||||
{
|
||||
ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync();
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: Permiso/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create([Bind("Modulo,Codigo,Nombre,Descripcion,Url,Icono,Orden,EsMenu")] Permiso permiso)
|
||||
public async Task<IActionResult> 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<IActionResult> Edit(int id, [Bind("Id,Modulo,Codigo,Nombre,Descripcion,Url,Icono,Orden,EsMenu")] Permiso permiso)
|
||||
public async Task<IActionResult> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ using foundation_system.Models;
|
||||
|
||||
namespace foundation_system.Controllers;
|
||||
|
||||
[Authorize(Roles = "ROOT")]
|
||||
[Authorize]
|
||||
public class RolController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,8 +15,10 @@ public class ApplicationDbContext : DbContext
|
||||
public DbSet<RolSistema> RolesSistema { get; set; }
|
||||
public DbSet<RolUsuario> RolesUsuario { get; set; }
|
||||
public DbSet<Permiso> Permisos { get; set; }
|
||||
public DbSet<Modulo> Modulos { get; set; }
|
||||
public DbSet<RolPermiso> RolesPermisos { get; set; }
|
||||
public DbSet<Colaborador> Colaboradores { get; set; }
|
||||
public DbSet<CargoColaborador> CargosColaboradores { get; set; }
|
||||
public DbSet<AsistenciaColaborador> AsistenciasColaboradores { get; set; }
|
||||
public DbSet<Nino> Ninos { get; set; }
|
||||
public DbSet<Asistencia> Asistencias { get; set; }
|
||||
@@ -57,11 +59,21 @@ public class ApplicationDbContext : DbContext
|
||||
.WithMany()
|
||||
.HasForeignKey(rp => rp.PermisoId);
|
||||
|
||||
modelBuilder.Entity<Permiso>()
|
||||
.HasOne(p => p.Modulo)
|
||||
.WithMany(m => m.Permisos)
|
||||
.HasForeignKey(p => p.ModuloId);
|
||||
|
||||
modelBuilder.Entity<Usuario>()
|
||||
.HasOne(u => u.Persona)
|
||||
.WithMany()
|
||||
.HasForeignKey(u => u.PersonaId);
|
||||
|
||||
modelBuilder.Entity<Colaborador>()
|
||||
.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())
|
||||
{
|
||||
|
||||
61
foundation_system/Filters/DynamicAuthorizationFilter.cs
Normal file
61
foundation_system/Filters/DynamicAuthorizationFilter.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
34
foundation_system/Migrations/Scripts/MigrateCargos.sql
Normal file
34
foundation_system/Migrations/Scripts/MigrateCargos.sql
Normal file
@@ -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;
|
||||
38
foundation_system/Migrations/Scripts/MigrateModules.sql
Normal file
38
foundation_system/Migrations/Scripts/MigrateModules.sql
Normal file
@@ -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;
|
||||
298
foundation_system/Migrations/Scripts/UpdateBuscarPersonas.sql
Normal file
298
foundation_system/Migrations/Scripts/UpdateBuscarPersonas.sql
Normal file
@@ -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;
|
||||
28
foundation_system/Models/CargoColaborador.cs
Normal file
28
foundation_system/Models/CargoColaborador.cs
Normal file
@@ -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<Colaborador> Colaboradores { get; set; } = new List<Colaborador>();
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
33
foundation_system/Models/Modulo.cs
Normal file
33
foundation_system/Models/Modulo.cs
Normal file
@@ -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<Permiso> Permisos { get; set; } = new List<Permiso>();
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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();
|
||||
|
||||
54
foundation_system/Views/CargoColaborador/Create.cshtml
Normal file
54
foundation_system/Views/CargoColaborador/Create.cshtml
Normal file
@@ -0,0 +1,54 @@
|
||||
@model foundation_system.Models.CargoColaborador
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Nuevo Cargo";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary">@ViewData["Title"]</h6>
|
||||
<a asp-action="Index" class="btn btn-sm btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Create">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Nombre" class="form-label">Nombre del Cargo</label>
|
||||
<input asp-for="Nombre" class="form-control" placeholder="Ej. Encargado, Cocinera/o, Maestra/o..." />
|
||||
<span asp-validation-for="Nombre" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Descripcion" class="form-label">Descripción</label>
|
||||
<textarea asp-for="Descripcion" class="form-control" rows="3"></textarea>
|
||||
<span asp-validation-for="Descripcion" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" asp-for="Activo">
|
||||
<label class="form-check-label" asp-for="Activo">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save me-2"></i>Guardar Cargo
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
56
foundation_system/Views/CargoColaborador/Edit.cshtml
Normal file
56
foundation_system/Views/CargoColaborador/Edit.cshtml
Normal file
@@ -0,0 +1,56 @@
|
||||
@model foundation_system.Models.CargoColaborador
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Editar Cargo";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary">@ViewData["Title"]: @Model.Nombre</h6>
|
||||
<a asp-action="Index" class="btn btn-sm btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Edit">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<input type="hidden" asp-for="CreadoEn" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Nombre" class="form-label">Nombre del Cargo</label>
|
||||
<input asp-for="Nombre" class="form-control" />
|
||||
<span asp-validation-for="Nombre" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Descripcion" class="form-label">Descripción</label>
|
||||
<textarea asp-for="Descripcion" class="form-control" rows="3"></textarea>
|
||||
<span asp-validation-for="Descripcion" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" asp-for="Activo">
|
||||
<label class="form-check-label" asp-for="Activo">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save me-2"></i>Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
84
foundation_system/Views/CargoColaborador/Index.cshtml
Normal file
84
foundation_system/Views/CargoColaborador/Index.cshtml
Normal file
@@ -0,0 +1,84 @@
|
||||
@model IEnumerable<foundation_system.Models.CargoColaborador>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Gestión de Cargos de Colaboradores";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="h3 mb-0 text-gray-800"><i class="bi bi-briefcase me-2"></i>@ViewData["Title"]</h2>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-lg"></i> Nuevo Cargo
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i> @TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Descripción</th>
|
||||
<th class="text-center">Estado</th>
|
||||
<th class="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td class="fw-bold">@item.Nombre</td>
|
||||
<td>@item.Descripcion</td>
|
||||
<td class="text-center">
|
||||
@if (item.Activo)
|
||||
{
|
||||
<span class="text-success"><i class="bi bi-check-circle-fill"></i></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted"><i class="bi bi-x-circle"></i></span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-outline-primary" title="Editar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger" title="Eliminar"
|
||||
onclick="confirmDelete(@item.Id, '@item.Nombre')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="deleteForm" asp-action="Delete" method="post" style="display:none;">
|
||||
<input type="hidden" name="id" id="deleteId" />
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function confirmDelete(id, name) {
|
||||
if (confirm(`¿Está seguro de que desea eliminar el cargo "${name}"? Solo se eliminará si no tiene colaboradores asociados.`)) {
|
||||
document.getElementById('deleteId').value = id;
|
||||
document.getElementById('deleteForm').submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -72,9 +72,11 @@
|
||||
<h6 class="text-primary border-bottom pb-2 mb-3">Información Laboral</h6>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Cargo" class="form-label"></label>
|
||||
<input asp-for="Cargo" class="form-control" placeholder="Ej: Maestro, Cocinero, Administrador" />
|
||||
<span asp-validation-for="Cargo" class="text-danger small"></span>
|
||||
<label asp-for="CargoId" class="form-label"></label>
|
||||
<select asp-for="CargoId" class="form-select" asp-items="@(new SelectList(ViewBag.Cargos, "Id", "Nombre"))">
|
||||
<option value="">Seleccione un cargo...</option>
|
||||
</select>
|
||||
<span asp-validation-for="CargoId" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="TipoColaborador" class="form-label"></label>
|
||||
|
||||
@@ -73,9 +73,11 @@
|
||||
<h6 class="text-primary border-bottom pb-2 mb-3">Información Laboral</h6>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Cargo" class="form-label"></label>
|
||||
<input asp-for="Cargo" class="form-control" />
|
||||
<span asp-validation-for="Cargo" class="text-danger small"></span>
|
||||
<label asp-for="CargoId" class="form-label"></label>
|
||||
<select asp-for="CargoId" class="form-select" asp-items="@(new SelectList(ViewBag.Cargos, "Id", "Nombre"))">
|
||||
<option value="">Seleccione un cargo...</option>
|
||||
</select>
|
||||
<span asp-validation-for="CargoId" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="TipoColaborador" class="form-label"></label>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge bg-info text-dark">@item.Cargo</span></td>
|
||||
<td><span class="badge bg-info text-dark">@(item.Cargo?.Nombre ?? "Sin Cargo")</span></td>
|
||||
<td>@item.TipoColaborador</td>
|
||||
<td>@item.Persona.Dui</td>
|
||||
<td>@item.Persona.Telefono</td>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
@model IEnumerable<foundation_system.Models.Colaborador>
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
var selectedDate = (DateOnly)ViewBag.SelectedDate;
|
||||
var asistencias = (Dictionary<long, foundation_system.Models.AsistenciaColaborador>)ViewBag.Asistencias;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Asistencia - @selectedDate.ToString("dd/MM/yyyy")</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; padding: 20px; background: white; }
|
||||
.report-header { border-bottom: 2px solid #333; margin-bottom: 20px; padding-bottom: 10px; }
|
||||
.table th { background-color: #f8f9fa !important; }
|
||||
@@media print {
|
||||
.no-print { display: none !important; }
|
||||
body { padding: 0; }
|
||||
.table th { background-color: #f8f9fa !important; -webkit-print-color-adjust: exact; }
|
||||
}
|
||||
.status-PRESENTE { color: #198754; font-weight: bold; }
|
||||
.status-AUSENTE { color: #dc3545; font-weight: bold; }
|
||||
.status-TARDANZA { color: #fd7e14; font-weight: bold; }
|
||||
.status-JUSTIFICADO { color: #0dcaf0; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center no-print mb-4">
|
||||
<a href="javascript:window.history.back()" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Volver</a>
|
||||
<button onclick="window.print()" class="btn btn-primary"><i class="bi bi-printer"></i> Imprimir Reporte</button>
|
||||
</div>
|
||||
|
||||
<div class="report-header text-center">
|
||||
<h2>Misión Esperanza (MIES)</h2>
|
||||
<h4>Control de Asistencia de Colaboradores</h4>
|
||||
<p class="mb-0"><strong>Fecha:</strong> @selectedDate.ToString("dddd, dd de MMMM de yyyy")</p>
|
||||
</div>
|
||||
|
||||
<table class="table table-bordered table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th>Colaborador</th>
|
||||
<th>Cargo</th>
|
||||
<th class="text-center">Estado</th>
|
||||
<th>Observaciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@{ int i = 1; }
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
var asistencia = asistencias.ContainsKey(item.Id) ? asistencias[item.Id] : null;
|
||||
<tr>
|
||||
<td>@i++</td>
|
||||
<td>
|
||||
<strong>@item.Persona.Apellidos, @item.Persona.Nombres</strong>
|
||||
</td>
|
||||
<td>@(item.Cargo?.Nombre ?? "Sin Cargo")</td>
|
||||
<td class="text-center">
|
||||
@if (asistencia != null)
|
||||
{
|
||||
<span class="status-@asistencia.Estado">@asistencia.Estado</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">SIN REGISTRO</span>
|
||||
}
|
||||
</td>
|
||||
<td>@(asistencia?.Observaciones ?? "-")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-5 row">
|
||||
<div class="col-6 text-center">
|
||||
<div style="border-top: 1px solid #000; width: 200px; margin: 50px auto 0;"></div>
|
||||
<p>Firma Responsable</p>
|
||||
</div>
|
||||
<div class="col-6 text-center">
|
||||
<div style="border-top: 1px solid #000; width: 200px; margin: 50px auto 0;"></div>
|
||||
<p>Sello Institucional</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end small text-muted">
|
||||
Generado el @DateTime.Now.ToString("dd/MM/yyyy HH:mm")
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,12 +10,20 @@
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center py-3">
|
||||
<h5 class="mb-0"><i class="bi bi-calendar-check me-2"></i>@ViewData["Title"]</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<a asp-action="ReporteAsistencia" class="btn btn-light btn-sm">
|
||||
<i class="bi bi-file-earmark-bar-graph me-1"></i> Reporte por Colaborador
|
||||
</a>
|
||||
<a asp-action="ImprimirAsistencia" asp-route-fecha="@selectedDate.ToString("yyyy-MM-dd")" class="btn btn-light btn-sm">
|
||||
<i class="bi bi-printer me-1"></i> Imprimir Hoy
|
||||
</a>
|
||||
<div class="d-flex align-items-center ms-2">
|
||||
<label class="me-2 mb-0">Fecha:</label>
|
||||
<input type="date" id="fechaAsistencia" class="form-control form-control-sm"
|
||||
value="@selectedDate.ToString("yyyy-MM-dd")" onchange="changeDate(this.value)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
@@ -38,7 +46,7 @@
|
||||
<div class="fw-bold">@item.Persona.Nombres @item.Persona.Apellidos</div>
|
||||
<small class="text-muted">@item.HorarioEntrada?.ToString(@"hh\:mm") - @item.HorarioSalida?.ToString(@"hh\:mm")</small>
|
||||
</td>
|
||||
<td><span class="badge bg-info text-dark">@item.Cargo</span></td>
|
||||
<td><span class="badge bg-info text-dark">@(item.Cargo?.Nombre ?? "Sin Cargo")</span></td>
|
||||
<td>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<input type="radio" class="btn-check" name="estado_@item.Id" id="pres_@item.Id" value="PRESENTE"
|
||||
@@ -74,6 +82,29 @@
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Configuración global de toastr
|
||||
toastr.options = {
|
||||
"closeButton": true,
|
||||
"progressBar": true,
|
||||
"positionClass": "toast-top-right",
|
||||
"timeOut": "3000"
|
||||
};
|
||||
|
||||
@if (TempData["InfoMessage"] != null)
|
||||
{
|
||||
<text>toastr.info('@TempData["InfoMessage"]');</text>
|
||||
}
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<text>toastr.success('@TempData["SuccessMessage"]');</text>
|
||||
}
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<text>toastr.error('@TempData["ErrorMessage"]');</text>
|
||||
}
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
@model IEnumerable<foundation_system.Models.AsistenciaColaborador>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Reporte de Asistencia por Colaborador";
|
||||
var colaboradores = (IEnumerable<foundation_system.Models.Colaborador>)ViewBag.Colaboradores;
|
||||
var inicio = (DateOnly)ViewBag.Inicio;
|
||||
var fin = (DateOnly)ViewBag.Fin;
|
||||
var selectedColaboradorId = (long?)ViewBag.ColaboradorId;
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="h3 mb-0 text-gray-800"><i class="bi bi-file-earmark-bar-graph me-2"></i>@ViewData["Title"]</h2>
|
||||
<div class="no-print">
|
||||
<button onclick="window.print()" class="btn btn-primary me-2">
|
||||
<i class="bi bi-printer me-1"></i> Imprimir
|
||||
</button>
|
||||
<a asp-action="Index" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4 no-print">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Filtros de Reporte</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Colaborador</label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" id="colaboradorId" name="colaboradorId" value="@selectedColaboradorId" />
|
||||
<input type="text" id="colaboradorNombre" class="form-control"
|
||||
value="@(selectedColaboradorId.HasValue && colaboradores.Any(c => c.Id == selectedColaboradorId) ?
|
||||
$"{colaboradores.First(c => c.Id == selectedColaboradorId).Persona.Apellidos}, {colaboradores.First(c => c.Id == selectedColaboradorId).Persona.Nombres}" : "")"
|
||||
readonly placeholder="Seleccione un colaborador..." required />
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#searchModal">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Desde</label>
|
||||
<input type="date" name="inicio" class="form-control" value="@inicio.ToString("yyyy-MM-dd")" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Hasta</label>
|
||||
<input type="date" name="fin" class="form-control" value="@fin.ToString("yyyy-MM-dd")" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-file-earmark-text me-2"></i>Generar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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)
|
||||
{
|
||||
<div class="mb-4 p-3 border rounded bg-light print-header">
|
||||
<h4 class="mb-2 text-primary">@selectedColaborador.Persona.Apellidos, @selectedColaborador.Persona.Nombres</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<strong><i class="bi bi-briefcase me-1"></i>Cargo:</strong> @(selectedColaborador.Cargo?.Nombre ?? "Sin Cargo")
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong><i class="bi bi-card-heading me-1"></i>DUI:</strong> @selectedColaborador.Persona.Dui
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong><i class="bi bi-telephone me-1"></i>Tel:</strong> @selectedColaborador.Persona.Telefono
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row mb-4 print-row">
|
||||
<div class="col-xl-3 col-md-6 mb-4 print-col">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Total Registros</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.Count()</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-calendar-check fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 mb-4 print-col">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Presentes</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@(stats.ContainsKey("PRESENTE") ? stats["PRESENTE"] : 0)</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-person-check fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 mb-4 print-col">
|
||||
<div class="card border-left-danger shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Ausentes</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@(stats.ContainsKey("AUSENTE") ? stats["AUSENTE"] : 0)</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-person-x fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 mb-4 print-col">
|
||||
<div class="card border-left-warning shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Tardanzas</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@(stats.ContainsKey("TARDANZA") ? stats["TARDANZA"] : 0)</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-clock-history fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Día</th>
|
||||
<th>Estado</th>
|
||||
<th>Observaciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Fecha.ToString("dd/MM/yyyy")</td>
|
||||
<td class="text-capitalize">@item.Fecha.ToString("dddd")</td>
|
||||
<td>
|
||||
<span class="badge @(item.Estado == "PRESENTE" ? "bg-success" : item.Estado == "AUSENTE" ? "bg-danger" : "bg-warning")">
|
||||
@item.Estado
|
||||
</span>
|
||||
</td>
|
||||
<td>@item.Observaciones</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-4">No se encontraron registros en este rango de fechas.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-search fa-4x text-gray-300 mb-3"></i>
|
||||
<p class="text-muted">Seleccione un colaborador y un rango de fechas para generar el reporte.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<div class="modal fade" id="searchModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Buscar Colaborador</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Escriba el nombre del colaborador..." autocomplete="off">
|
||||
<button class="btn btn-outline-primary" type="button" id="btnSearch">Buscar</button>
|
||||
</div>
|
||||
<div class="list-group" id="searchResults">
|
||||
<!-- Results will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
const searchModal = new bootstrap.Modal(document.getElementById('searchModal'));
|
||||
let debounceTimer;
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => performSearch(this.value), 300);
|
||||
});
|
||||
|
||||
document.getElementById('btnSearch').addEventListener('click', function() {
|
||||
performSearch(searchInput.value);
|
||||
});
|
||||
|
||||
function performSearch(term) {
|
||||
if (term.length < 2) {
|
||||
searchResults.innerHTML = '<div class="text-center text-muted p-3">Ingrese al menos 2 caracteres</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
searchResults.innerHTML = '<div class="text-center p-3"><div class="spinner-border text-primary" role="status"></div></div>';
|
||||
|
||||
fetch(`@Url.Action("SearchColaboradores")?term=${encodeURIComponent(term)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
searchResults.innerHTML = '';
|
||||
if (data.length === 0) {
|
||||
searchResults.innerHTML = '<div class="text-center text-muted p-3">No se encontraron resultados</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(item => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'list-group-item list-group-item-action';
|
||||
button.innerHTML = item.text; // The text already contains Name (Cargo)
|
||||
button.onclick = function() {
|
||||
selectColaborador(item.id, item.text);
|
||||
};
|
||||
searchResults.appendChild(button);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
searchResults.innerHTML = '<div class="text-center text-danger p-3">Error al buscar</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function selectColaborador(id, name) {
|
||||
// Extract just the name part if needed, but the full text is fine for display
|
||||
// Format from DB is: Name (Cargo)
|
||||
document.getElementById('colaboradorId').value = id;
|
||||
document.getElementById('colaboradorNombre').value = name;
|
||||
|
||||
// Close modal
|
||||
const modalEl = document.getElementById('searchModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
// Focus input when modal opens
|
||||
document.getElementById('searchModal').addEventListener('shown.bs.modal', function () {
|
||||
searchInput.focus();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
.border-left-primary { border-left: 0.25rem solid #4e73df !important; }
|
||||
.border-left-success { border-left: 0.25rem solid #1cc88a !important; }
|
||||
.border-left-danger { border-left: 0.25rem solid #e74a3b !important; }
|
||||
.border-left-warning { border-left: 0.25rem solid #f6c23e !important; }
|
||||
.text-xs { font-size: .7rem; }
|
||||
|
||||
@@media print {
|
||||
.no-print { display: none !important; }
|
||||
.sidebar, .top-header, .footer { display: none !important; }
|
||||
.main-content { margin-left: 0 !important; padding: 0 !important; }
|
||||
.page-container { padding: 0 !important; }
|
||||
.card { border: 1px solid #ddd !important; box-shadow: none !important; }
|
||||
.card-body { padding: 1rem !important; }
|
||||
body { background: white !important; }
|
||||
.container-fluid { width: 100% !important; padding: 0 !important; }
|
||||
|
||||
/* Force row layout for cards */
|
||||
.print-row {
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
gap: 10px;
|
||||
}
|
||||
.print-col {
|
||||
flex: 0 0 25% !important;
|
||||
max-width: 25% !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.print-header {
|
||||
background-color: #f8f9fa !important;
|
||||
border: 1px solid #dee2e6 !important;
|
||||
padding: 15px !important;
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
.print-header h4 {
|
||||
color: #000 !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
73
foundation_system/Views/Modulo/Create.cshtml
Normal file
73
foundation_system/Views/Modulo/Create.cshtml
Normal file
@@ -0,0 +1,73 @@
|
||||
@model foundation_system.Models.Modulo
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Nuevo Módulo";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary">@ViewData["Title"]</h6>
|
||||
<a asp-action="Index" class="btn btn-sm btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Create">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label asp-for="Nombre" class="form-label">Nombre del Módulo</label>
|
||||
<input asp-for="Nombre" class="form-control" placeholder="Ej. Administración, Reportes..." />
|
||||
<span asp-validation-for="Nombre" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label asp-for="Orden" class="form-label">Orden de Visualización</label>
|
||||
<input asp-for="Orden" class="form-control" type="number" />
|
||||
<span asp-validation-for="Orden" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label asp-for="Icono" class="form-label">Icono (Bootstrap Icons)</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i id="iconPreview" class="bi bi-question-circle"></i></span>
|
||||
<input asp-for="Icono" class="form-control" placeholder="bi-gear, bi-person..." oninput="updateIconPreview(this.value)" />
|
||||
</div>
|
||||
<small class="text-muted">Use nombres de <a href="https://icons.getbootstrap.com/" target="_blank">Bootstrap Icons</a></small>
|
||||
<span asp-validation-for="Icono" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label d-block">Estado</label>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input class="form-check-input" type="checkbox" asp-for="Activo">
|
||||
<label class="form-check-label" asp-for="Activo">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save me-2"></i>Guardar Módulo
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
<script>
|
||||
function updateIconPreview(iconName) {
|
||||
const preview = document.getElementById('iconPreview');
|
||||
preview.className = 'bi ' + (iconName || 'bi-question-circle');
|
||||
}
|
||||
</script>
|
||||
}
|
||||
75
foundation_system/Views/Modulo/Edit.cshtml
Normal file
75
foundation_system/Views/Modulo/Edit.cshtml
Normal file
@@ -0,0 +1,75 @@
|
||||
@model foundation_system.Models.Modulo
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Editar Módulo";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary">@ViewData["Title"]: @Model.Nombre</h6>
|
||||
<a asp-action="Index" class="btn btn-sm btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="Edit">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<input type="hidden" asp-for="CreadoEn" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label asp-for="Nombre" class="form-label">Nombre del Módulo</label>
|
||||
<input asp-for="Nombre" class="form-control" />
|
||||
<span asp-validation-for="Nombre" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label asp-for="Orden" class="form-label">Orden de Visualización</label>
|
||||
<input asp-for="Orden" class="form-control" type="number" />
|
||||
<span asp-validation-for="Orden" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mb-3">
|
||||
<label asp-for="Icono" class="form-label">Icono (Bootstrap Icons)</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i id="iconPreview" class="bi @(Model.Icono ?? "bi-question-circle")"></i></span>
|
||||
<input asp-for="Icono" class="form-control" oninput="updateIconPreview(this.value)" />
|
||||
</div>
|
||||
<small class="text-muted">Use nombres de <a href="https://icons.getbootstrap.com/" target="_blank">Bootstrap Icons</a></small>
|
||||
<span asp-validation-for="Icono" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label d-block">Estado</label>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input class="form-check-input" type="checkbox" asp-for="Activo">
|
||||
<label class="form-check-label" asp-for="Activo">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-end">
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save me-2"></i>Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
<script>
|
||||
function updateIconPreview(iconName) {
|
||||
const preview = document.getElementById('iconPreview');
|
||||
preview.className = 'bi ' + (iconName || 'bi-question-circle');
|
||||
}
|
||||
</script>
|
||||
}
|
||||
86
foundation_system/Views/Modulo/Index.cshtml
Normal file
86
foundation_system/Views/Modulo/Index.cshtml
Normal file
@@ -0,0 +1,86 @@
|
||||
@model IEnumerable<foundation_system.Models.Modulo>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Gestión de Módulos / Secciones";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="h3 mb-0 text-gray-800"><i class="bi bi-folder2-open me-2"></i>@ViewData["Title"]</h2>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-lg"></i> Nuevo Módulo
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i> @TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Orden</th>
|
||||
<th>Nombre</th>
|
||||
<th>Icono</th>
|
||||
<th class="text-center">Activo</th>
|
||||
<th class="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td><span class="badge bg-secondary">@item.Orden</span></td>
|
||||
<td class="fw-bold">@item.Nombre</td>
|
||||
<td><i class="bi @item.Icono me-2"></i>@item.Icono</td>
|
||||
<td class="text-center">
|
||||
@if (item.Activo)
|
||||
{
|
||||
<span class="text-success"><i class="bi bi-check-circle-fill"></i></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted"><i class="bi bi-x-circle"></i></span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-outline-primary" title="Editar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger" title="Eliminar"
|
||||
onclick="confirmDelete(@item.Id, '@item.Nombre')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="deleteForm" asp-action="Delete" method="post" style="display:none;">
|
||||
<input type="hidden" name="id" id="deleteId" />
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function confirmDelete(id, name) {
|
||||
if (confirm(`¿Está seguro de que desea eliminar el módulo "${name}"? Se eliminará solo si no tiene permisos asociados.`)) {
|
||||
document.getElementById('deleteId').value = id;
|
||||
document.getElementById('deleteForm').submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
@@ -17,9 +17,12 @@
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Modulo" class="form-label">Módulo (Sección)</label>
|
||||
<input asp-for="Modulo" class="form-control" placeholder="Ej: Gestión, Administración" />
|
||||
<span asp-validation-for="Modulo" class="text-danger small"></span>
|
||||
<label asp-for="ModuloId" class="form-label">Módulo (Sección)</label>
|
||||
<select asp-for="ModuloId" class="form-select"
|
||||
asp-items="@(new SelectList(ViewBag.Modulos, "Id", "Nombre"))">
|
||||
<option value="">-- Seleccione un Módulo --</option>
|
||||
</select>
|
||||
<span asp-validation-for="ModuloId" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -18,9 +18,12 @@
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Modulo" class="form-label">Módulo (Sección)</label>
|
||||
<input asp-for="Modulo" class="form-control" />
|
||||
<span asp-validation-for="Modulo" class="text-danger small"></span>
|
||||
<label asp-for="ModuloId" class="form-label">Módulo (Sección)</label>
|
||||
<select asp-for="ModuloId" class="form-select"
|
||||
asp-items="@(new SelectList(ViewBag.Modulos, "Id", "Nombre"))">
|
||||
<option value="">-- Seleccione un Módulo --</option>
|
||||
</select>
|
||||
<span asp-validation-for="ModuloId" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Orden</th>
|
||||
<th>Módulo</th>
|
||||
<th>Orden</th>
|
||||
<th>Nombre</th>
|
||||
<th>Ruta (URL)</th>
|
||||
<th>Icono</th>
|
||||
@@ -39,8 +39,13 @@
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge bg-info text-dark">
|
||||
<i class="bi @item.Modulo?.Icono me-1"></i>
|
||||
@(item.Modulo?.Nombre ?? "Sin Módulo")
|
||||
</span>
|
||||
</td>
|
||||
<td><span class="badge bg-secondary">@item.Orden</span></td>
|
||||
<td><span class="badge bg-info text-dark">@item.Modulo</span></td>
|
||||
<td class="fw-bold">@item.Nombre</td>
|
||||
<td class="font-monospace small">@item.Url</td>
|
||||
<td><i class="bi @item.Icono me-2"></i>@item.Icono</td>
|
||||
|
||||
@@ -12,7 +12,13 @@
|
||||
|
||||
@foreach (var group in groupedMenu)
|
||||
{
|
||||
<div class="nav-section-title mt-3">@group.Key</div>
|
||||
<div class="nav-section-title mt-3">
|
||||
@if (!string.IsNullOrEmpty(group.Key?.Icono))
|
||||
{
|
||||
<i class="bi @group.Key.Icono me-1"></i>
|
||||
}
|
||||
@(group.Key?.Nombre ?? "Sin Módulo")
|
||||
</div>
|
||||
@foreach (var item in group)
|
||||
{
|
||||
<a class="nav-link-custom @(currentController == item.Codigo ? "active" : "")" href="@item.Url">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--content-bg);
|
||||
color: var(--text-main);
|
||||
margin: 0;
|
||||
@@ -72,12 +72,15 @@ body {
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
padding: 0.5rem 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 1.25rem 1.5rem 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--sidebar-text-muted);
|
||||
letter-spacing: 0.05em;
|
||||
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 {
|
||||
@@ -160,7 +163,12 @@ body {
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user