Version Stable 001
This commit is contained in:
@@ -29,8 +29,9 @@ public class MenuViewComponent : ViewComponent
|
|||||||
if (isRoot)
|
if (isRoot)
|
||||||
{
|
{
|
||||||
menuItems = await _context.Permisos
|
menuItems = await _context.Permisos
|
||||||
|
.Include(p => p.Modulo)
|
||||||
.Where(p => p.EsMenu)
|
.Where(p => p.EsMenu)
|
||||||
.OrderBy(p => p.Modulo)
|
.OrderBy(p => p.Modulo!.Orden)
|
||||||
.ThenBy(p => p.Orden)
|
.ThenBy(p => p.Orden)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
@@ -40,8 +41,9 @@ public class MenuViewComponent : ViewComponent
|
|||||||
.Where(ru => ru.UsuarioId == userId)
|
.Where(ru => ru.UsuarioId == userId)
|
||||||
.Join(_context.RolesPermisos, ru => ru.RolId, rp => rp.RolId, (ru, rp) => rp)
|
.Join(_context.RolesPermisos, ru => ru.RolId, rp => rp.RolId, (ru, rp) => rp)
|
||||||
.Join(_context.Permisos, rp => rp.PermisoId, p => p.Id, (rp, p) => p)
|
.Join(_context.Permisos, rp => rp.PermisoId, p => p.Id, (rp, p) => p)
|
||||||
|
.Include(p => p.Modulo)
|
||||||
.Where(p => p.EsMenu)
|
.Where(p => p.EsMenu)
|
||||||
.OrderBy(p => p.Modulo)
|
.OrderBy(p => p.Modulo!.Orden)
|
||||||
.ThenBy(p => p.Orden)
|
.ThenBy(p => p.Orden)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ using foundation_system.Data;
|
|||||||
using foundation_system.Models;
|
using foundation_system.Models;
|
||||||
using foundation_system.Models.ViewModels;
|
using foundation_system.Models.ViewModels;
|
||||||
using foundation_system.Services;
|
using foundation_system.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace foundation_system.Controllers;
|
namespace foundation_system.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class AntecedentesController : Controller
|
public class AntecedentesController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using foundation_system.Data;
|
using foundation_system.Data;
|
||||||
using foundation_system.Models;
|
using foundation_system.Models;
|
||||||
using foundation_system.Models.ViewModels;
|
using foundation_system.Models.ViewModels;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace foundation_system.Controllers;
|
namespace foundation_system.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class AsistenciaController : Controller
|
public class AsistenciaController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
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)
|
public async Task<IActionResult> Index(DateOnly? fecha)
|
||||||
{
|
{
|
||||||
var selectedDate = fecha ?? DateOnly.FromDateTime(DateTime.Today);
|
var selectedDate = fecha ?? DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
if (fecha.HasValue)
|
||||||
|
{
|
||||||
|
TempData["InfoMessage"] = $"Mostrando asistencia del {selectedDate:dd/MM/yyyy}";
|
||||||
|
}
|
||||||
ViewBag.SelectedDate = selectedDate;
|
ViewBag.SelectedDate = selectedDate;
|
||||||
|
|
||||||
var colaboradores = await _context.Colaboradores
|
var colaboradores = await _context.Colaboradores
|
||||||
.Include(c => c.Persona)
|
.Include(c => c.Persona)
|
||||||
|
.Include(c => c.Cargo)
|
||||||
.Where(c => c.Activo)
|
.Where(c => c.Activo)
|
||||||
.OrderBy(c => c.Persona.Apellidos)
|
.OrderBy(c => c.Persona.Apellidos)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -66,4 +71,83 @@ public class ColaboradorAsistenciaController : Controller
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return Json(new { success = true });
|
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
|
var colaboradores = await _context.Colaboradores
|
||||||
.Include(c => c.Persona)
|
.Include(c => c.Persona)
|
||||||
|
.Include(c => c.Cargo)
|
||||||
.OrderBy(c => c.Persona.Apellidos)
|
.OrderBy(c => c.Persona.Apellidos)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return View(colaboradores);
|
return View(colaboradores);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: Colaborador/Create
|
// 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());
|
return View(new ColaboradorViewModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ public class ColaboradorController : Controller
|
|||||||
var colaborador = new Colaborador
|
var colaborador = new Colaborador
|
||||||
{
|
{
|
||||||
PersonaId = persona.Id,
|
PersonaId = persona.Id,
|
||||||
Cargo = model.Cargo,
|
CargoId = model.CargoId,
|
||||||
TipoColaborador = model.TipoColaborador,
|
TipoColaborador = model.TipoColaborador,
|
||||||
FechaIngreso = model.FechaIngreso,
|
FechaIngreso = model.FechaIngreso,
|
||||||
HorarioEntrada = model.HorarioEntrada,
|
HorarioEntrada = model.HorarioEntrada,
|
||||||
@@ -84,6 +86,7 @@ public class ColaboradorController : Controller
|
|||||||
ModelState.AddModelError("", "Ocurrió un error al guardar el colaborador.");
|
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);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +97,7 @@ public class ColaboradorController : Controller
|
|||||||
|
|
||||||
var colaborador = await _context.Colaboradores
|
var colaborador = await _context.Colaboradores
|
||||||
.Include(c => c.Persona)
|
.Include(c => c.Persona)
|
||||||
|
.Include(c => c.Cargo)
|
||||||
.FirstOrDefaultAsync(c => c.Id == id);
|
.FirstOrDefaultAsync(c => c.Id == id);
|
||||||
|
|
||||||
if (colaborador == null) return NotFound();
|
if (colaborador == null) return NotFound();
|
||||||
@@ -111,7 +115,7 @@ public class ColaboradorController : Controller
|
|||||||
Email = colaborador.Persona.Email,
|
Email = colaborador.Persona.Email,
|
||||||
Telefono = colaborador.Persona.Telefono,
|
Telefono = colaborador.Persona.Telefono,
|
||||||
Direccion = colaborador.Persona.Direccion,
|
Direccion = colaborador.Persona.Direccion,
|
||||||
Cargo = colaborador.Cargo,
|
CargoId = colaborador.CargoId,
|
||||||
TipoColaborador = colaborador.TipoColaborador,
|
TipoColaborador = colaborador.TipoColaborador,
|
||||||
FechaIngreso = colaborador.FechaIngreso,
|
FechaIngreso = colaborador.FechaIngreso,
|
||||||
HorarioEntrada = colaborador.HorarioEntrada,
|
HorarioEntrada = colaborador.HorarioEntrada,
|
||||||
@@ -119,6 +123,7 @@ public class ColaboradorController : Controller
|
|||||||
Activo = colaborador.Activo
|
Activo = colaborador.Activo
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ViewBag.Cargos = await _context.CargosColaboradores.Where(c => c.Activo).OrderBy(c => c.Nombre).ToListAsync();
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +157,7 @@ public class ColaboradorController : Controller
|
|||||||
colaborador.Persona.Direccion = model.Direccion;
|
colaborador.Persona.Direccion = model.Direccion;
|
||||||
|
|
||||||
// Update Colaborador
|
// Update Colaborador
|
||||||
colaborador.Cargo = model.Cargo;
|
colaborador.CargoId = model.CargoId;
|
||||||
colaborador.TipoColaborador = model.TipoColaborador;
|
colaborador.TipoColaborador = model.TipoColaborador;
|
||||||
colaborador.FechaIngreso = model.FechaIngreso;
|
colaborador.FechaIngreso = model.FechaIngreso;
|
||||||
colaborador.HorarioEntrada = model.HorarioEntrada;
|
colaborador.HorarioEntrada = model.HorarioEntrada;
|
||||||
@@ -172,6 +177,7 @@ public class ColaboradorController : Controller
|
|||||||
ModelState.AddModelError("", "Ocurrió un error al actualizar el colaborador.");
|
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);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
|
|
||||||
namespace foundation_system.Controllers;
|
namespace foundation_system.Controllers;
|
||||||
|
|
||||||
[Authorize(Roles = "ROOT,SUPERADMIN")]
|
[Authorize]
|
||||||
public class ConfiguracionController : Controller
|
public class ConfiguracionController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ using foundation_system.Data;
|
|||||||
using foundation_system.Models;
|
using foundation_system.Models;
|
||||||
using foundation_system.Models.ViewModels;
|
using foundation_system.Models.ViewModels;
|
||||||
using foundation_system.Services;
|
using foundation_system.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace foundation_system.Controllers;
|
namespace foundation_system.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class ExpedienteController : Controller
|
public class ExpedienteController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using foundation_system.Data;
|
using foundation_system.Data;
|
||||||
using foundation_system.Models;
|
using foundation_system.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace foundation_system.Controllers;
|
namespace foundation_system.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class HomeController : Controller
|
public class HomeController : Controller
|
||||||
{
|
{
|
||||||
private readonly ILogger<HomeController> _logger;
|
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;
|
namespace foundation_system.Controllers;
|
||||||
|
|
||||||
[Authorize(Roles = "ROOT")]
|
[Authorize]
|
||||||
public class PermisoController : Controller
|
public class PermisoController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
@@ -19,25 +19,32 @@ public class PermisoController : Controller
|
|||||||
// GET: Permiso
|
// GET: Permiso
|
||||||
public async Task<IActionResult> Index()
|
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
|
// GET: Permiso/Create
|
||||||
public IActionResult Create()
|
public async Task<IActionResult> Create()
|
||||||
{
|
{
|
||||||
|
ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync();
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: Permiso/Create
|
// POST: Permiso/Create
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[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 (ModelState.IsValid)
|
||||||
{
|
{
|
||||||
if (await _context.Permisos.AnyAsync(p => p.Codigo == permiso.Codigo))
|
if (await _context.Permisos.AnyAsync(p => p.Codigo == permiso.Codigo))
|
||||||
{
|
{
|
||||||
ModelState.AddModelError("Codigo", "El código ya existe.");
|
ModelState.AddModelError("Codigo", "El código ya existe.");
|
||||||
|
ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync();
|
||||||
return View(permiso);
|
return View(permiso);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +53,7 @@ public class PermisoController : Controller
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync();
|
||||||
return View(permiso);
|
return View(permiso);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,13 +64,15 @@ public class PermisoController : Controller
|
|||||||
|
|
||||||
var permiso = await _context.Permisos.FindAsync(id);
|
var permiso = await _context.Permisos.FindAsync(id);
|
||||||
if (permiso == null) return NotFound();
|
if (permiso == null) return NotFound();
|
||||||
|
|
||||||
|
ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync();
|
||||||
return View(permiso);
|
return View(permiso);
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: Permiso/Edit/5
|
// POST: Permiso/Edit/5
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[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();
|
if (id != permiso.Id) return NotFound();
|
||||||
|
|
||||||
@@ -80,6 +90,7 @@ public class PermisoController : Controller
|
|||||||
}
|
}
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
ViewBag.Modulos = await _context.Modulos.OrderBy(m => m.Orden).ToListAsync();
|
||||||
return View(permiso);
|
return View(permiso);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using foundation_system.Models;
|
|||||||
|
|
||||||
namespace foundation_system.Controllers;
|
namespace foundation_system.Controllers;
|
||||||
|
|
||||||
[Authorize(Roles = "ROOT")]
|
[Authorize]
|
||||||
public class RolController : Controller
|
public class RolController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
|
|
||||||
namespace foundation_system.Controllers;
|
namespace foundation_system.Controllers;
|
||||||
|
|
||||||
[Authorize(Roles = "ROOT,SUPERADMIN")]
|
[Authorize]
|
||||||
public class UsuarioController : Controller
|
public class UsuarioController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ public class ApplicationDbContext : DbContext
|
|||||||
public DbSet<RolSistema> RolesSistema { get; set; }
|
public DbSet<RolSistema> RolesSistema { get; set; }
|
||||||
public DbSet<RolUsuario> RolesUsuario { get; set; }
|
public DbSet<RolUsuario> RolesUsuario { get; set; }
|
||||||
public DbSet<Permiso> Permisos { get; set; }
|
public DbSet<Permiso> Permisos { get; set; }
|
||||||
|
public DbSet<Modulo> Modulos { get; set; }
|
||||||
public DbSet<RolPermiso> RolesPermisos { get; set; }
|
public DbSet<RolPermiso> RolesPermisos { get; set; }
|
||||||
public DbSet<Colaborador> Colaboradores { get; set; }
|
public DbSet<Colaborador> Colaboradores { get; set; }
|
||||||
|
public DbSet<CargoColaborador> CargosColaboradores { get; set; }
|
||||||
public DbSet<AsistenciaColaborador> AsistenciasColaboradores { get; set; }
|
public DbSet<AsistenciaColaborador> AsistenciasColaboradores { get; set; }
|
||||||
public DbSet<Nino> Ninos { get; set; }
|
public DbSet<Nino> Ninos { get; set; }
|
||||||
public DbSet<Asistencia> Asistencias { get; set; }
|
public DbSet<Asistencia> Asistencias { get; set; }
|
||||||
@@ -57,11 +59,21 @@ public class ApplicationDbContext : DbContext
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(rp => rp.PermisoId);
|
.HasForeignKey(rp => rp.PermisoId);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Permiso>()
|
||||||
|
.HasOne(p => p.Modulo)
|
||||||
|
.WithMany(m => m.Permisos)
|
||||||
|
.HasForeignKey(p => p.ModuloId);
|
||||||
|
|
||||||
modelBuilder.Entity<Usuario>()
|
modelBuilder.Entity<Usuario>()
|
||||||
.HasOne(u => u.Persona)
|
.HasOne(u => u.Persona)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(u => u.PersonaId);
|
.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
|
// Global configuration: Convert all dates to UTC when saving
|
||||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
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")]
|
[ForeignKey("PersonaId")]
|
||||||
public virtual Persona Persona { get; set; } = null!;
|
public virtual Persona Persona { get; set; } = null!;
|
||||||
|
|
||||||
|
[Column("cargo_id")]
|
||||||
[Required]
|
[Required]
|
||||||
[MaxLength(100)]
|
public long CargoId { get; set; }
|
||||||
[Column("cargo")]
|
|
||||||
public string Cargo { get; set; } = string.Empty;
|
[ForeignKey("CargoId")]
|
||||||
|
public virtual CargoColaborador? Cargo { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[MaxLength(50)]
|
[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")]
|
[Column("id")]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
[Column("modulo")]
|
[Column("modulo_id")]
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(50)]
|
public int ModuloId { get; set; }
|
||||||
public string Modulo { get; set; } = string.Empty;
|
|
||||||
|
[ForeignKey("ModuloId")]
|
||||||
|
public virtual Modulo? Modulo { get; set; }
|
||||||
|
|
||||||
[Column("codigo")]
|
[Column("codigo")]
|
||||||
[Required]
|
[Required]
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public class ColaboradorViewModel
|
|||||||
|
|
||||||
[Required(ErrorMessage = "El cargo es requerido")]
|
[Required(ErrorMessage = "El cargo es requerido")]
|
||||||
[Display(Name = "Cargo")]
|
[Display(Name = "Cargo")]
|
||||||
public string Cargo { get; set; } = string.Empty;
|
public long CargoId { get; set; }
|
||||||
|
|
||||||
[Required(ErrorMessage = "El tipo de colaborador es requerido")]
|
[Required(ErrorMessage = "El tipo de colaborador es requerido")]
|
||||||
[Display(Name = "Tipo de Colaborador")]
|
[Display(Name = "Tipo de Colaborador")]
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ builder.Services.AddControllersWithViews(options =>
|
|||||||
.RequireAuthenticatedUser()
|
.RequireAuthenticatedUser()
|
||||||
.Build();
|
.Build();
|
||||||
options.Filters.Add(new Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter(policy));
|
options.Filters.Add(new Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter(policy));
|
||||||
|
options.Filters.Add(new foundation_system.Filters.DynamicAuthorizationFilter());
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
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>
|
<h6 class="text-primary border-bottom pb-2 mb-3">Información Laboral</h6>
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="Cargo" class="form-label"></label>
|
<label asp-for="CargoId" class="form-label"></label>
|
||||||
<input asp-for="Cargo" class="form-control" placeholder="Ej: Maestro, Cocinero, Administrador" />
|
<select asp-for="CargoId" class="form-select" asp-items="@(new SelectList(ViewBag.Cargos, "Id", "Nombre"))">
|
||||||
<span asp-validation-for="Cargo" class="text-danger small"></span>
|
<option value="">Seleccione un cargo...</option>
|
||||||
|
</select>
|
||||||
|
<span asp-validation-for="CargoId" class="text-danger small"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="TipoColaborador" class="form-label"></label>
|
<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>
|
<h6 class="text-primary border-bottom pb-2 mb-3">Información Laboral</h6>
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="Cargo" class="form-label"></label>
|
<label asp-for="CargoId" class="form-label"></label>
|
||||||
<input asp-for="Cargo" class="form-control" />
|
<select asp-for="CargoId" class="form-select" asp-items="@(new SelectList(ViewBag.Cargos, "Id", "Nombre"))">
|
||||||
<span asp-validation-for="Cargo" class="text-danger small"></span>
|
<option value="">Seleccione un cargo...</option>
|
||||||
|
</select>
|
||||||
|
<span asp-validation-for="CargoId" class="text-danger small"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="TipoColaborador" class="form-label"></label>
|
<label asp-for="TipoColaborador" class="form-label"></label>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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.TipoColaborador</td>
|
||||||
<td>@item.Persona.Dui</td>
|
<td>@item.Persona.Dui</td>
|
||||||
<td>@item.Persona.Telefono</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 shadow mb-4">
|
||||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center py-3">
|
<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>
|
<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>
|
<label class="me-2 mb-0">Fecha:</label>
|
||||||
<input type="date" id="fechaAsistencia" class="form-control form-control-sm"
|
<input type="date" id="fechaAsistencia" class="form-control form-control-sm"
|
||||||
value="@selectedDate.ToString("yyyy-MM-dd")" onchange="changeDate(this.value)" />
|
value="@selectedDate.ToString("yyyy-MM-dd")" onchange="changeDate(this.value)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
@@ -38,7 +46,7 @@
|
|||||||
<div class="fw-bold">@item.Persona.Nombres @item.Persona.Apellidos</div>
|
<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>
|
<small class="text-muted">@item.HorarioEntrada?.ToString(@"hh\:mm") - @item.HorarioSalida?.ToString(@"hh\:mm")</small>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<div class="btn-group w-100" role="group">
|
<div class="btn-group w-100" role="group">
|
||||||
<input type="radio" class="btn-check" name="estado_@item.Id" id="pres_@item.Id" value="PRESENTE"
|
<input type="radio" class="btn-check" name="estado_@item.Id" id="pres_@item.Id" value="PRESENTE"
|
||||||
@@ -74,6 +82,29 @@
|
|||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<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) {
|
function changeDate(fecha) {
|
||||||
window.location.href = '@Url.Action("Index")?fecha=' + fecha;
|
window.location.href = '@Url.Action("Index")?fecha=' + fecha;
|
||||||
}
|
}
|
||||||
@@ -105,9 +136,14 @@
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Opcional: Mostrar feedback visual sutil
|
toastr.success('Asistencia guardada correctamente');
|
||||||
console.log('Guardado');
|
} else {
|
||||||
|
toastr.error('Error al guardar la asistencia');
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
toastr.error('Ocurrió un error al procesar la solicitud');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</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="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="Modulo" class="form-label">Módulo (Sección)</label>
|
<label asp-for="ModuloId" class="form-label">Módulo (Sección)</label>
|
||||||
<input asp-for="Modulo" class="form-control" placeholder="Ej: Gestión, Administración" />
|
<select asp-for="ModuloId" class="form-select"
|
||||||
<span asp-validation-for="Modulo" class="text-danger small"></span>
|
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>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
@@ -18,9 +18,12 @@
|
|||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label asp-for="Modulo" class="form-label">Módulo (Sección)</label>
|
<label asp-for="ModuloId" class="form-label">Módulo (Sección)</label>
|
||||||
<input asp-for="Modulo" class="form-control" />
|
<select asp-for="ModuloId" class="form-select"
|
||||||
<span asp-validation-for="Modulo" class="text-danger small"></span>
|
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>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
<table class="table table-hover align-middle">
|
<table class="table table-hover align-middle">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Orden</th>
|
|
||||||
<th>Módulo</th>
|
<th>Módulo</th>
|
||||||
|
<th>Orden</th>
|
||||||
<th>Nombre</th>
|
<th>Nombre</th>
|
||||||
<th>Ruta (URL)</th>
|
<th>Ruta (URL)</th>
|
||||||
<th>Icono</th>
|
<th>Icono</th>
|
||||||
@@ -39,8 +39,13 @@
|
|||||||
@foreach (var item in Model)
|
@foreach (var item in Model)
|
||||||
{
|
{
|
||||||
<tr>
|
<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-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="fw-bold">@item.Nombre</td>
|
||||||
<td class="font-monospace small">@item.Url</td>
|
<td class="font-monospace small">@item.Url</td>
|
||||||
<td><i class="bi @item.Icono me-2"></i>@item.Icono</td>
|
<td><i class="bi @item.Icono me-2"></i>@item.Icono</td>
|
||||||
|
|||||||
@@ -12,7 +12,13 @@
|
|||||||
|
|
||||||
@foreach (var group in groupedMenu)
|
@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)
|
@foreach (var item in group)
|
||||||
{
|
{
|
||||||
<a class="nav-link-custom @(currentController == item.Codigo ? "active" : "")" href="@item.Url">
|
<a class="nav-link-custom @(currentController == item.Codigo ? "active" : "")" href="@item.Url">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
||||||
background-color: var(--content-bg);
|
background-color: var(--content-bg);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -72,12 +72,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-section-title {
|
.nav-section-title {
|
||||||
padding: 0.5rem 1.5rem;
|
padding: 1.25rem 1.5rem 0.5rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--sidebar-text-muted);
|
color: white;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.08em;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link-custom {
|
.nav-link-custom {
|
||||||
@@ -160,7 +163,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user