911 lines
34 KiB
Plaintext
911 lines
34 KiB
Plaintext
@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 text-white" style="background: linear-gradient(180deg, #2c3e50 0%, #1a2530 100%);">
|
||
<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 w-100" style="background: linear-gradient(180deg, #2c3e50 0%, #1a2530 100%); color: white;">
|
||
<i class="fas fa-search text-white"></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-hover table-sm" id="tablaAsistencia">
|
||
<thead class="table-dark sticky-top">
|
||
<tr>
|
||
<th class="sticky-left bg-dark text-white" style="min-width: 200px;">
|
||
<div>Nombre</div>
|
||
<div class="small">Edad</div>
|
||
</th>
|
||
@foreach (var dia in Model.DiasDelMes)
|
||
{
|
||
var diaSemana = ((int)dia.DayOfWeek).ToString();
|
||
var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true;
|
||
|
||
if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var nombreDia = dia.ToString("ddd", new System.Globalization.CultureInfo("es-ES"));
|
||
var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday;
|
||
|
||
<th class="text-center @(esFinDeSemana ? "" : "")" style="min-width: 50px;">
|
||
<div>@dia.Day</div>
|
||
<div class="small">@nombreDia</div>
|
||
</th>
|
||
}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var expediente in Model.Expedientes)
|
||
{
|
||
<!-- Calcular propiedades aquí mismo -->
|
||
|
||
var nombreCompleto = $"{expediente.Nombre} {expediente.Apellidos}".Trim();
|
||
var edad = 0;
|
||
if (expediente.FechaNacimiento != DateTime.MinValue)
|
||
{
|
||
var today = DateTime.Today;
|
||
edad = today.Year - expediente.FechaNacimiento.Year;
|
||
if (expediente.FechaNacimiento.Date > today.AddYears(-edad))
|
||
{
|
||
edad--;
|
||
}
|
||
}
|
||
|
||
<tr data-expediente-id="@expediente.Id">
|
||
<td class="sticky-left bg-white" style="min-width: 200px;">
|
||
<div class="fw-bold">@nombreCompleto</div>
|
||
<div class="small text-muted">Edad: @edad años</div>
|
||
</td>
|
||
|
||
@foreach (var dia in Model.DiasDelMes)
|
||
{
|
||
var diaSemana = ((int)dia.DayOfWeek).ToString();
|
||
var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true;
|
||
|
||
if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var key = $"{expediente.Id}_{dia:yyyy-MM-dd}";
|
||
var estadoActual = Model.Asistencias.ContainsKey(key)
|
||
? Model.Asistencias[key]
|
||
: "";
|
||
|
||
// Determinar clase CSS según estado
|
||
var claseEstado = estadoActual switch
|
||
{
|
||
"P" => "celda-presente",
|
||
"T" => "celda-tarde",
|
||
"F" => "celda-falta",
|
||
_ => ""
|
||
};
|
||
|
||
var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday;
|
||
|
||
<td class="text-center p-0 celda-asistencia @claseEstado @(esFinDeSemana ? "bg-light" : "")"
|
||
data-expediente="@expediente.Id"
|
||
data-fecha="@dia.ToString("yyyy-MM-dd")"
|
||
style="min-width: 50px;">
|
||
|
||
<select class="form-select form-select-sm estado-select border-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>
|
||
/* Estilos generales de la tabla */
|
||
.table th, .table td {
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* Colores para las celdas según estado */
|
||
.celda-presente {
|
||
background-color: #d1e7dd !important; /* Verde claro */
|
||
}
|
||
|
||
.celda-tarde {
|
||
background-color: #fff3cd !important; /* Amarillo claro */
|
||
}
|
||
|
||
.celda-falta {
|
||
background-color: #f8d7da !important; /* Rojo claro */
|
||
}
|
||
|
||
/* Estilos para el select */
|
||
.estado-select {
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border: 1px solid transparent !important;
|
||
text-align: center;
|
||
-webkit-appearance: none;
|
||
-moz-appearance: none;
|
||
appearance: none;
|
||
}
|
||
|
||
/* Ocultar flecha del select */
|
||
.estado-select::-ms-expand {
|
||
display: none;
|
||
}
|
||
|
||
.estado-select:focus {
|
||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||
outline: none;
|
||
}
|
||
|
||
/* Colores para el select según estado */
|
||
.estado-select.bg-success {
|
||
background-color: #198754 !important;
|
||
color: white !important;
|
||
border-color: #198754 !important;
|
||
}
|
||
|
||
.estado-select.bg-warning {
|
||
background-color: #ffc107 !important;
|
||
color: black !important;
|
||
border-color: #ffc107 !important;
|
||
}
|
||
|
||
.estado-select.bg-danger {
|
||
background-color: #dc3545 !important;
|
||
color: white !important;
|
||
border-color: #dc3545 !important;
|
||
}
|
||
|
||
/* Select vacío */
|
||
.estado-select:not([value]):not([value=""]) {
|
||
background-color: white !important;
|
||
color: black !important;
|
||
}
|
||
|
||
/* Celdas cambiadas (no guardadas) */
|
||
.celda-asistencia.changed {
|
||
outline: 2px solid #0d6efd;
|
||
outline-offset: -2px;
|
||
}
|
||
|
||
/* Posición fija para la primera columna */
|
||
.sticky-left {
|
||
position: sticky;
|
||
left: 0;
|
||
background: white;
|
||
z-index: 5;
|
||
}
|
||
|
||
/* Estilos para checkboxes de días */
|
||
.dias-semana-checkboxes {
|
||
background: #f8f9fa;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
/* Colores para días de semana en encabezados */
|
||
.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;
|
||
}
|
||
|
||
/* Estilos para encabezados fijos */
|
||
.table-dark.sticky-top {
|
||
z-index: 10;
|
||
}
|
||
|
||
/* Estilos para tarjetas de estadísticas */
|
||
.card .display-6 {
|
||
font-size: 2rem;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* Progress bars en estadísticas */
|
||
.progress {
|
||
background-color: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
/* Hover effects para celdas */
|
||
.celda-asistencia:hover {
|
||
background-color: #f8f9fa !important;
|
||
}
|
||
|
||
.celda-presente:hover {
|
||
background-color: #c3e6cb !important;
|
||
}
|
||
|
||
.celda-tarde:hover {
|
||
background-color: #ffeaa7 !important;
|
||
}
|
||
|
||
.celda-falta:hover {
|
||
background-color: #f5c6cb !important;
|
||
}
|
||
|
||
/* Responsive adjustments */
|
||
@@media (max-width: 768px) {
|
||
.table-responsive
|
||
|
||
{
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.estado-select {
|
||
padding: 0.25rem;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.dias-semana-checkboxes .form-check-inline {
|
||
display: block;
|
||
margin-right: 0;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
}
|
||
|
||
/* Animación suave para cambios de color */
|
||
.celda-asistencia, .estado-select {
|
||
transition: background-color 0.3s ease, color 0.3s ease;
|
||
}
|
||
|
||
/* Estilos para fin de semana */
|
||
.bg-light {
|
||
background-color: #f8f9fa !important;
|
||
}
|
||
|
||
/* Leyenda de colores */
|
||
.leyenda .badge {
|
||
font-size: 0.8rem;
|
||
padding: 0.4em 0.8em;
|
||
}
|
||
|
||
/* Cambia de 50px a 65px */
|
||
.celda-asistencia {
|
||
min-width: 65px !important; /* ← Cambiado de 50px */
|
||
}
|
||
|
||
.table th.text-center {
|
||
min-width: 65px !important; /* ← Cambiado de 50px */
|
||
}
|
||
|
||
/* Cambia de 200px a 240px */
|
||
.sticky-left {
|
||
min-width: 240px !important; /* ← Cambiado de 200px */
|
||
}
|
||
|
||
/* Aumenta el padding del select */
|
||
.estado-select {
|
||
padding: 10px 4px !important; /* ← Más padding */
|
||
font-size: 14px !important; /* ← Texto más grande */
|
||
}
|
||
|
||
</style>
|
||
}
|
||
|
||
@section Scripts {
|
||
<script>
|
||
$(document).ready(function() {
|
||
// Actualizar estadísticas al cargar
|
||
cargarEstadisticas();
|
||
|
||
// Manejar checkboxes de días de semana
|
||
$('.dia-checkbox').change(function() {
|
||
var diasSeleccionados = $('.dia-checkbox:checked').map(function() {
|
||
return $(this).val();
|
||
}).get().join(',');
|
||
$('#diasSemanaInput').val(diasSeleccionados);
|
||
});
|
||
|
||
// Inicializar colores al cargar
|
||
inicializarColores();
|
||
|
||
// Guardar asistencia individual y actualizar colores
|
||
$('.estado-select').change(function() {
|
||
var estado = $(this).val();
|
||
var celda = $(this).closest('.celda-asistencia');
|
||
var initial = $(this).data('initial');
|
||
|
||
// Actualizar colores
|
||
aplicarColorSelect($(this), estado);
|
||
aplicarColorCelda(celda, estado);
|
||
|
||
// Marcar como cambiado si es diferente
|
||
if (initial !== estado) {
|
||
celda.addClass('changed');
|
||
} else {
|
||
celda.removeClass('changed');
|
||
}
|
||
|
||
// Guardar automáticamente
|
||
var expedienteId = celda.data('expediente');
|
||
var fecha = celda.data('fecha');
|
||
|
||
guardarAsistencia(expedienteId, fecha, estado, celda);
|
||
});
|
||
|
||
// Guardar todas las asistencias cambiadas
|
||
$('#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
|
||
});
|
||
});
|
||
|
||
// Enviar cambios masivos
|
||
$.ajax({
|
||
url: '@Url.Action("GuardarAsistenciasMasivas", "Asistencia")',
|
||
type: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify(cambios),
|
||
success: function(response) {
|
||
$('#modalCarga').modal('hide');
|
||
if (response.success) {
|
||
// Quitar marca de cambiado y actualizar estado inicial
|
||
$('.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');
|
||
}
|
||
});
|
||
});
|
||
|
||
// Exportar a Excel
|
||
$('#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');
|
||
});
|
||
|
||
// Cambiar mes/año con selects
|
||
$('#selectAnio, #selectMes').change(function() {
|
||
$('#filtroForm').submit();
|
||
});
|
||
|
||
// Aplicar colores a días de semana
|
||
aplicarColoresDias();
|
||
});
|
||
|
||
// Función para aplicar color a una celda según el estado
|
||
function aplicarColorCelda(celda, estado) {
|
||
// Remover todas las clases de color
|
||
celda.removeClass('celda-presente celda-tarde celda-falta');
|
||
|
||
// Aplicar nueva clase según estado
|
||
switch(estado) {
|
||
case 'P':
|
||
celda.addClass('celda-presente');
|
||
break;
|
||
case 'T':
|
||
celda.addClass('celda-tarde');
|
||
break;
|
||
case 'F':
|
||
celda.addClass('celda-falta');
|
||
break;
|
||
default:
|
||
// Vacío - no aplicar color
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Función para aplicar color al select según estado
|
||
function aplicarColorSelect(select, estado) {
|
||
// Remover todas las clases de color
|
||
select.removeClass('bg-success bg-warning bg-danger text-white');
|
||
|
||
// Aplicar nueva clase según estado
|
||
switch(estado) {
|
||
case 'P':
|
||
select.addClass('bg-success text-white');
|
||
break;
|
||
case 'T':
|
||
select.addClass('bg-warning');
|
||
break;
|
||
case 'F':
|
||
select.addClass('bg-danger text-white');
|
||
break;
|
||
default:
|
||
// Vacío - color por defecto
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Función para inicializar colores al cargar la página
|
||
function inicializarColores() {
|
||
$('.estado-select').each(function() {
|
||
var estado = $(this).val();
|
||
var celda = $(this).closest('.celda-asistencia');
|
||
|
||
aplicarColorSelect($(this), estado);
|
||
aplicarColorCelda(celda, estado);
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// FUNCIONES PARA GUARDAR ASISTENCIA
|
||
// ============================================
|
||
|
||
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 correctamente');
|
||
cargarEstadisticas();
|
||
} else {
|
||
toastr.error(response.message);
|
||
// Revertir color si hay error
|
||
var select = celda.find('.estado-select');
|
||
var initial = select.data('initial');
|
||
aplicarColorSelect(select, initial);
|
||
aplicarColorCelda(celda, initial);
|
||
select.val(initial);
|
||
}
|
||
},
|
||
error: function() {
|
||
toastr.error('Error de conexión');
|
||
// Revertir color
|
||
var select = celda.find('.estado-select');
|
||
var initial = select.data('initial');
|
||
aplicarColorSelect(select, initial);
|
||
aplicarColorCelda(celda, initial);
|
||
select.val(initial);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// FUNCIONES PARA ESTADÍSTICAS
|
||
// ============================================
|
||
|
||
function cargarEstadisticas() {
|
||
var año = $('#selectAnio').val();
|
||
var mes = $('#selectMes').val();
|
||
|
||
// Mostrar indicador de carga
|
||
$('#estadisticasContainer').html(`
|
||
<div class="col-md-12 text-center">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Cargando...</span>
|
||
</div>
|
||
<p class="mt-2">Cargando estadísticas...</p>
|
||
</div>
|
||
`);
|
||
|
||
$.ajax({
|
||
url: '@Url.Action("ObtenerEstadisticas", "Asistencia")',
|
||
type: 'GET',
|
||
data: { año: año, mes: mes },
|
||
success: function(data) {
|
||
// Verificar si data es null o undefined
|
||
if (!data) {
|
||
mostrarEstadisticasVacias();
|
||
return;
|
||
}
|
||
|
||
// Verificar si hay error
|
||
if (data.error) {
|
||
console.error('Error del servidor:', data.error);
|
||
mostrarEstadisticasVacias('Error: ' + data.error);
|
||
return;
|
||
}
|
||
|
||
// Extraer valores con manejo seguro
|
||
var total = extraerNumero(data, 'total');
|
||
var presentes = extraerNumero(data, 'presentes');
|
||
var tardes = extraerNumero(data, 'tardes');
|
||
var faltas = extraerNumero(data, 'faltas');
|
||
|
||
// Verificar consistencia
|
||
var suma = presentes + tardes + faltas;
|
||
if (total > 0 && suma !== total) {
|
||
console.warn('Inconsistencia en datos: total=' + total + ', suma=' + suma);
|
||
total = suma;
|
||
}
|
||
|
||
// Calcular porcentajes
|
||
var porcentajePresentes = calcularPorcentaje(presentes, total);
|
||
var porcentajeTardes = calcularPorcentaje(tardes, total);
|
||
var porcentajeFaltas = calcularPorcentaje(faltas, total);
|
||
|
||
// Mostrar estadísticas
|
||
mostrarEstadisticasHTML(total, presentes, tardes, faltas,
|
||
porcentajePresentes, porcentajeTardes, porcentajeFaltas);
|
||
},
|
||
error: function(xhr, status, error) {
|
||
console.error('Error AJAX:', status, error);
|
||
mostrarEstadisticasVacias('Error al cargar estadísticas');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Función auxiliar para extraer números de forma segura
|
||
function extraerNumero(obj, propiedad) {
|
||
if (!obj || obj[propiedad] === null || obj[propiedad] === undefined) {
|
||
return 0;
|
||
}
|
||
|
||
var valor = obj[propiedad];
|
||
|
||
// Convertir a número si es string
|
||
if (typeof valor === 'string') {
|
||
valor = parseFloat(valor);
|
||
}
|
||
|
||
// Verificar que sea un número válido
|
||
if (isNaN(valor) || !isFinite(valor)) {
|
||
return 0;
|
||
}
|
||
|
||
// Redondear a entero si es decimal
|
||
return Math.round(valor);
|
||
}
|
||
|
||
// Función para calcular porcentajes
|
||
function calcularPorcentaje(parcial, total) {
|
||
if (!total || total <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
var porcentaje = (parcial / total) * 100;
|
||
|
||
// Redondear a 1 decimal
|
||
return Math.round(porcentaje * 10) / 10;
|
||
}
|
||
|
||
// Función para mostrar estadísticas vacías o con error
|
||
function mostrarEstadisticasVacias(mensaje) {
|
||
var mensajeMostrar = mensaje || 'No hay datos de asistencia para este período';
|
||
|
||
$('#estadisticasContainer').html(`
|
||
<div class="col-md-12">
|
||
<div class="alert alert-warning">
|
||
<i class="fas fa-exclamation-triangle"></i> ${mensajeMostrar}
|
||
</div>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
// Función para mostrar estadísticas en HTML
|
||
function mostrarEstadisticasHTML(total, presentes, tardes, faltas,
|
||
porcentajePresentes, porcentajeTardes, porcentajeFaltas) {
|
||
|
||
// Calcular porcentaje de asistencia general
|
||
var asistenciaGeneral = total > 0 ?
|
||
((presentes + tardes) / 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">
|
||
<i class="fas fa-check-circle"></i> Presentes
|
||
</h6>
|
||
<h2 class="display-6">${presentes.toLocaleString()}</h2>
|
||
<div class="mt-2">
|
||
<small>${porcentajePresentes}%</small>
|
||
<div class="progress mt-1" style="height: 5px;">
|
||
<div class="progress-bar bg-light"
|
||
style="width: ${porcentajePresentes}%"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="card bg-warning">
|
||
<div class="card-body text-center">
|
||
<h6 class="card-title">
|
||
<i class="fas fa-clock"></i> Tardes
|
||
</h6>
|
||
<h2 class="display-6">${tardes.toLocaleString()}</h2>
|
||
<div class="mt-2">
|
||
<small>${porcentajeTardes}%</small>
|
||
<div class="progress mt-1" style="height: 5px;">
|
||
<div class="progress-bar bg-dark"
|
||
style="width: ${porcentajeTardes}%"></div>
|
||
</div>
|
||
</div>
|
||
</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">
|
||
<i class="fas fa-times-circle"></i> Faltas
|
||
</h6>
|
||
<h2 class="display-6">${faltas.toLocaleString()}</h2>
|
||
<div class="mt-2">
|
||
<small>${porcentajeFaltas}%</small>
|
||
<div class="progress mt-1" style="height: 5px;">
|
||
<div class="progress-bar bg-light"
|
||
style="width: ${porcentajeFaltas}%"></div>
|
||
</div>
|
||
</div>
|
||
</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">
|
||
<i class="fas fa-chart-bar"></i> Resumen
|
||
</h6>
|
||
<h2 class="display-6">${total.toLocaleString()}</h2>
|
||
<div class="mt-2">
|
||
<small>Asistencia: ${asistenciaGeneral}%</small>
|
||
<div class="progress mt-1" style="height: 5px;">
|
||
<div class="progress-bar bg-light"
|
||
style="width: ${asistenciaGeneral}%"></div>
|
||
</div>
|
||
<div class="small mt-2">
|
||
<div>Presentes: ${presentes}</div>
|
||
<div>Tardes: ${tardes}</div>
|
||
<div>Faltas: ${faltas}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`);
|
||
}
|
||
|
||
// ============================================
|
||
// FUNCIONES ADICIONALES
|
||
// ============================================
|
||
|
||
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>
|
||
}
|