Permisos y roles

This commit is contained in:
2025-12-30 23:30:13 -06:00
parent cc28fa00e8
commit 4ab3e9e756
38 changed files with 2552 additions and 36 deletions

View File

@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using foundation_system.Data;
using System.Security.Claims;
namespace foundation_system.Components;
public class MenuViewComponent : ViewComponent
{
private readonly ApplicationDbContext _context;
public MenuViewComponent(ApplicationDbContext context)
{
_context = context;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var userIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim == null || !long.TryParse(userIdClaim.Value, out var userId))
{
return View(new List<foundation_system.Models.Permiso>());
}
var isRoot = HttpContext.User.IsInRole("ROOT");
List<foundation_system.Models.Permiso> menuItems;
if (isRoot)
{
menuItems = await _context.Permisos
.Where(p => p.EsMenu)
.OrderBy(p => p.Modulo)
.ThenBy(p => p.Orden)
.ToListAsync();
}
else
{
menuItems = await _context.RolesUsuario
.Where(ru => ru.UsuarioId == userId)
.Join(_context.RolesPermisos, ru => ru.RolId, rp => rp.RolId, (ru, rp) => rp)
.Join(_context.Permisos, rp => rp.PermisoId, p => p.Id, (rp, p) => p)
.Where(p => p.EsMenu)
.OrderBy(p => p.Modulo)
.ThenBy(p => p.Orden)
.Distinct()
.ToListAsync();
}
return View(menuItems);
}
}

View File

@@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using foundation_system.Models.ViewModels; using foundation_system.Models.ViewModels;
using foundation_system.Services; using foundation_system.Services;
using foundation_system.Data;
using Microsoft.EntityFrameworkCore;
namespace foundation_system.Controllers; namespace foundation_system.Controllers;
@@ -12,11 +14,13 @@ public class AccountController : Controller
{ {
private readonly IAuthService _authService; private readonly IAuthService _authService;
private readonly ILogger<AccountController> _logger; private readonly ILogger<AccountController> _logger;
private readonly ApplicationDbContext _context;
public AccountController(IAuthService authService, ILogger<AccountController> logger) public AccountController(IAuthService authService, ILogger<AccountController> logger, ApplicationDbContext context)
{ {
_authService = authService; _authService = authService;
_logger = logger; _logger = logger;
_context = context;
} }
// GET: /Account/Login // GET: /Account/Login
@@ -72,6 +76,20 @@ public class AccountController : Controller
claims.Add(new Claim(ClaimTypes.Role, role)); claims.Add(new Claim(ClaimTypes.Role, role));
} }
// Add permissions as claims
var permissions = await _context.RolesUsuario
.Where(ru => ru.UsuarioId == usuario.Id) // Changed user.Id to usuario.Id
.Join(_context.RolesPermisos, ru => ru.RolId, rp => rp.RolId, (ru, rp) => rp)
.Join(_context.Permisos, rp => rp.PermisoId, p => p.Id, (rp, p) => p)
.Select(p => p.Codigo)
.Distinct()
.ToListAsync();
foreach (var permission in permissions)
{
claims.Add(new Claim("Permission", permission));
}
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties var authProperties = new AuthenticationProperties

View File

@@ -209,6 +209,102 @@ public class AsistenciaController : Controller
return Json(new { success = true, message = "Cambios guardados correctamente" }); return Json(new { success = true, message = "Cambios guardados correctamente" });
} }
[HttpGet]
public async Task<IActionResult> ReporteMensual(int? año, int? mes, string? diasSemana)
{
int targetAnio = año ?? DateTime.Now.Year;
int targetMes = mes ?? DateTime.Now.Month;
var firstDayOfMonth = new DateTime(targetAnio, targetMes, 1);
var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1);
var ninos = await _context.Ninos
.Include(n => n.Persona)
.Where(n => n.Activo && n.Estado == "ACTIVO")
.OrderBy(n => n.Persona.Nombres)
.ToListAsync();
var firstDateOnly = DateOnly.FromDateTime(firstDayOfMonth);
var lastDateOnly = DateOnly.FromDateTime(lastDayOfMonth);
var asistencias = await _context.Asistencias
.Where(a => a.Fecha >= firstDateOnly && a.Fecha <= lastDateOnly)
.ToListAsync();
var viewModel = new AsistenciaGridViewModel
{
Año = targetAnio,
Mes = targetMes,
NombreMes = firstDayOfMonth.ToString("MMMM", new System.Globalization.CultureInfo("es-ES")).ToUpper(),
DiasSemanaSeleccionados = diasSemana ?? "",
DiasDelMes = Enumerable.Range(0, lastDayOfMonth.Day)
.Select(day => firstDayOfMonth.AddDays(day))
.ToList(),
Expedientes = ninos,
Asistencias = asistencias.ToDictionary(
a => $"{a.NinoId}_{a.Fecha:yyyy-MM-dd}",
a => a.Estado switch {
"PRESENTE" => "P",
"TARDE" => "T",
"AUSENTE" => "F",
"JUSTIFICADO" => "J",
"ENFERMO" => "E",
_ => ""
}
)
};
return View(viewModel);
}
[HttpGet]
public async Task<IActionResult> ReporteIndividual(long ninoId, int? año, int? mes, string? diasSemana)
{
int targetAnio = año ?? DateTime.Now.Year;
int targetMes = mes ?? DateTime.Now.Month;
var firstDayOfMonth = new DateTime(targetAnio, targetMes, 1);
var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1);
var nino = await _context.Ninos
.Include(n => n.Persona)
.FirstOrDefaultAsync(n => n.Id == ninoId);
if (nino == null) return NotFound();
var firstDateOnly = DateOnly.FromDateTime(firstDayOfMonth);
var lastDateOnly = DateOnly.FromDateTime(lastDayOfMonth);
var asistencias = await _context.Asistencias
.Where(a => a.NinoId == ninoId && a.Fecha >= firstDateOnly && a.Fecha <= lastDateOnly)
.ToListAsync();
var viewModel = new AsistenciaGridViewModel
{
Año = targetAnio,
Mes = targetMes,
NombreMes = firstDayOfMonth.ToString("MMMM", new System.Globalization.CultureInfo("es-ES")).ToUpper(),
DiasSemanaSeleccionados = diasSemana ?? "",
DiasDelMes = Enumerable.Range(0, lastDayOfMonth.Day)
.Select(day => firstDayOfMonth.AddDays(day))
.ToList(),
Expedientes = new List<Nino> { nino },
Asistencias = asistencias.ToDictionary(
a => $"{a.NinoId}_{a.Fecha:yyyy-MM-dd}",
a => a.Estado switch {
"PRESENTE" => "P",
"TARDE" => "T",
"AUSENTE" => "F",
"JUSTIFICADO" => "J",
"ENFERMO" => "E",
_ => ""
}
)
};
return View(viewModel);
}
[HttpGet] [HttpGet]
public IActionResult ExportarExcel(int año, int mes, string diasSemana) public IActionResult ExportarExcel(int año, int mes, string diasSemana)
{ {

View File

@@ -0,0 +1,69 @@
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 ColaboradorAsistenciaController : Controller
{
private readonly ApplicationDbContext _context;
public ColaboradorAsistenciaController(ApplicationDbContext context)
{
_context = context;
}
// GET: ColaboradorAsistencia
public async Task<IActionResult> Index(DateOnly? fecha)
{
var selectedDate = fecha ?? DateOnly.FromDateTime(DateTime.Today);
ViewBag.SelectedDate = selectedDate;
var colaboradores = await _context.Colaboradores
.Include(c => c.Persona)
.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);
}
// POST: ColaboradorAsistencia/Save
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Save(long colaboradorId, DateOnly fecha, string estado, string? observaciones)
{
var asistencia = await _context.AsistenciasColaboradores
.FirstOrDefaultAsync(a => a.ColaboradorId == colaboradorId && a.Fecha == fecha);
if (asistencia == null)
{
asistencia = new AsistenciaColaborador
{
ColaboradorId = colaboradorId,
Fecha = fecha,
Estado = estado,
Observaciones = observaciones,
CreadoEn = DateTime.UtcNow
};
_context.AsistenciasColaboradores.Add(asistencia);
}
else
{
asistencia.Estado = estado;
asistencia.Observaciones = observaciones;
}
await _context.SaveChangesAsync();
return Json(new { success = true });
}
}

View File

@@ -0,0 +1,192 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using foundation_system.Data;
using foundation_system.Models;
using foundation_system.Models.ViewModels;
namespace foundation_system.Controllers;
[Authorize]
public class ColaboradorController : Controller
{
private readonly ApplicationDbContext _context;
public ColaboradorController(ApplicationDbContext context)
{
_context = context;
}
// GET: Colaborador
public async Task<IActionResult> Index()
{
var colaboradores = await _context.Colaboradores
.Include(c => c.Persona)
.OrderBy(c => c.Persona.Apellidos)
.ToListAsync();
return View(colaboradores);
}
// GET: Colaborador/Create
public IActionResult Create()
{
return View(new ColaboradorViewModel());
}
// POST: Colaborador/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(ColaboradorViewModel model)
{
if (ModelState.IsValid)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var persona = new Persona
{
Nombres = model.Nombres,
Apellidos = model.Apellidos,
Dui = model.Dui,
Nit = model.Nit,
FechaNacimiento = model.FechaNacimiento,
Genero = model.Genero,
Email = model.Email,
Telefono = model.Telefono,
Direccion = model.Direccion,
Activo = true
};
_context.Personas.Add(persona);
await _context.SaveChangesAsync();
var colaborador = new Colaborador
{
PersonaId = persona.Id,
Cargo = model.Cargo,
TipoColaborador = model.TipoColaborador,
FechaIngreso = model.FechaIngreso,
HorarioEntrada = model.HorarioEntrada,
HorarioSalida = model.HorarioSalida,
Activo = true
};
_context.Colaboradores.Add(colaborador);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
TempData["SuccessMessage"] = "Colaborador creado exitosamente.";
return RedirectToAction(nameof(Index));
}
catch (Exception)
{
await transaction.RollbackAsync();
ModelState.AddModelError("", "Ocurrió un error al guardar el colaborador.");
}
}
return View(model);
}
// GET: Colaborador/Edit/5
public async Task<IActionResult> Edit(long? id)
{
if (id == null) return NotFound();
var colaborador = await _context.Colaboradores
.Include(c => c.Persona)
.FirstOrDefaultAsync(c => c.Id == id);
if (colaborador == null) return NotFound();
var model = new ColaboradorViewModel
{
Id = colaborador.Id,
PersonaId = colaborador.PersonaId,
Nombres = colaborador.Persona.Nombres,
Apellidos = colaborador.Persona.Apellidos,
Dui = colaborador.Persona.Dui,
Nit = colaborador.Persona.Nit,
FechaNacimiento = colaborador.Persona.FechaNacimiento,
Genero = colaborador.Persona.Genero,
Email = colaborador.Persona.Email,
Telefono = colaborador.Persona.Telefono,
Direccion = colaborador.Persona.Direccion,
Cargo = colaborador.Cargo,
TipoColaborador = colaborador.TipoColaborador,
FechaIngreso = colaborador.FechaIngreso,
HorarioEntrada = colaborador.HorarioEntrada,
HorarioSalida = colaborador.HorarioSalida,
Activo = colaborador.Activo
};
return View(model);
}
// POST: Colaborador/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(long id, ColaboradorViewModel model)
{
if (id != model.Id) return NotFound();
if (ModelState.IsValid)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var colaborador = await _context.Colaboradores
.Include(c => c.Persona)
.FirstOrDefaultAsync(c => c.Id == id);
if (colaborador == null) return NotFound();
// Update Persona
colaborador.Persona.Nombres = model.Nombres;
colaborador.Persona.Apellidos = model.Apellidos;
colaborador.Persona.Dui = model.Dui;
colaborador.Persona.Nit = model.Nit;
colaborador.Persona.FechaNacimiento = model.FechaNacimiento;
colaborador.Persona.Genero = model.Genero;
colaborador.Persona.Email = model.Email;
colaborador.Persona.Telefono = model.Telefono;
colaborador.Persona.Direccion = model.Direccion;
// Update Colaborador
colaborador.Cargo = model.Cargo;
colaborador.TipoColaborador = model.TipoColaborador;
colaborador.FechaIngreso = model.FechaIngreso;
colaborador.HorarioEntrada = model.HorarioEntrada;
colaborador.HorarioSalida = model.HorarioSalida;
colaborador.Activo = model.Activo;
colaborador.ActualizadoEn = DateTime.UtcNow;
await _context.SaveChangesAsync();
await transaction.CommitAsync();
TempData["SuccessMessage"] = "Colaborador actualizado exitosamente.";
return RedirectToAction(nameof(Index));
}
catch (Exception)
{
await transaction.RollbackAsync();
ModelState.AddModelError("", "Ocurrió un error al actualizar el colaborador.");
}
}
return View(model);
}
// POST: Colaborador/Delete/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(long id)
{
var colaborador = await _context.Colaboradores.FindAsync(id);
if (colaborador != null)
{
colaborador.Activo = false; // Soft delete
await _context.SaveChangesAsync();
TempData["SuccessMessage"] = "Colaborador desactivado exitosamente.";
}
return RedirectToAction(nameof(Index));
}
}

View File

@@ -3,8 +3,11 @@ 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(Roles = "ROOT,SUPERADMIN")]
public class ConfiguracionController : Controller public class ConfiguracionController : Controller
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;

View File

@@ -0,0 +1,111 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using foundation_system.Data;
using foundation_system.Models;
namespace foundation_system.Controllers;
[Authorize(Roles = "ROOT")]
public class PermisoController : Controller
{
private readonly ApplicationDbContext _context;
public PermisoController(ApplicationDbContext context)
{
_context = context;
}
// GET: Permiso
public async Task<IActionResult> Index()
{
return View(await _context.Permisos.OrderBy(p => p.Modulo).ThenBy(p => p.Orden).ToListAsync());
}
// GET: Permiso/Create
public IActionResult Create()
{
return View();
}
// POST: Permiso/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Modulo,Codigo,Nombre,Descripcion,Url,Icono,Orden,EsMenu")] Permiso permiso)
{
if (ModelState.IsValid)
{
if (await _context.Permisos.AnyAsync(p => p.Codigo == permiso.Codigo))
{
ModelState.AddModelError("Codigo", "El código ya existe.");
return View(permiso);
}
permiso.CreadoEn = DateTime.UtcNow;
_context.Add(permiso);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(permiso);
}
// GET: Permiso/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null) return NotFound();
var permiso = await _context.Permisos.FindAsync(id);
if (permiso == null) return NotFound();
return View(permiso);
}
// POST: Permiso/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Modulo,Codigo,Nombre,Descripcion,Url,Icono,Orden,EsMenu")] Permiso permiso)
{
if (id != permiso.Id) return NotFound();
if (ModelState.IsValid)
{
try
{
_context.Update(permiso);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!PermisoExists(permiso.Id)) return NotFound();
else throw;
}
return RedirectToAction(nameof(Index));
}
return View(permiso);
}
// POST: Permiso/Delete/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
var permiso = await _context.Permisos.FindAsync(id);
if (permiso != null)
{
var isUsed = await _context.RolesPermisos.AnyAsync(rp => rp.PermisoId == id);
if (isUsed)
{
TempData["ErrorMessage"] = "No se puede eliminar porque está asignado a roles.";
return RedirectToAction(nameof(Index));
}
_context.Permisos.Remove(permiso);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
private bool PermisoExists(int id)
{
return _context.Permisos.Any(e => e.Id == id);
}
}

View File

@@ -0,0 +1,192 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using foundation_system.Data;
using foundation_system.Models;
namespace foundation_system.Controllers;
[Authorize(Roles = "ROOT")]
public class RolController : Controller
{
private readonly ApplicationDbContext _context;
public RolController(ApplicationDbContext context)
{
_context = context;
}
// GET: Rol
public async Task<IActionResult> Index()
{
return View(await _context.RolesSistema
.Include(r => r.RolesPermisos)
.OrderBy(r => r.Nombre)
.ToListAsync());
}
// GET: Rol/Create
public IActionResult Create()
{
return View();
}
// POST: Rol/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Codigo,Nombre,Descripcion")] RolSistema rol)
{
if (ModelState.IsValid)
{
if (await _context.RolesSistema.AnyAsync(r => r.Codigo == rol.Codigo))
{
ModelState.AddModelError("Codigo", "El código de rol ya existe.");
return View(rol);
}
rol.CreadoEn = DateTime.UtcNow;
_context.Add(rol);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(rol);
}
// GET: Rol/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null) return NotFound();
var rol = await _context.RolesSistema.FindAsync(id);
if (rol == null) return NotFound();
return View(rol);
}
// POST: Rol/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Codigo,Nombre,Descripcion")] RolSistema rol)
{
if (id != rol.Id) return NotFound();
if (ModelState.IsValid)
{
try
{
_context.Update(rol);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!RolExists(rol.Id)) return NotFound();
else throw;
}
return RedirectToAction(nameof(Index));
}
return View(rol);
}
// GET: Rol/Permissions/5
public async Task<IActionResult> Permissions(int? id)
{
if (id == null) return NotFound();
var rol = await _context.RolesSistema
.Include(r => r.RolesPermisos)
.ThenInclude(rp => rp.Permiso)
.FirstOrDefaultAsync(r => r.Id == id);
if (rol == null) return NotFound();
// Fetch all permissions from DB
var permissions = await _context.Permisos
.OrderBy(p => p.Modulo)
.ThenBy(p => p.Orden)
.ToListAsync();
ViewBag.Rol = rol;
ViewBag.AssignedControllerCodes = rol.RolesPermisos.Select(rp => rp.Permiso.Codigo).ToList();
return View(permissions);
}
// POST: Rol/UpdatePermissions
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdatePermissions(int rolId, string[] selectedControllers)
{
var rol = await _context.RolesSistema
.Include(r => r.RolesPermisos)
.FirstOrDefaultAsync(r => r.Id == rolId);
if (rol == null) return NotFound();
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Remove existing permissions
_context.RolesPermisos.RemoveRange(rol.RolesPermisos);
await _context.SaveChangesAsync();
// Add new permissions
if (selectedControllers != null)
{
foreach (var controllerCode in selectedControllers)
{
var permiso = await _context.Permisos.FirstOrDefaultAsync(p => p.Codigo == controllerCode);
if (permiso != null)
{
_context.RolesPermisos.Add(new RolPermiso
{
RolId = rolId,
PermisoId = permiso.Id,
AsignadoEn = DateTime.UtcNow
});
}
}
}
await _context.SaveChangesAsync();
await transaction.CommitAsync();
TempData["SuccessMessage"] = "Permisos actualizados correctamente.";
}
catch (Exception)
{
await transaction.RollbackAsync();
TempData["ErrorMessage"] = "Ocurrió un error al actualizar los permisos.";
}
return RedirectToAction(nameof(Index));
}
// POST: Rol/Delete/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
var rol = await _context.RolesSistema.FindAsync(id);
if (rol != null)
{
// Check if it's being used by users
var isUsed = await _context.RolesUsuario.AnyAsync(ru => ru.RolId == id);
if (isUsed)
{
TempData["ErrorMessage"] = "No se puede eliminar el rol porque está asignado a uno o más usuarios.";
return RedirectToAction(nameof(Index));
}
// Remove permissions first
var permissions = await _context.RolesPermisos.Where(rp => rp.RolId == id).ToListAsync();
_context.RolesPermisos.RemoveRange(permissions);
_context.RolesSistema.Remove(rol);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
private bool RolExists(int id)
{
return _context.RolesSistema.Any(e => e.Id == id);
}
}

View File

@@ -5,8 +5,11 @@ using foundation_system.Models;
using foundation_system.Models.ViewModels; using foundation_system.Models.ViewModels;
using BCrypt.Net; using BCrypt.Net;
using Microsoft.AspNetCore.Authorization;
namespace foundation_system.Controllers; namespace foundation_system.Controllers;
[Authorize(Roles = "ROOT,SUPERADMIN")]
public class UsuarioController : Controller public class UsuarioController : Controller
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
@@ -21,13 +24,16 @@ public class UsuarioController : Controller
{ {
var usuarios = await _context.Usuarios var usuarios = await _context.Usuarios
.Include(u => u.Persona) .Include(u => u.Persona)
.Include(u => u.RolesUsuario)
.ThenInclude(ru => ru.Rol)
.ToListAsync(); .ToListAsync();
return View(usuarios); return View(usuarios);
} }
// GET: Usuario/Create // GET: Usuario/Create
public IActionResult Create() public async Task<IActionResult> Create()
{ {
ViewBag.Roles = await _context.RolesSistema.ToListAsync();
return View(new UsuarioViewModel()); return View(new UsuarioViewModel());
} }
@@ -43,16 +49,17 @@ public class UsuarioController : Controller
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
// Check if username or email already exists
if (await _context.Usuarios.AnyAsync(u => u.NombreUsuario == model.NombreUsuario)) if (await _context.Usuarios.AnyAsync(u => u.NombreUsuario == model.NombreUsuario))
{ {
ModelState.AddModelError("NombreUsuario", "El nombre de usuario ya está en uso"); ModelState.AddModelError("NombreUsuario", "El nombre de usuario ya está en uso");
ViewBag.Roles = await _context.RolesSistema.ToListAsync();
return View(model); return View(model);
} }
if (await _context.Usuarios.AnyAsync(u => u.Email == model.Email)) if (await _context.Usuarios.AnyAsync(u => u.Email == model.Email))
{ {
ModelState.AddModelError("Email", "El correo electrónico ya está en uso"); ModelState.AddModelError("Email", "El correo electrónico ya está en uso");
ViewBag.Roles = await _context.RolesSistema.ToListAsync();
return View(model); return View(model);
} }
@@ -85,6 +92,21 @@ public class UsuarioController : Controller
_context.Usuarios.Add(usuario); _context.Usuarios.Add(usuario);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
// Assign Roles
if (model.SelectedRoles != null)
{
foreach (var roleId in model.SelectedRoles)
{
_context.RolesUsuario.Add(new RolUsuario
{
UsuarioId = usuario.Id,
RolId = roleId,
AsignadoEn = DateTime.UtcNow
});
}
await _context.SaveChangesAsync();
}
await transaction.CommitAsync(); await transaction.CommitAsync();
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }
@@ -94,6 +116,7 @@ public class UsuarioController : Controller
ModelState.AddModelError("", "Ocurrió un error al crear el usuario."); ModelState.AddModelError("", "Ocurrió un error al crear el usuario.");
} }
} }
ViewBag.Roles = await _context.RolesSistema.ToListAsync();
return View(model); return View(model);
} }
@@ -104,6 +127,7 @@ public class UsuarioController : Controller
var usuario = await _context.Usuarios var usuario = await _context.Usuarios
.Include(u => u.Persona) .Include(u => u.Persona)
.Include(u => u.RolesUsuario)
.FirstOrDefaultAsync(u => u.Id == id); .FirstOrDefaultAsync(u => u.Id == id);
if (usuario == null) return NotFound(); if (usuario == null) return NotFound();
@@ -116,9 +140,11 @@ public class UsuarioController : Controller
NombreUsuario = usuario.NombreUsuario, NombreUsuario = usuario.NombreUsuario,
Email = usuario.Email, Email = usuario.Email,
Telefono = usuario.Persona.Telefono, Telefono = usuario.Persona.Telefono,
Activo = usuario.Activo Activo = usuario.Activo,
SelectedRoles = usuario.RolesUsuario.Select(ru => ru.RolId).ToList()
}; };
ViewBag.Roles = await _context.RolesSistema.ToListAsync();
return View(model); return View(model);
} }
@@ -133,10 +159,12 @@ public class UsuarioController : Controller
{ {
var usuario = await _context.Usuarios var usuario = await _context.Usuarios
.Include(u => u.Persona) .Include(u => u.Persona)
.Include(u => u.RolesUsuario)
.FirstOrDefaultAsync(u => u.Id == id); .FirstOrDefaultAsync(u => u.Id == id);
if (usuario == null) return NotFound(); if (usuario == null) return NotFound();
using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
// Update Persona // Update Persona
@@ -151,21 +179,37 @@ public class UsuarioController : Controller
usuario.Activo = model.Activo; usuario.Activo = model.Activo;
usuario.ActualizadoEn = DateTime.UtcNow; usuario.ActualizadoEn = DateTime.UtcNow;
// Update password if provided
if (!string.IsNullOrEmpty(model.Contrasena)) if (!string.IsNullOrEmpty(model.Contrasena))
{ {
usuario.HashContrasena = BCrypt.Net.BCrypt.HashPassword(model.Contrasena); usuario.HashContrasena = BCrypt.Net.BCrypt.HashPassword(model.Contrasena);
} }
// Update Roles
_context.RolesUsuario.RemoveRange(usuario.RolesUsuario);
if (model.SelectedRoles != null)
{
foreach (var roleId in model.SelectedRoles)
{
_context.RolesUsuario.Add(new RolUsuario
{
UsuarioId = usuario.Id,
RolId = roleId,
AsignadoEn = DateTime.UtcNow
});
}
}
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await transaction.CommitAsync();
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }
catch (DbUpdateConcurrencyException) catch (Exception)
{ {
if (!UsuarioExists(model.Id.Value)) return NotFound(); await transaction.RollbackAsync();
else throw; ModelState.AddModelError("", "Ocurrió un error al actualizar el usuario.");
} }
} }
ViewBag.Roles = await _context.RolesSistema.ToListAsync();
return View(model); return View(model);
} }

View File

@@ -14,6 +14,10 @@ public class ApplicationDbContext : DbContext
public DbSet<Usuario> Usuarios { get; set; } public DbSet<Usuario> Usuarios { get; set; }
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<RolPermiso> RolesPermisos { get; set; }
public DbSet<Colaborador> Colaboradores { 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; }
public DbSet<ConfiguracionSistema> Configuraciones { get; set; } public DbSet<ConfiguracionSistema> Configuraciones { get; set; }
@@ -39,6 +43,20 @@ public class ApplicationDbContext : DbContext
.WithMany(r => r.RolesUsuario) .WithMany(r => r.RolesUsuario)
.HasForeignKey(ru => ru.RolId); .HasForeignKey(ru => ru.RolId);
// Configure composite key for RolPermiso
modelBuilder.Entity<RolPermiso>()
.HasKey(rp => new { rp.RolId, rp.PermisoId });
modelBuilder.Entity<RolPermiso>()
.HasOne(rp => rp.Rol)
.WithMany(r => r.RolesPermisos)
.HasForeignKey(rp => rp.RolId);
modelBuilder.Entity<RolPermiso>()
.HasOne(rp => rp.Permiso)
.WithMany()
.HasForeignKey(rp => rp.PermisoId);
modelBuilder.Entity<Usuario>() modelBuilder.Entity<Usuario>()
.HasOne(u => u.Persona) .HasOne(u => u.Persona)
.WithMany() .WithMany()

View File

@@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using foundation_system.Services;
using System.Security.Claims;
namespace foundation_system.Filters;
public class PermissionAttribute : TypeFilterAttribute
{
public PermissionAttribute(string permissionCode) : base(typeof(PermissionFilter))
{
Arguments = new object[] { permissionCode };
}
}
public class PermissionFilter : IAsyncAuthorizationFilter
{
private readonly string _permissionCode;
private readonly IAuthService _authService;
public PermissionFilter(string permissionCode, IAuthService authService)
{
_permissionCode = permissionCode;
_authService = authService;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (!context.HttpContext.User.Identity?.IsAuthenticated ?? true)
{
return;
}
var userIdClaim = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim == null || !long.TryParse(userIdClaim.Value, out var userId))
{
context.Result = new ForbidResult();
return;
}
var hasPermission = await _authService.HasPermissionAsync(userId, _permissionCode);
if (!hasPermission)
{
context.Result = new ForbidResult();
}
}
}

View File

@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace foundation_system.Models;
[Table("asistencia_colaboradores", Schema = "public")]
public class AsistenciaColaborador
{
[Key]
[Column("id")]
public long Id { get; set; }
[Column("colaborador_id")]
public long ColaboradorId { get; set; }
[ForeignKey("ColaboradorId")]
public virtual Colaborador Colaborador { get; set; } = null!;
[Column("fecha")]
public DateOnly Fecha { get; set; }
[Required]
[MaxLength(20)]
[Column("estado")]
public string Estado { get; set; } = "PRESENTE";
[Column("observaciones")]
public string? Observaciones { get; set; }
[Column("creado_en")]
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace foundation_system.Models;
[Table("colaboradores", Schema = "public")]
public class Colaborador
{
[Key]
[Column("id")]
public long Id { get; set; }
[Column("persona_id")]
public long PersonaId { get; set; }
[ForeignKey("PersonaId")]
public virtual Persona Persona { get; set; } = null!;
[Required]
[MaxLength(100)]
[Column("cargo")]
public string Cargo { get; set; } = string.Empty;
[Required]
[MaxLength(50)]
[Column("tipo_colaborador")]
public string TipoColaborador { get; set; } = string.Empty;
[Column("fecha_ingreso")]
public DateOnly FechaIngreso { get; set; } = DateOnly.FromDateTime(DateTime.Today);
[Column("horario_entrada")]
public TimeSpan? HorarioEntrada { get; set; }
[Column("horario_salida")]
public TimeSpan? HorarioSalida { get; set; }
[Column("activo")]
public bool Activo { get; set; } = true;
[Column("creado_en")]
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
[Column("actualizado_en")]
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
public virtual ICollection<AsistenciaColaborador> Asistencias { get; set; } = new List<AsistenciaColaborador>();
}

View File

@@ -0,0 +1,45 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace foundation_system.Models;
[Table("permisos")]
public class Permiso
{
[Key]
[Column("id")]
public int Id { get; set; }
[Column("modulo")]
[Required]
[StringLength(50)]
public string Modulo { get; set; } = string.Empty;
[Column("codigo")]
[Required]
[StringLength(100)]
public string Codigo { get; set; } = string.Empty;
[Column("nombre")]
[Required]
[StringLength(100)]
public string Nombre { get; set; } = string.Empty;
[Column("descripcion")]
public string? Descripcion { get; set; }
[Column("url")]
public string? Url { get; set; }
[Column("icono")]
public string? Icono { get; set; }
[Column("orden")]
public int Orden { get; set; } = 0;
[Column("es_menu")]
public bool EsMenu { get; set; } = true;
[Column("creado_en")]
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace foundation_system.Models;
[Table("roles_permisos")]
public class RolPermiso
{
[Column("rol_id")]
public int RolId { get; set; }
[Column("permiso_id")]
public int PermisoId { get; set; }
[Column("asignado_en")]
public DateTime AsignadoEn { get; set; } = DateTime.UtcNow;
// Navigation properties
[ForeignKey("RolId")]
public RolSistema Rol { get; set; } = null!;
[ForeignKey("PermisoId")]
public Permiso Permiso { get; set; } = null!;
}

View File

@@ -27,4 +27,5 @@ public class RolSistema
public DateTime CreadoEn { get; set; } = DateTime.UtcNow; public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
public ICollection<RolUsuario> RolesUsuario { get; set; } = new List<RolUsuario>(); public ICollection<RolUsuario> RolesUsuario { get; set; } = new List<RolUsuario>();
public ICollection<RolPermiso> RolesPermisos { get; set; } = new List<RolPermiso>();
} }

View File

@@ -0,0 +1,62 @@
using System.ComponentModel.DataAnnotations;
namespace foundation_system.Models.ViewModels;
public class ColaboradorViewModel
{
public long? Id { get; set; }
public long? PersonaId { get; set; }
[Required(ErrorMessage = "Los nombres son requeridos")]
[Display(Name = "Nombres")]
public string Nombres { get; set; } = string.Empty;
[Required(ErrorMessage = "Los apellidos son requeridos")]
[Display(Name = "Apellidos")]
public string Apellidos { get; set; } = string.Empty;
[Display(Name = "DUI")]
public string? Dui { get; set; }
[Display(Name = "NIT")]
public string? Nit { get; set; }
[Display(Name = "Fecha de Nacimiento")]
[DataType(DataType.Date)]
public DateOnly? FechaNacimiento { get; set; }
[Display(Name = "Género")]
public string? Genero { get; set; }
[EmailAddress(ErrorMessage = "Email inválido")]
public string? Email { get; set; }
[Display(Name = "Teléfono")]
public string? Telefono { get; set; }
[Display(Name = "Dirección")]
public string? Direccion { get; set; }
[Required(ErrorMessage = "El cargo es requerido")]
[Display(Name = "Cargo")]
public string Cargo { get; set; } = string.Empty;
[Required(ErrorMessage = "El tipo de colaborador es requerido")]
[Display(Name = "Tipo de Colaborador")]
public string TipoColaborador { get; set; } = string.Empty;
[Required(ErrorMessage = "La fecha de ingreso es requerida")]
[Display(Name = "Fecha de Ingreso")]
[DataType(DataType.Date)]
public DateOnly FechaIngreso { get; set; } = DateOnly.FromDateTime(DateTime.Today);
[Display(Name = "Horario de Entrada")]
[DataType(DataType.Time)]
public TimeSpan? HorarioEntrada { get; set; }
[Display(Name = "Horario de Salida")]
[DataType(DataType.Time)]
public TimeSpan? HorarioSalida { get; set; }
public bool Activo { get; set; } = true;
}

View File

@@ -39,4 +39,7 @@ public class UsuarioViewModel
[Display(Name = "Teléfono")] [Display(Name = "Teléfono")]
public string? Telefono { get; set; } public string? Telefono { get; set; }
[Display(Name = "Roles Asignados")]
public List<int> SelectedRoles { get; set; } = new List<int>();
} }

View File

@@ -145,4 +145,23 @@ public class AuthService : IAuthService
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
} }
} }
public async Task<bool> HasPermissionAsync(long userId, string permissionCode)
{
// ROOT has all permissions
var roles = await GetUserRolesAsync(userId);
if (roles.Contains("ROOT")) return true;
return await _context.RolesUsuario
.Where(ru => ru.UsuarioId == userId)
.Join(_context.RolesPermisos,
ru => ru.RolId,
rp => rp.RolId,
(ru, rp) => rp)
.Join(_context.Permisos,
rp => rp.PermisoId,
p => p.Id,
(rp, p) => p)
.AnyAsync(p => p.Codigo == permissionCode);
}
} }

View File

@@ -24,4 +24,9 @@ public interface IAuthService
/// Updates the last login timestamp /// Updates the last login timestamp
/// </summary> /// </summary>
Task UpdateLastLoginAsync(long userId); Task UpdateLastLoginAsync(long userId);
/// <summary>
/// Checks if a user has a specific permission
/// </summary>
Task<bool> HasPermissionAsync(long userId, string permissionCode);
} }

View File

@@ -93,6 +93,9 @@
<button class="btn btn-light btn-sm" id="btnExportar"> <button class="btn btn-light btn-sm" id="btnExportar">
<i class="bi bi-file-earmark-excel"></i> Exportar <i class="bi bi-file-earmark-excel"></i> Exportar
</button> </button>
<button class="btn btn-info btn-sm" id="btnReporte">
<i class="bi bi-file-earmark-pdf"></i> Reporte Mensual
</button>
</div> </div>
</div> </div>
@@ -143,8 +146,16 @@
<tr data-expediente-id="@expediente.Id"> <tr data-expediente-id="@expediente.Id">
<td class="sticky-left bg-white" style="min-width: 200px;"> <td class="sticky-left bg-white" style="min-width: 200px;">
<div class="d-flex align-items-center">
<a href="javascript:void(0)" class="btn-reporte-individual text-decoration-none"
data-id="@expediente.Id" title="Ver reporte individual">
<i class="bi bi-file-earmark-person text-info me-2"></i>
</a>
<div>
<div class="fw-bold">@nombreCompleto</div> <div class="fw-bold">@nombreCompleto</div>
<div class="small text-muted">Edad: @edad años</div> <div class="small text-muted">Edad: @edad años</div>
</div>
</div>
</td> </td>
@foreach (var dia in Model.DiasDelMes) @foreach (var dia in Model.DiasDelMes)
@@ -650,6 +661,34 @@
window.open(url, '_blank'); window.open(url, '_blank');
}); });
$('#btnReporte').click(function () {
var año = $('#selectAnio').val();
var mes = $('#selectMes').val();
var diasSemana = $('#diasSemanaInput').val();
var url = '@Url.Action("ReporteMensual", "Asistencia")' +
'?año=' + año +
'&mes=' + mes +
'&diasSemana=' + diasSemana;
window.open(url, '_blank');
});
$('.btn-reporte-individual').click(function () {
var ninoId = $(this).data('id');
var año = $('#selectAnio').val();
var mes = $('#selectMes').val();
var diasSemana = $('#diasSemanaInput').val();
var url = '@Url.Action("ReporteIndividual", "Asistencia")' +
'?ninoId=' + ninoId +
'&año=' + año +
'&mes=' + mes +
'&diasSemana=' + diasSemana;
window.open(url, '_blank');
});
$('#selectAnio, #selectMes').change(function () { $('#selectAnio, #selectMes').change(function () {
$('#filtroForm').submit(); $('#filtroForm').submit();
}); });

View File

@@ -0,0 +1,182 @@
@model foundation_system.Models.ViewModels.AsistenciaGridViewModel
@{
Layout = null;
ViewData["Title"] = "Reporte Individual de Asistencia";
var nino = Model.Expedientes.First();
var diasSeleccionadosList = new List<string>();
if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados))
{
diasSeleccionadosList = Model.DiasSemanaSeleccionados
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(d => d.Trim())
.ToList();
}
var diasAMostrar = Model.DiasDelMes
.Where(d => diasSeleccionadosList.Count == 0 ||
diasSeleccionadosList.Contains(((int)d.DayOfWeek).ToString()))
.ToList();
}
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" />
<style>
@@media print {
.no-print { display: none !important; }
body { padding: 0; margin: 0; }
.container { width: 100%; max-width: none; padding: 0; }
@@page { size: portrait; margin: 1.5cm; }
}
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: white; color: #333; }
.report-header { border-bottom: 3px solid #2c3e50; margin-bottom: 30px; padding-bottom: 15px; }
.foundation-name { font-size: 28px; font-weight: bold; color: #2c3e50; }
.report-title { font-size: 20px; color: #7f8c8d; text-transform: uppercase; letter-spacing: 1px; }
.info-card { background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin-bottom: 30px; }
.info-label { font-weight: bold; color: #2c3e50; width: 150px; display: inline-block; }
.attendance-table { border: 1px solid #dee2e6; }
.attendance-table th { background-color: #2c3e50; color: white; text-align: center; }
.attendance-table td { text-align: center; vertical-align: middle; }
.state-badge { padding: 5px 10px; border-radius: 4px; font-weight: bold; font-size: 0.9rem; }
.bg-P { background-color: #d1e7dd; color: #0f5132; }
.bg-F { background-color: #f8d7da; color: #842029; }
.bg-T { background-color: #fff3cd; color: #664d03; }
.bg-J { background-color: #cff4fc; color: #055160; }
.bg-E { background-color: #e2e3e5; color: #41464b; }
.summary-box { border: 2px solid #2c3e50; border-radius: 8px; padding: 15px; }
.summary-item { display: flex; justify-content: space-between; margin-bottom: 5px; border-bottom: 1px dashed #dee2e6; }
.summary-item:last-child { border-bottom: none; }
.signature-section { margin-top: 80px; }
.signature-line { border-top: 1px solid #000; width: 250px; margin: 0 auto; margin-top: 50px; }
</style>
</head>
<body>
<div class="container py-5">
<div class="no-print mb-4 text-end">
<button onclick="window.print()" class="btn btn-primary shadow-sm">
<i class="bi bi-printer"></i> Imprimir Reporte
</button>
<button onclick="window.close()" class="btn btn-outline-secondary shadow-sm">
<i class="bi bi-x-lg"></i> Cerrar
</button>
</div>
<div class="report-header d-flex justify-content-between align-items-end">
<div>
<div class="foundation-name">FUNDACIÓN MIES</div>
<div class="report-title">Registro Individual de Asistencia</div>
</div>
<div class="text-end">
<div class="h5 mb-0">PERIODO: @Model.NombreMes @Model.Año</div>
<div class="small text-muted">Fecha de emisión: @DateTime.Now.ToString("dd/MM/yyyy")</div>
</div>
</div>
<div class="info-card shadow-sm">
<h5 class="border-bottom pb-2 mb-3"><i class="bi bi-person-badge"></i> Información del Niño/a</h5>
<div class="row">
<div class="col-md-7">
<p><span class="info-label">Nombre Completo:</span> <span class="h6">@nino.Persona.Nombres @nino.Persona.Apellidos</span></p>
<p><span class="info-label">Código:</span> <span>@nino.CodigoInscripcion</span></p>
<p><span class="info-label">Estado:</span> <span class="badge bg-success">@nino.Estado</span></p>
</div>
<div class="col-md-5">
@{
var totalP = 0; var totalT = 0; var totalF = 0; var totalJ = 0; var totalE = 0;
foreach (var dia in diasAMostrar) {
var key = $"{nino.Id}_{dia:yyyy-MM-dd}";
var estado = Model.Asistencias.ContainsKey(key) ? Model.Asistencias[key] : "";
switch(estado) {
case "P": totalP++; break;
case "T": totalT++; break;
case "F": totalF++; break;
case "J": totalJ++; break;
case "E": totalE++; break;
}
}
var totalRegistros = totalP + totalT + totalF + totalJ + totalE;
var porcentaje = totalRegistros > 0 ? (totalP * 100.0 / totalRegistros) : 0;
}
<div class="summary-box">
<h6 class="text-center mb-3">Resumen de Asistencia</h6>
<div class="summary-item"><span>Presentes (P):</span> <strong>@totalP</strong></div>
<div class="summary-item"><span>Tardes (T):</span> <strong>@totalT</strong></div>
<div class="summary-item"><span>Faltas (F):</span> <strong>@totalF</strong></div>
<div class="summary-item"><span>Justificados (J):</span> <strong>@totalJ</strong></div>
<div class="summary-item"><span>Enfermos (E):</span> <strong>@totalE</strong></div>
<div class="summary-item mt-2 pt-2 border-top border-dark">
<span>Asistencia Efectiva:</span>
<strong class="@(porcentaje >= 80 ? "text-success" : "text-danger")">@porcentaje.ToString("F1")%</strong>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<table class="table table-bordered attendance-table shadow-sm">
<thead>
<tr>
<th style="width: 20%;">Fecha</th>
<th style="width: 20%;">Día</th>
<th style="width: 30%;">Estado</th>
</tr>
</thead>
<tbody>
@foreach (var dia in diasAMostrar)
{
var key = $"{nino.Id}_{dia:yyyy-MM-dd}";
var estado = Model.Asistencias.ContainsKey(key) ? Model.Asistencias[key] : "";
var nombreDia = dia.ToString("dddd", new System.Globalization.CultureInfo("es-ES"));
var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday;
<tr class="@(esFinDeSemana ? "bg-light" : "")">
<td>@dia.ToString("dd/MM/yyyy")</td>
<td class="text-capitalize">@nombreDia</td>
<td>
@if (!string.IsNullOrEmpty(estado))
{
<span class="state-badge bg-@estado">
@(estado switch {
"P" => "PRESENTE",
"T" => "TARDE",
"F" => "AUSENTE",
"J" => "JUSTIFICADO",
"E" => "ENFERMO",
_ => ""
})
</span>
}
else
{
<span class="text-muted small">- Sin registro -</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="row signature-section text-center">
<div class="col-6">
<div class="signature-line"></div>
<div class="mt-2 fw-bold">Firma del Responsable</div>
<div class="small text-muted">Control de Asistencia</div>
</div>
<div class="col-6">
<div class="signature-line"></div>
<div class="mt-2 fw-bold">Sello de la Institución</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,174 @@
@model foundation_system.Models.ViewModels.AsistenciaGridViewModel
@{
Layout = null;
ViewData["Title"] = "Reporte Mensual de Asistencia";
var diasSeleccionadosList = new List<string>();
if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados))
{
diasSeleccionadosList = Model.DiasSemanaSeleccionados
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(d => d.Trim())
.ToList();
}
var diasAMostrar = Model.DiasDelMes
.Where(d => diasSeleccionadosList.Count == 0 ||
diasSeleccionadosList.Contains(((int)d.DayOfWeek).ToString()))
.ToList();
}
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" />
<style>
@@media print {
.no-print { display: none !important; }
body { padding: 0; margin: 0; }
.container-fluid { width: 100%; padding: 0; }
.table { font-size: 8pt; }
.table th, .table td { padding: 2px !important; }
@@page { size: landscape; margin: 1cm; }
}
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: white; }
.report-header { border-bottom: 2px solid #2c3e50; margin-bottom: 20px; padding-bottom: 10px; }
.foundation-name { font-size: 24px; font-weight: bold; color: #2c3e50; }
.report-title { font-size: 18px; color: #7f8c8d; }
.table-report { border: 1px solid #dee2e6; }
.table-report th { background-color: #f8f9fa; text-align: center; vertical-align: middle; border: 1px solid #dee2e6; }
.table-report td { text-align: center; vertical-align: middle; border: 1px solid #dee2e6; }
.name-column { text-align: left !important; padding-left: 10px !important; min-width: 180px; }
.total-column { font-weight: bold; background-color: #f8f9fa; }
.signature-section { margin-top: 50px; }
.signature-line { border-top: 1px solid #000; width: 200px; margin: 0 auto; margin-top: 40px; }
.state-P { color: #198754; font-weight: bold; }
.state-F { color: #dc3545; font-weight: bold; }
.state-T { color: #ffc107; font-weight: bold; }
.state-J { color: #0dcaf0; font-weight: bold; }
.state-E { color: #6c757d; font-weight: bold; }
</style>
</head>
<body>
<div class="container-fluid p-4">
<div class="no-print mb-4 text-end">
<button onclick="window.print()" class="btn btn-primary">
<i class="bi bi-printer"></i> Imprimir Reporte
</button>
<button onclick="window.close()" class="btn btn-secondary">
<i class="bi bi-x-lg"></i> Cerrar
</button>
</div>
<div class="report-header d-flex justify-content-between align-items-center">
<div>
<div class="foundation-name">FUNDACIÓN MIES</div>
<div class="report-title">Reporte Mensual de Asistencia</div>
</div>
<div class="text-end">
<div class="fw-bold">MES: @Model.NombreMes @Model.Año</div>
<div class="small text-muted">Generado el: @DateTime.Now.ToString("dd/MM/yyyy HH:mm")</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-sm table-report">
<thead>
<tr>
<th rowspan="2" class="name-column">Nombre del Niño/a</th>
<th colspan="@diasAMostrar.Count">Días del Mes</th>
<th colspan="5">Totales</th>
</tr>
<tr>
@foreach (var dia in diasAMostrar)
{
<th>@dia.Day</th>
}
<th title="Presente">P</th>
<th title="Tarde">T</th>
<th title="Falta">F</th>
<th title="Justificado">J</th>
<th title="Enfermo">E</th>
</tr>
</thead>
<tbody>
@foreach (var nino in Model.Expedientes)
{
var totalP = 0;
var totalT = 0;
var totalF = 0;
var totalJ = 0;
var totalE = 0;
<tr>
<td class="name-column">@nino.Persona.Nombres @nino.Persona.Apellidos</td>
@foreach (var dia in diasAMostrar)
{
var key = $"{nino.Id}_{dia:yyyy-MM-dd}";
var estado = Model.Asistencias.ContainsKey(key) ? Model.Asistencias[key] : "";
switch(estado) {
case "P": totalP++; break;
case "T": totalT++; break;
case "F": totalF++; break;
case "J": totalJ++; break;
case "E": totalE++; break;
}
<td class="state-@estado">@estado</td>
}
<td class="total-column">@totalP</td>
<td class="total-column">@totalT</td>
<td class="total-column">@totalF</td>
<td class="total-column">@totalJ</td>
<td class="total-column">@totalE</td>
</tr>
}
</tbody>
<tfoot>
<tr class="fw-bold">
<td class="name-column">TOTALES POR DÍA</td>
@foreach (var dia in diasAMostrar)
{
var totalDia = 0;
foreach (var nino in Model.Expedientes)
{
var key = $"{nino.Id}_{dia:yyyy-MM-dd}";
if (Model.Asistencias.ContainsKey(key) && !string.IsNullOrEmpty(Model.Asistencias[key]))
{
totalDia++;
}
}
<td>@totalDia</td>
}
<td colspan="5" class="bg-light"></td>
</tr>
</tfoot>
</table>
</div>
<div class="row signature-section text-center">
<div class="col-4">
<div class="signature-line"></div>
<div class="mt-2">Elaborado por</div>
</div>
<div class="col-4">
<div class="signature-line"></div>
<div class="mt-2">Revisado por</div>
</div>
<div class="col-4">
<div class="signature-line"></div>
<div class="mt-2">Sello de la Institución</div>
</div>
</div>
<div class="mt-4 small text-muted no-print">
<p><strong>Leyenda:</strong> P: Presente, T: Tarde, F: Falta, J: Justificado, E: Enfermo</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,123 @@
@model foundation_system.Models.ViewModels.ColaboradorViewModel
@{
ViewData["Title"] = "Nuevo Colaborador";
}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card shadow">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-person-plus-fill me-2"></i>@ViewData["Title"]</h5>
</div>
<div class="card-body">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
<h6 class="text-primary border-bottom pb-2 mb-3">Información Personal</h6>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="Nombres" class="form-label"></label>
<input asp-for="Nombres" class="form-control" />
<span asp-validation-for="Nombres" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="Apellidos" class="form-label"></label>
<input asp-for="Apellidos" class="form-control" />
<span asp-validation-for="Apellidos" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Dui" class="form-label"></label>
<input asp-for="Dui" class="form-control" placeholder="00000000-0" />
<span asp-validation-for="Dui" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Nit" class="form-label"></label>
<input asp-for="Nit" class="form-control" placeholder="0000-000000-000-0" />
<span asp-validation-for="Nit" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Genero" class="form-label"></label>
<select asp-for="Genero" class="form-select">
<option value="">Seleccione...</option>
<option value="M">Masculino</option>
<option value="F">Femenino</option>
<option value="O">Otro</option>
</select>
<span asp-validation-for="Genero" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="FechaNacimiento" class="form-label"></label>
<input asp-for="FechaNacimiento" class="form-control" />
<span asp-validation-for="FechaNacimiento" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Telefono" class="form-label"></label>
<input asp-for="Telefono" class="form-control" />
<span asp-validation-for="Telefono" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger small"></span>
</div>
<div class="col-12">
<label asp-for="Direccion" class="form-label"></label>
<textarea asp-for="Direccion" class="form-control" rows="2"></textarea>
<span asp-validation-for="Direccion" class="text-danger small"></span>
</div>
</div>
<h6 class="text-primary border-bottom pb-2 mb-3">Información Laboral</h6>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="Cargo" class="form-label"></label>
<input asp-for="Cargo" class="form-control" placeholder="Ej: Maestro, Cocinero, Administrador" />
<span asp-validation-for="Cargo" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="TipoColaborador" class="form-label"></label>
<select asp-for="TipoColaborador" class="form-select">
<option value="Permanente">Permanente</option>
<option value="Voluntario">Voluntario</option>
<option value="Temporal">Temporal</option>
<option value="Por Horas">Por Horas</option>
</select>
<span asp-validation-for="TipoColaborador" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="FechaIngreso" class="form-label"></label>
<input asp-for="FechaIngreso" class="form-control" />
<span asp-validation-for="FechaIngreso" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="HorarioEntrada" class="form-label"></label>
<input asp-for="HorarioEntrada" class="form-control" type="time" />
<span asp-validation-for="HorarioEntrada" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="HorarioSalida" class="form-label"></label>
<input asp-for="HorarioSalida" class="form-control" type="time" />
<span asp-validation-for="HorarioSalida" class="text-danger small"></span>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i> Volver
</a>
<button type="submit" class="btn btn-primary px-5">
<i class="bi bi-save me-2"></i> Guardar Colaborador
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,130 @@
@model foundation_system.Models.ViewModels.ColaboradorViewModel
@{
ViewData["Title"] = "Editar Colaborador";
}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card shadow">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-pencil-square me-2"></i>@ViewData["Title"]</h5>
</div>
<div class="card-body">
<form asp-action="Edit">
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="PersonaId" />
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
<h6 class="text-primary border-bottom pb-2 mb-3">Información Personal</h6>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="Nombres" class="form-label"></label>
<input asp-for="Nombres" class="form-control" />
<span asp-validation-for="Nombres" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="Apellidos" class="form-label"></label>
<input asp-for="Apellidos" class="form-control" />
<span asp-validation-for="Apellidos" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Dui" class="form-label"></label>
<input asp-for="Dui" class="form-control" />
<span asp-validation-for="Dui" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Nit" class="form-label"></label>
<input asp-for="Nit" class="form-control" />
<span asp-validation-for="Nit" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Genero" class="form-label"></label>
<select asp-for="Genero" class="form-select">
<option value="M">Masculino</option>
<option value="F">Femenino</option>
<option value="O">Otro</option>
</select>
<span asp-validation-for="Genero" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="FechaNacimiento" class="form-label"></label>
<input asp-for="FechaNacimiento" class="form-control" />
<span asp-validation-for="FechaNacimiento" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Telefono" class="form-label"></label>
<input asp-for="Telefono" class="form-control" />
<span asp-validation-for="Telefono" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger small"></span>
</div>
<div class="col-12">
<label asp-for="Direccion" class="form-label"></label>
<textarea asp-for="Direccion" class="form-control" rows="2"></textarea>
<span asp-validation-for="Direccion" class="text-danger small"></span>
</div>
</div>
<h6 class="text-primary border-bottom pb-2 mb-3">Información Laboral</h6>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label asp-for="Cargo" class="form-label"></label>
<input asp-for="Cargo" class="form-control" />
<span asp-validation-for="Cargo" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="TipoColaborador" class="form-label"></label>
<select asp-for="TipoColaborador" class="form-select">
<option value="Permanente">Permanente</option>
<option value="Voluntario">Voluntario</option>
<option value="Temporal">Temporal</option>
<option value="Por Horas">Por Horas</option>
</select>
<span asp-validation-for="TipoColaborador" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="FechaIngreso" class="form-label"></label>
<input asp-for="FechaIngreso" class="form-control" />
<span asp-validation-for="FechaIngreso" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="HorarioEntrada" class="form-label"></label>
<input asp-for="HorarioEntrada" class="form-control" type="time" />
<span asp-validation-for="HorarioEntrada" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="HorarioSalida" class="form-label"></label>
<input asp-for="HorarioSalida" class="form-control" type="time" />
<span asp-validation-for="HorarioSalida" class="text-danger small"></span>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="Activo">
<label class="form-check-label" asp-for="Activo">Colaborador Activo</label>
</div>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i> Volver
</a>
<button type="submit" class="btn btn-primary px-5">
<i class="bi bi-save me-2"></i> Guardar Cambios
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,106 @@
@model IEnumerable<foundation_system.Models.Colaborador>
@{
ViewData["Title"] = "Gestión 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-people-fill me-2"></i>@ViewData["Title"]</h2>
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-person-plus-fill me-2"></i> Nuevo Colaborador
</a>
</div>
<div class="card shadow mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle" id="colaboradoresTable">
<thead class="table-light">
<tr>
<th>Nombre Completo</th>
<th>Cargo</th>
<th>Tipo</th>
<th>DUI</th>
<th>Teléfono</th>
<th>Estado</th>
<th class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-circle me-3">
@item.Persona.Nombres.Substring(0, 1)@item.Persona.Apellidos.Substring(0, 1)
</div>
<div>
<div class="fw-bold">@item.Persona.Nombres @item.Persona.Apellidos</div>
<small class="text-muted">@item.Persona.Email</small>
</div>
</div>
</td>
<td><span class="badge bg-info text-dark">@item.Cargo</span></td>
<td>@item.TipoColaborador</td>
<td>@item.Persona.Dui</td>
<td>@item.Persona.Telefono</td>
<td>
@if (item.Activo)
{
<span class="badge bg-success">Activo</span>
}
else
{
<span class="badge bg-danger">Inactivo</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="Desactivar"
onclick="confirmDelete(@item.Id, '@item.Persona.Nombres @item.Persona.Apellidos')">
<i class="bi bi-person-x"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
<style>
.avatar-circle {
width: 40px;
height: 40px;
background-color: #e9ecef;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #495057;
}
</style>
<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 desactivar a "${name}"?`)) {
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
}

View File

@@ -0,0 +1,116 @@
@model IEnumerable<foundation_system.Models.Colaborador>
@{
ViewData["Title"] = "Asistencia de Colaboradores";
var selectedDate = (DateOnly)ViewBag.SelectedDate;
var asistencias = (Dictionary<long, foundation_system.Models.AsistenciaColaborador>)ViewBag.Asistencias;
}
<div class="container-fluid">
<div class="card shadow mb-4">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center py-3">
<h5 class="mb-0"><i class="bi bi-calendar-check me-2"></i>@ViewData["Title"]</h5>
<div class="d-flex align-items-center">
<label class="me-2 mb-0">Fecha:</label>
<input type="date" id="fechaAsistencia" class="form-control form-control-sm"
value="@selectedDate.ToString("yyyy-MM-dd")" onchange="changeDate(this.value)" />
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Colaborador</th>
<th>Cargo</th>
<th class="text-center" style="width: 400px;">Estado de Asistencia</th>
<th>Observaciones</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
var asistencia = asistencias.ContainsKey(item.Id) ? asistencias[item.Id] : null;
var estadoActual = asistencia?.Estado ?? "";
<tr>
<td>
<div class="fw-bold">@item.Persona.Nombres @item.Persona.Apellidos</div>
<small class="text-muted">@item.HorarioEntrada?.ToString(@"hh\:mm") - @item.HorarioSalida?.ToString(@"hh\:mm")</small>
</td>
<td><span class="badge bg-info text-dark">@item.Cargo</span></td>
<td>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="estado_@item.Id" id="pres_@item.Id" value="PRESENTE"
@(estadoActual == "PRESENTE" ? "checked" : "") onchange="saveAsistencia(@item.Id, 'PRESENTE')">
<label class="btn btn-outline-success btn-sm" for="pres_@item.Id">Presente</label>
<input type="radio" class="btn-check" name="estado_@item.Id" id="aus_@item.Id" value="AUSENTE"
@(estadoActual == "AUSENTE" ? "checked" : "") onchange="saveAsistencia(@item.Id, 'AUSENTE')">
<label class="btn btn-outline-danger btn-sm" for="aus_@item.Id">Ausente</label>
<input type="radio" class="btn-check" name="estado_@item.Id" id="just_@item.Id" value="JUSTIFICADO"
@(estadoActual == "JUSTIFICADO" ? "checked" : "") onchange="saveAsistencia(@item.Id, 'JUSTIFICADO')">
<label class="btn btn-outline-warning btn-sm" for="just_@item.Id">Justif.</label>
<input type="radio" class="btn-check" name="estado_@item.Id" id="enf_@item.Id" value="ENFERMO"
@(estadoActual == "ENFERMO" ? "checked" : "") onchange="saveAsistencia(@item.Id, 'ENFERMO')">
<label class="btn btn-outline-info btn-sm" for="enf_@item.Id">Enfermo</label>
</div>
</td>
<td>
<input type="text" class="form-control form-control-sm" id="obs_@item.Id"
value="@asistencia?.Observaciones" placeholder="Nota..."
onblur="saveAsistencia(@item.Id, null)" />
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
function changeDate(fecha) {
window.location.href = '@Url.Action("Index")?fecha=' + fecha;
}
function saveAsistencia(colaboradorId, estado) {
const fecha = document.getElementById('fechaAsistencia').value;
const observaciones = document.getElementById('obs_' + colaboradorId).value;
// Si el estado es null, tomamos el que esté seleccionado
if (!estado) {
const checkedRadio = document.querySelector(`input[name="estado_${colaboradorId}"]:checked`);
if (!checkedRadio) return;
estado = checkedRadio.value;
}
const formData = new FormData();
formData.append('colaboradorId', colaboradorId);
formData.append('fecha', fecha);
formData.append('estado', estado);
formData.append('observaciones', observaciones);
fetch('@Url.Action("Save")', {
method: 'POST',
body: formData,
headers: {
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Opcional: Mostrar feedback visual sutil
console.log('Guardado');
}
});
}
</script>
}
@Html.AntiForgeryToken()

View File

@@ -0,0 +1,86 @@
@model foundation_system.Models.Permiso
@{
ViewData["Title"] = "Nueva Opción de Menú";
}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>@ViewData["Title"]</h5>
</div>
<div class="card-body">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Modulo" class="form-label">Módulo (Sección)</label>
<input asp-for="Modulo" class="form-control" placeholder="Ej: Gestión, Administración" />
<span asp-validation-for="Modulo" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="Codigo" class="form-label">Código Único (Controlador)</label>
<input asp-for="Codigo" class="form-control" placeholder="Ej: Expediente" />
<span asp-validation-for="Codigo" class="text-danger small"></span>
</div>
<div class="col-md-8">
<label asp-for="Nombre" class="form-label">Nombre en Menú</label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Expedientes" />
<span asp-validation-for="Nombre" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Orden" class="form-label">Orden</label>
<input asp-for="Orden" class="form-control" type="number" />
<span asp-validation-for="Orden" class="text-danger small"></span>
</div>
<div class="col-md-8">
<label asp-for="Url" class="form-label">Ruta (URL)</label>
<input asp-for="Url" class="form-control" placeholder="Ej: /Expediente/Index" />
<span asp-validation-for="Url" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Icono" class="form-label">Icono (Bootstrap Icons)</label>
<input asp-for="Icono" class="form-control" placeholder="Ej: bi-folder" />
<span asp-validation-for="Icono" class="text-danger small"></span>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="EsMenu">
<label class="form-check-label" asp-for="EsMenu">Mostrar en el menú lateral</label>
</div>
</div>
<div class="col-12">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="2"></textarea>
<span asp-validation-for="Descripcion" class="text-danger small"></span>
</div>
</div>
<div class="mt-4 d-flex justify-content-between">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Volver
</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-save me-2"></i> Guardar Opción
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,87 @@
@model foundation_system.Models.Permiso
@{
ViewData["Title"] = "Editar Opción de Menú";
}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-pencil-square me-2"></i>@ViewData["Title"]</h5>
</div>
<div class="card-body">
<form asp-action="Edit">
<input type="hidden" asp-for="Id" />
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Modulo" class="form-label">Módulo (Sección)</label>
<input asp-for="Modulo" class="form-control" />
<span asp-validation-for="Modulo" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="Codigo" class="form-label">Código Único</label>
<input asp-for="Codigo" class="form-control" readonly />
<span asp-validation-for="Codigo" class="text-danger small"></span>
</div>
<div class="col-md-8">
<label asp-for="Nombre" class="form-label">Nombre en Menú</label>
<input asp-for="Nombre" class="form-control" />
<span asp-validation-for="Nombre" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Orden" class="form-label">Orden</label>
<input asp-for="Orden" class="form-control" type="number" />
<span asp-validation-for="Orden" class="text-danger small"></span>
</div>
<div class="col-md-8">
<label asp-for="Url" class="form-label">Ruta (URL)</label>
<input asp-for="Url" class="form-control" />
<span asp-validation-for="Url" class="text-danger small"></span>
</div>
<div class="col-md-4">
<label asp-for="Icono" class="form-label">Icono</label>
<input asp-for="Icono" class="form-control" />
<span asp-validation-for="Icono" class="text-danger small"></span>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" asp-for="EsMenu">
<label class="form-check-label" asp-for="EsMenu">Mostrar en el menú lateral</label>
</div>
</div>
<div class="col-12">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="2"></textarea>
<span asp-validation-for="Descripcion" class="text-danger small"></span>
</div>
</div>
<div class="mt-4 d-flex justify-content-between">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Volver
</a>
<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");}
}

View File

@@ -0,0 +1,90 @@
@model IEnumerable<foundation_system.Models.Permiso>
@{
ViewData["Title"] = "Gestión de Menú y Permisos";
}
<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-list-ul me-2"></i>@ViewData["Title"]</h2>
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Nueva Opción
</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>Módulo</th>
<th>Nombre</th>
<th>Ruta (URL)</th>
<th>Icono</th>
<th class="text-center">Es Menú</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><span class="badge bg-info text-dark">@item.Modulo</span></td>
<td class="fw-bold">@item.Nombre</td>
<td class="font-monospace small">@item.Url</td>
<td><i class="bi @item.Icono me-2"></i>@item.Icono</td>
<td class="text-center">
@if (item.EsMenu)
{
<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 "${name}"?`)) {
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
}

View File

@@ -0,0 +1,55 @@
@model foundation_system.Models.RolSistema
@{
ViewData["Title"] = "Nuevo Rol";
}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-plus-circle me-2"></i>@ViewData["Title"]</h5>
</div>
<div class="card-body">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Codigo" class="form-label">Código del Rol</label>
<input asp-for="Codigo" class="form-control" placeholder="Ej: ADMIN, OPERADOR" />
<span asp-validation-for="Codigo" class="text-danger small"></span>
</div>
<div class="col-md-6">
<label asp-for="Nombre" class="form-label">Nombre</label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Administrador" />
<span asp-validation-for="Nombre" class="text-danger small"></span>
</div>
<div class="col-12">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="3" placeholder="Describa las responsabilidades de este rol..."></textarea>
<span asp-validation-for="Descripcion" class="text-danger small"></span>
</div>
</div>
<div class="mt-4 d-flex justify-content-between">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Volver al listado
</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-save me-2"></i> Guardar Rol
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,57 @@
@model foundation_system.Models.RolSistema
@{
ViewData["Title"] = "Editar Rol";
}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-pencil-square me-2"></i>@ViewData["Title"]</h5>
</div>
<div class="card-body">
<form asp-action="Edit">
<input type="hidden" asp-for="Id" />
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
<div class="row g-3">
<div class="col-md-6">
<label asp-for="Codigo" class="form-label">Código del Rol</label>
<input asp-for="Codigo" class="form-control" readonly />
<span asp-validation-for="Codigo" class="text-danger small"></span>
<div class="form-text">El código no se puede modificar.</div>
</div>
<div class="col-md-6">
<label asp-for="Nombre" class="form-label">Nombre</label>
<input asp-for="Nombre" class="form-control" />
<span asp-validation-for="Nombre" class="text-danger small"></span>
</div>
<div class="col-12">
<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 small"></span>
</div>
</div>
<div class="mt-4 d-flex justify-content-between">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Volver al listado
</a>
<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");}
}

View File

@@ -0,0 +1,90 @@
@model IEnumerable<foundation_system.Models.RolSistema>
@{
ViewData["Title"] = "Gestión de Roles";
}
<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-person-badge me-2"></i>@ViewData["Title"]</h2>
<a asp-action="Create" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Nuevo Rol
</a>
</div>
@if (TempData["SuccessMessage"] != null)
{
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> @TempData["SuccessMessage"]
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</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>Código</th>
<th>Nombre</th>
<th>Descripción</th>
<th class="text-center">Permisos</th>
<th class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td class="font-monospace">@item.Codigo</td>
<td class="fw-bold">@item.Nombre</td>
<td class="text-muted small">@item.Descripcion</td>
<td class="text-center">
<span class="badge bg-secondary">@item.RolesPermisos.Count asignados</span>
</td>
<td class="text-center">
<div class="btn-group btn-group-sm">
<a asp-action="Permissions" asp-route-id="@item.Id" class="btn btn-outline-info" title="Gestionar Permisos">
<i class="bi bi-shield-check"></i>
</a>
<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 rol "${name}"?`)) {
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
}

View File

@@ -0,0 +1,62 @@
@model IEnumerable<foundation_system.Models.Permiso>
@{
var rol = ViewBag.Rol as foundation_system.Models.RolSistema;
var assignedCodes = ViewBag.AssignedControllerCodes as List<string>;
ViewData["Title"] = "Gestionar Acceso a Menú: " + rol?.Nombre;
}
<div class="container">
<div class="card shadow">
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-shield-check me-2"></i>@ViewData["Title"]</h5>
<span class="badge bg-light text-dark">@rol?.Codigo</span>
</div>
<div class="card-body">
<p class="text-muted mb-4">Seleccione las opciones del menú que estarán disponibles para este rol.</p>
<form asp-action="UpdatePermissions" method="post">
<input type="hidden" name="rolId" value="@rol?.Id" />
@{
var groupedControllers = Model.GroupBy(c => c.Modulo);
}
<div class="row">
@foreach (var group in groupedControllers)
{
<div class="col-md-6 mb-4">
<div class="card h-100 border-light shadow-sm">
<div class="card-header bg-light py-2">
<h6 class="mb-0 fw-bold text-primary">@group.Key</h6>
</div>
<div class="card-body">
@foreach (var controller in group)
{
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="selectedControllers"
value="@controller.Codigo" id="ctrl_@controller.Codigo"
@(assignedCodes != null && assignedCodes.Contains(controller.Codigo) ? "checked" : "")>
<label class="form-check-label" for="ctrl_@controller.Codigo">
<span class="fw-semibold">@controller.Nombre</span>
</label>
</div>
}
</div>
</div>
</div>
}
</div>
<div class="mt-4 d-flex justify-content-between border-top pt-3">
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Cancelar
</a>
<button type="submit" class="btn btn-primary px-5">
<i class="bi bi-check-all me-2"></i> Guardar Configuración
</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
@model IEnumerable<foundation_system.Models.Permiso>
<nav class="nav flex-column">
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Home" ? "active" : "")" asp-controller="Home" asp-action="Index">
<i class="bi bi-house-door"></i> Inicio
</a>
@{
var groupedMenu = Model.GroupBy(m => m.Modulo);
var currentController = ViewContext.RouteData.Values["controller"]?.ToString();
}
@foreach (var group in groupedMenu)
{
<div class="nav-section-title mt-3">@group.Key</div>
@foreach (var item in group)
{
<a class="nav-link-custom @(currentController == item.Codigo ? "active" : "")" href="@item.Url">
<i class="bi @item.Icono"></i> @item.Nombre
</a>
}
}
</nav>

View File

@@ -24,30 +24,8 @@
<i class="bi bi-house-heart-fill me-2"></i>MIES <i class="bi bi-house-heart-fill me-2"></i>MIES
</a> </a>
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav p-3">
<div class="nav-section-title">Principal</div> @await Component.InvokeAsync("Menu")
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Home" ? "active" : "")" asp-controller="Home" asp-action="Index">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<div class="nav-section-title">Gestión</div>
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Expediente" ? "active" : "")" asp-controller="Expediente" asp-action="Index">
<i class="bi bi-folder2-open"></i> Expedientes
</a>
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Asistencia" ? "active" : "")" asp-controller="Asistencia" asp-action="Index">
<i class="bi bi-calendar-check"></i> Asistencia
</a>
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Antecedentes" ? "active" : "")" asp-controller="Antecedentes" asp-action="Index">
<i class="bi bi-journal-text"></i> Antecedentes
</a>
<div class="nav-section-title">Administración</div>
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Usuario" ? "active" : "")" asp-controller="Usuario" asp-action="Index">
<i class="bi bi-people"></i> Usuarios
</a>
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Configuracion" ? "active" : "")" asp-controller="Configuracion" asp-action="Index">
<i class="bi bi-gear"></i> Configuración
</a>
</nav> </nav>
<div class="sidebar-footer p-3 border-top border-secondary"> <div class="sidebar-footer p-3 border-top border-secondary">
<small class="text-muted">v1.0.0 &copy; 2025</small> <small class="text-muted">v1.0.0 &copy; 2025</small>

View File

@@ -38,9 +38,27 @@
<span asp-validation-for="Email" class="text-danger small"></span> <span asp-validation-for="Email" class="text-danger small"></span>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label asp-for="Telefono" class="form-label fw-semibold"></label> <label asp-for="Telefono" class="form-label"></label>
<input asp-for="Telefono" class="form-control" /> <input asp-for="Telefono" class="form-control" />
<span asp-validation-for="Telefono" class="text-danger small"></span> <span asp-validation-for="Telefono" class="text-danger"></span>
</div>
<div class="col-12 mt-4">
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-person-badge me-2"></i>Asignación de Roles</h6>
<div class="row g-3">
@foreach (var role in ViewBag.Roles as List<foundation_system.Models.RolSistema>)
{
<div class="col-md-4">
<div class="form-check card p-2 border-light shadow-sm">
<input class="form-check-input ms-0 me-2" type="checkbox" name="SelectedRoles" value="@role.Id" id="role_@role.Id">
<label class="form-check-label" for="role_@role.Id">
<strong>@role.Nombre</strong><br />
<small class="text-muted">@role.Descripcion</small>
</label>
</div>
</div>
}
</div>
</div> </div>
</div> </div>

View File

@@ -43,6 +43,25 @@
<input asp-for="Telefono" class="form-control" /> <input asp-for="Telefono" class="form-control" />
<span asp-validation-for="Telefono" class="text-danger small"></span> <span asp-validation-for="Telefono" class="text-danger small"></span>
</div> </div>
<div class="col-12 mt-4">
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-person-badge me-2"></i>Asignación de Roles</h6>
<div class="row g-3">
@foreach (var role in ViewBag.Roles as List<foundation_system.Models.RolSistema>)
{
<div class="col-md-4">
<div class="form-check card p-2 border-light shadow-sm">
<input class="form-check-input ms-0 me-2" type="checkbox" name="SelectedRoles" value="@role.Id" id="role_@role.Id"
@(Model.SelectedRoles.Contains(role.Id) ? "checked" : "")>
<label class="form-check-label" for="role_@role.Id">
<strong>@role.Nombre</strong><br />
<small class="text-muted">@role.Descripcion</small>
</label>
</div>
</div>
}
</div>
</div>
</div> </div>
<h5 class="mb-4 text-primary border-bottom pb-2">Configuración de Cuenta</h5> <h5 class="mb-4 text-primary border-bottom pb-2">Configuración de Cuenta</h5>