This commit is contained in:
2025-12-25 13:54:49 -06:00
parent d405b61ddd
commit 3457721238
26 changed files with 3509 additions and 139 deletions

View File

@@ -1,5 +1,495 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@model MieSystem.Models.ViewModels.AsistenciaViewModel
@{
ViewData["Title"] = "Control 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();
}
<div class="container-fluid">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-filter"></i> Filtros</h5>
</div>
<div class="card-body">
<form method="get" id="filtroForm" class="row g-3">
<div class="col-md-3">
<label class="form-label">Año</label>
<select name="año" class="form-select" id="selectAnio">
@foreach (var año in ViewBag.Años)
{
<option value="@año.Value" selected="@año.Selected">
@año.Text
</option>
}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Mes</label>
<select name="mes" class="form-select" id="selectMes">
@foreach (var mes in ViewBag.Meses)
{
<option value="@mes.Value" selected="@(mes.Value == Model.Mes.ToString())">
@mes.Text
</option>
}
</select>
</div>
<div class="col-md-4">
<label class="form-label">Días de la semana</label>
<div class="dias-semana-checkboxes">
@foreach (var dia in ViewBag.DiasSemana)
{
var isChecked = diasSeleccionadosList.Contains(dia.Value);
<div class="form-check form-check-inline">
<input class="form-check-input dia-checkbox"
type="checkbox"
value="@dia.Value"
id="dia@(dia.Value)"
@(isChecked ? "checked" : "")>
<label class="form-check-label" for="dia@(dia.Value)">
@dia.Text
</label>
</div>
}
<input type="hidden" name="diasSemana" id="diasSemanaInput"
value="@Model.DiasSemanaSeleccionados">
</div>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search"></i> Filtrar
</button>
</div>
</form>
</div>
</div>
<div class="row mb-4" id="estadisticasContainer">
</div>
<div class="card">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-calendar-check"></i>
Asistencia - @Model.NombreMes @Model.Año
<span class="badge bg-light text-dark ms-2">@Model.Expedientes.Count niños</span>
</h5>
<div>
<button class="btn btn-light btn-sm" id="btnGuardarTodo">
<i class="fas fa-save"></i> Guardar todo
</button>
<button class="btn btn-light btn-sm" id="btnExportar">
<i class="fas fa-file-excel"></i> Exportar
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 70vh; overflow-y: auto;">
<table class="table table-bordered table-striped mb-0" id="tablaAsistencia">
<thead class="table-dark sticky-top" style="top: 0;">
<tr>
<th style="min-width: 200px; position: sticky; left: 0; background: #343a40; z-index: 10;">
Niño
</th>
@foreach (var dia in diasAMostrar)
{
var esFinSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday;
<th class="text-center @(esFinSemana ? "" : "")"
style="min-width: 60px;">
<div class="small">@dia.ToString("ddd")</div>
<div class="fw-bold">@dia.Day</div>
</th>
}
</tr>
</thead>
<tbody>
@foreach (var expediente in Model.Expedientes)
{
<tr data-expediente-id="@expediente.Id">
<td style="position: sticky; left: 0; background: white; z-index: 5;">
<div class="fw-bold">@expediente.NombreCompleto</div>
<div class="small text-muted">Edad: @expediente.Edad años</div>
</td>
@foreach (var dia in diasAMostrar)
{
var key = $"{expediente.Id}_{dia:yyyy-MM-dd}";
var estadoActual = Model.Asistencias.ContainsKey(key)
? Model.Asistencias[key]
: "";
<td class="text-center p-0 celda-asistencia"
data-expediente="@expediente.Id"
data-fecha="@dia.ToString("yyyy-MM-dd")">
<select class="form-select form-select-sm estado-select border-0 rounded-0 h-100 w-100"
data-initial="@estadoActual">
<option value=""></option>
@if (estadoActual == "P")
{
<option value="P" selected class="bg-success text-white">P</option>
}
else
{
<option value="P" class="bg-success text-white">P</option>
}
@if (estadoActual == "T")
{
<option value="T" selected class="bg-warning">T</option>
}
else
{
<option value="T" class="bg-warning">T</option>
}
@if (estadoActual == "F")
{
<option value="F" selected class="bg-danger text-white">F</option>
}
else
{
<option value="F" class="bg-danger text-white">F</option>
}
</select>
</td>
}
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="card-footer">
<div class="row">
<div class="col-md-6">
<div class="leyenda">
<span class="badge bg-success me-2">P = Presente</span>
<span class="badge bg-warning me-2">T = Tarde</span>
<span class="badge bg-danger me-2">F = Falto</span>
<span class="text-muted ms-3">(Vacío = No registrado)</span>
</div>
</div>
<div class="col-md-6 text-end">
<small class="text-muted">
Total: @Model.Expedientes.Count niños × @diasAMostrar.Count días =
@(Model.Expedientes.Count * diasAMostrar.Count) registros posibles
</small>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="modalCarga" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-body text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p class="mt-2">Guardando cambios...</p>
</div>
</div>
</div>
</div>
@section Styles {
<style>
.table th, .table td {
vertical-align: middle;
}
.estado-select {
cursor: pointer;
transition: all 0.2s;
}
.estado-select:hover {
transform: scale(1.05);
box-shadow: 0 0 5px rgba(0,0,0,0.2);
}
.estado-select option[value="P"] {
background-color: #198754 !important;
color: white !important;
}
.estado-select option[value="T"] {
background-color: #ffc107 !important;
color: black !important;
}
.estado-select option[value="F"] {
background-color: #dc3545 !important;
color: white !important;
}
.celda-asistencia.changed {
outline: 2px solid #0d6efd;
outline-offset: -2px;
}
.sticky-left {
position: sticky;
left: 0;
background: white;
z-index: 5;
}
.dias-semana-checkboxes {
background: #f8f9fa;
padding: 10px;
border-radius: 5px;
border: 1px solid #dee2e6;
}
.bg-lunes { background-color: #e3f2fd !important; }
.bg-martes { background-color: #f3e5f5 !important; }
.bg-miercoles { background-color: #e8f5e8 !important; }
.bg-jueves { background-color: #fff3e0 !important; }
.bg-viernes { background-color: #fce4ec !important; }
.bg-sabado { background-color: #f1f8e9 !important; }
.bg-domingo { background-color: #fff8e1 !important; }
</style>
}
@section Scripts {
<script>
$(document).ready(function() {
$('.dia-checkbox').change(function() {
var diasSeleccionados = $('.dia-checkbox:checked').map(function() {
return $(this).val();
}).get().join(',');
$('#diasSemanaInput').val(diasSeleccionados);
});
$('.estado-select').change(function() {
var initial = $(this).data('initial');
var current = $(this).val();
if (initial !== current) {
$(this).closest('.celda-asistencia').addClass('changed');
} else {
$(this).closest('.celda-asistencia').removeClass('changed');
}
});
$('.estado-select').change(function() {
var celda = $(this).closest('.celda-asistencia');
var expedienteId = celda.data('expediente');
var fecha = celda.data('fecha');
var estado = $(this).val();
guardarAsistencia(expedienteId, fecha, estado, celda);
});
$('#btnGuardarTodo').click(function() {
var cambios = [];
var celdasCambiadas = $('.celda-asistencia.changed');
if (celdasCambiadas.length === 0) {
toastr.info('No hay cambios para guardar');
return;
}
if (!confirm(`¿Guardar ${celdasCambiadas.length} cambios?`)) {
return;
}
$('#modalCarga').modal('show');
celdasCambiadas.each(function() {
var celda = $(this);
var select = celda.find('.estado-select');
var expedienteId = celda.data('expediente');
var fecha = celda.data('fecha');
var estado = select.val();
cambios.push({
expedienteId: expedienteId,
fecha: fecha,
estado: estado
});
});
$.ajax({
url: '@Url.Action("GuardarAsistenciasMasivas", "Asistencia")',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(cambios),
success: function(response) {
$('#modalCarga').modal('hide');
if (response.success) {
$('.celda-asistencia').removeClass('changed');
$('.estado-select').each(function() {
$(this).data('initial', $(this).val());
});
toastr.success(response.message);
cargarEstadisticas();
} else {
toastr.error(response.message);
}
},
error: function() {
$('#modalCarga').modal('hide');
toastr.error('Error al guardar los cambios');
}
});
});
$('#btnExportar').click(function() {
var año = $('#selectAnio').val();
var mes = $('#selectMes').val();
var diasSemana = $('#diasSemanaInput').val();
var url = '@Url.Action("ExportarExcel", "Asistencia")' +
'?año=' + año +
'&mes=' + mes +
'&diasSemana=' + diasSemana;
window.open(url, '_blank');
});
$('#selectAnio, #selectMes').change(function() {
$('#filtroForm').submit();
});
aplicarColoresDias();
});
function guardarAsistencia(expedienteId, fecha, estado, celda) {
$.ajax({
url: '@Url.Action("GuardarAsistencia", "Asistencia")',
type: 'POST',
data: {
expedienteId: expedienteId,
fecha: fecha,
estado: estado
},
success: function(response) {
if (response.success) {
celda.removeClass('changed');
celda.find('.estado-select').data('initial', estado);
toastr.success('Guardado');
cargarEstadisticas();
} else {
toastr.error(response.message);
}
},
error: function() {
toastr.error('Error al guardar');
}
});
}
function cargarEstadisticas() {
var año = $('#selectAnio').val();
var mes = $('#selectMes').val();
$.ajax({
url: '@Url.Action("ObtenerEstadisticas", "Asistencia")',
type: 'GET',
data: { año: año, mes: mes },
success: function(data) {
if (data.error) {
console.error(data.error);
return;
}
var total = data.total || 0;
var presentes = data.presentes || 0;
var tardes = data.tardes || 0;
var faltas = data.faltas || 0;
var porcentajePresentes = total > 0 ? ((presentes / total) * 100).toFixed(1) : 0;
var porcentajeTardes = total > 0 ? ((tardes / total) * 100).toFixed(1) : 0;
var porcentajeFaltas = total > 0 ? ((faltas / total) * 100).toFixed(1) : 0;
$('#estadisticasContainer').html(`
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h6 class="card-title">Presentes</h6>
<h2>${presentes}</h2>
<small>${porcentajePresentes}%</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning">
<div class="card-body text-center">
<h6 class="card-title">Tardes</h6>
<h2>${tardes}</h2>
<small>${porcentajeTardes}%</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-danger text-white">
<div class="card-body text-center">
<h6 class="card-title">Faltas</h6>
<h2>${faltas}</h2>
<small>${porcentajeFaltas}%</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body text-center">
<h6 class="card-title">Total Registros</h6>
<h2>${total}</h2>
<small>Asistencia: ${porcentajePresentes}%</small>
</div>
</div>
</div>
`);
},
error: function() {
console.error('Error cargando estadísticas');
}
});
}
function aplicarColoresDias() {
$('th.text-center').each(function() {
var textoDia = $(this).find('.small').text().trim();
var claseColor = '';
switch(textoDia) {
case 'Lun': claseColor = 'bg-lunes'; break;
case 'Mar': claseColor = 'bg-martes'; break;
case 'Mié': claseColor = 'bg-miercoles'; break;
case 'Jue': claseColor = 'bg-jueves'; break
case 'Vie': claseColor = 'bg-viernes'; break;
case 'Sáb': claseColor = 'bg-sabado'; break;
case 'Dom': claseColor = 'bg-domingo'; break;
}
if (claseColor) {
$(this).addClass(claseColor);
}
});
}
</script>
}

View File

@@ -1,4 +1,5 @@
@model ExpedienteViewModel
@using MieSystem.Models.ViewModels
@model ExpedienteViewModel
@{
Layout = null;
ViewData["Title"] = "Expediente - " + Model.NombreCompleto;

View File

@@ -1,4 +1,5 @@
@{
@using MieSystem.Models.ViewModels
@{
ViewData["Title"] = "Expedientes";
ViewData["ActionButtons"] = @"<button class='btn btn-primary' data-bs-toggle='modal' data-bs-target='#createModal'>
<i class='bi bi-plus-circle me-1'></i> Nuevo Expediente
@@ -104,7 +105,7 @@
</div>
<!-- Modal para crear nuevo expediente -->
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true" >
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
@@ -262,7 +263,7 @@
<button class="btn btn-sm btn-outline-warning" onclick="editExpediente(${expediente.id})" title="Editar">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteExpediente(${expediente.id})" title="Eliminar">
<button style="visibility:hidden" class="btn btn-sm btn-outline-danger" onclick="deleteExpediente(${expediente.id})" title="Eliminar">
<i class="bi bi-trash"></i>
</button>
</div>

View File

@@ -1,4 +1,5 @@
@using MieSystem.Models;
@using MieSystem.Models.ViewModels
@model ExpedienteViewModel

View File

@@ -5,9 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - MieSystem</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.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/MieSystem.styles.css" asp-append-version="true" />
<!-- Toastr CSS -->
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/toastr.min.css" />
@await RenderSectionAsync("Styles", required: false)
<style>
/* Estilos personalizados para el menú lateral */
body {
@@ -200,6 +204,7 @@
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow">
<div class="container-fluid">
<button class="toggle-sidebar-btn" id="toggleSidebar">
@@ -319,10 +324,10 @@
</div>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/lib/bootstrap/dist/js/toastr.min.js"></script>
<script>
// Control del menú lateral