Compare commits
2 Commits
be7d5ef10d
...
3457721238
| Author | SHA1 | Date | |
|---|---|---|---|
| 3457721238 | |||
| d405b61ddd |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -361,3 +361,4 @@ MigrationBackup/
|
|||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
/MieSystem/wwwroot/uploads
|
||||||
|
|||||||
@@ -1,12 +1,261 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MieSystem.Data.Interfaces;
|
||||||
|
using MieSystem.Models;
|
||||||
|
using MieSystem.Models.ViewModels;
|
||||||
|
|
||||||
namespace MieSystem.Controllers
|
namespace MieSystem.Controllers
|
||||||
{
|
{
|
||||||
public class AsistenciaController : Controller
|
public class AsistenciaController : Controller
|
||||||
{
|
{
|
||||||
public IActionResult Index()
|
private readonly IExpedienteRepository _expedienteRepository;
|
||||||
|
private readonly IAsistenciaRepository _asistenciaRepository;
|
||||||
|
|
||||||
|
public AsistenciaController(
|
||||||
|
IExpedienteRepository expedienteRepository,
|
||||||
|
IAsistenciaRepository asistenciaRepository)
|
||||||
{
|
{
|
||||||
return View();
|
_expedienteRepository = expedienteRepository;
|
||||||
|
_asistenciaRepository = asistenciaRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Index(int? año, int? mes, string diasSemana = null)
|
||||||
|
{
|
||||||
|
// Valores por defecto: mes actual
|
||||||
|
var fechaActual = DateTime.Now;
|
||||||
|
var añoSeleccionado = año ?? fechaActual.Year;
|
||||||
|
var mesSeleccionado = mes ?? fechaActual.Month;
|
||||||
|
|
||||||
|
// Obtener todos los niños activos
|
||||||
|
var expedientes = await _expedienteRepository.GetActivosAsync();
|
||||||
|
|
||||||
|
// Obtener días del mes seleccionado
|
||||||
|
var diasDelMes = GetDiasDelMes(añoSeleccionado, mesSeleccionado);
|
||||||
|
|
||||||
|
// Filtrar por días de semana si se especifica
|
||||||
|
if (!string.IsNullOrEmpty(diasSemana))
|
||||||
|
{
|
||||||
|
var diasFiltro = diasSemana.Split(',')
|
||||||
|
.Select(d => int.Parse(d))
|
||||||
|
.ToList();
|
||||||
|
diasDelMes = diasDelMes.Where(d => diasFiltro.Contains((int)d.DayOfWeek)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener asistencias para el mes
|
||||||
|
var asistencias = await _asistenciaRepository.GetAsistenciasPorMesAsync(
|
||||||
|
añoSeleccionado, mesSeleccionado);
|
||||||
|
|
||||||
|
// Crear diccionario para acceso rápido
|
||||||
|
var dictAsistencias = new Dictionary<string, string>();
|
||||||
|
foreach (var asistencia in asistencias)
|
||||||
|
{
|
||||||
|
var key = $"{asistencia.ExpedienteId}_{asistencia.Fecha:yyyy-MM-dd}";
|
||||||
|
dictAsistencias[key] = asistencia.Estado;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear modelo de vista
|
||||||
|
var model = new AsistenciaViewModel
|
||||||
|
{
|
||||||
|
Año = añoSeleccionado,
|
||||||
|
Mes = mesSeleccionado,
|
||||||
|
NombreMes = GetNombreMes(mesSeleccionado),
|
||||||
|
DiasSemanaSeleccionados = diasSemana,
|
||||||
|
Expedientes = expedientes.ToList(),
|
||||||
|
DiasDelMes = diasDelMes,
|
||||||
|
Asistencias = dictAsistencias
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewBag.Meses = GetListaMeses();
|
||||||
|
ViewBag.Años = GetListaAños();
|
||||||
|
ViewBag.DiasSemana = GetDiasSemana();
|
||||||
|
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> GuardarAsistencia(int expedienteId, string fecha, string estado)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!DateTime.TryParse(fecha, out DateTime fechaDate))
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = "Fecha inválida" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var asistencia = new Asistencia
|
||||||
|
{
|
||||||
|
ExpedienteId = expedienteId,
|
||||||
|
Fecha = fechaDate,
|
||||||
|
Estado = estado,
|
||||||
|
UsuarioRegistro = User.Identity?.Name ?? "Sistema"
|
||||||
|
};
|
||||||
|
|
||||||
|
var resultado = await _asistenciaRepository.GuardarAsistenciaAsync(asistencia);
|
||||||
|
|
||||||
|
return Json(new { success = resultado, message = "Asistencia guardada" });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> GuardarAsistenciasMasivas([FromBody] List<AsistenciaMasivaDto> asistencias)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var asistenciasModel = new List<Asistencia>();
|
||||||
|
|
||||||
|
foreach (var asistenciaDto in asistencias)
|
||||||
|
{
|
||||||
|
var asistencia = new Asistencia
|
||||||
|
{
|
||||||
|
ExpedienteId = asistenciaDto.ExpedienteId,
|
||||||
|
Fecha = asistenciaDto.Fecha,
|
||||||
|
Estado = asistenciaDto.Estado,
|
||||||
|
UsuarioRegistro = User.Identity?.Name ?? "Sistema"
|
||||||
|
};
|
||||||
|
asistenciasModel.Add(asistencia);
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultado = await _asistenciaRepository.GuardarAsistenciasMasivasAsync(asistenciasModel);
|
||||||
|
|
||||||
|
return Json(new
|
||||||
|
{
|
||||||
|
success = resultado,
|
||||||
|
message = resultado ? "Asistencias guardadas correctamente" : "Error al guardar asistencias"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> ObtenerEstadisticas(int año, int mes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var estadisticas = await _asistenciaRepository.GetEstadisticasMesAsync(año, mes);
|
||||||
|
return Json(estadisticas);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Json(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> ExportarExcel(int año, int mes, string diasSemana = null)
|
||||||
|
{
|
||||||
|
// Implementar exportación a Excel
|
||||||
|
// Por ahora solo redirige
|
||||||
|
return Content($"Exportar Excel: Año={año}, Mes={mes}, Días={diasSemana}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Métodos auxiliares privados
|
||||||
|
|
||||||
|
private List<DateTime> GetDiasDelMes(int año, int mes)
|
||||||
|
{
|
||||||
|
var dias = new List<DateTime>();
|
||||||
|
var fecha = new DateTime(año, mes, 1);
|
||||||
|
var ultimoDia = fecha.AddMonths(1).AddDays(-1);
|
||||||
|
|
||||||
|
for (var dia = fecha; dia <= ultimoDia; dia = dia.AddDays(1))
|
||||||
|
{
|
||||||
|
dias.Add(dia);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dias;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetNombreMes(int mes)
|
||||||
|
{
|
||||||
|
return mes switch
|
||||||
|
{
|
||||||
|
1 => "Enero",
|
||||||
|
2 => "Febrero",
|
||||||
|
3 => "Marzo",
|
||||||
|
4 => "Abril",
|
||||||
|
5 => "Mayo",
|
||||||
|
6 => "Junio",
|
||||||
|
7 => "Julio",
|
||||||
|
8 => "Agosto",
|
||||||
|
9 => "Septiembre",
|
||||||
|
10 => "Octubre",
|
||||||
|
11 => "Noviembre",
|
||||||
|
12 => "Diciembre",
|
||||||
|
_ => "Mes inválido"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SelectListItem> GetListaMeses()
|
||||||
|
{
|
||||||
|
return new List<SelectListItem>
|
||||||
|
{
|
||||||
|
new SelectListItem { Value = "1", Text = "Enero" },
|
||||||
|
new SelectListItem { Value = "2", Text = "Febrero" },
|
||||||
|
new SelectListItem { Value = "3", Text = "Marzo" },
|
||||||
|
new SelectListItem { Value = "4", Text = "Abril" },
|
||||||
|
new SelectListItem { Value = "5", Text = "Mayo" },
|
||||||
|
new SelectListItem { Value = "6", Text = "Junio" },
|
||||||
|
new SelectListItem { Value = "7", Text = "Julio" },
|
||||||
|
new SelectListItem { Value = "8", Text = "Agosto" },
|
||||||
|
new SelectListItem { Value = "9", Text = "Septiembre" },
|
||||||
|
new SelectListItem { Value = "10", Text = "Octubre" },
|
||||||
|
new SelectListItem { Value = "11", Text = "Noviembre" },
|
||||||
|
new SelectListItem { Value = "12", Text = "Diciembre" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SelectListItem> GetListaAños()
|
||||||
|
{
|
||||||
|
var años = new List<SelectListItem>();
|
||||||
|
var añoActual = DateTime.Now.Year;
|
||||||
|
|
||||||
|
for (int i = añoActual - 5; i <= añoActual + 1; i++)
|
||||||
|
{
|
||||||
|
años.Add(new SelectListItem
|
||||||
|
{
|
||||||
|
Value = i.ToString(),
|
||||||
|
Text = i.ToString(),
|
||||||
|
Selected = i == añoActual
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return años;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SelectListItem> GetDiasSemana()
|
||||||
|
{
|
||||||
|
return new List<SelectListItem>
|
||||||
|
{
|
||||||
|
new SelectListItem { Value = "1", Text = "Lunes" },
|
||||||
|
new SelectListItem { Value = "2", Text = "Martes" },
|
||||||
|
new SelectListItem { Value = "3", Text = "Miércoles" },
|
||||||
|
new SelectListItem { Value = "4", Text = "Jueves" },
|
||||||
|
new SelectListItem { Value = "5", Text = "Viernes" },
|
||||||
|
new SelectListItem { Value = "6", Text = "Sábado" },
|
||||||
|
new SelectListItem { Value = "0", Text = "Domingo" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Modelos de vista
|
||||||
|
|
||||||
|
public class SelectListItem
|
||||||
|
{
|
||||||
|
public string Value { get; set; }
|
||||||
|
public string Text { get; set; }
|
||||||
|
public bool Selected { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,9 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MieSystem.Models;
|
using MieSystem.Models;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using MieSystem.Models;
|
|
||||||
using MieSystem.Data.Interfaces;
|
using MieSystem.Data.Interfaces;
|
||||||
|
using AspNetCoreGeneratedDocument;
|
||||||
|
using MieSystem.Models.ViewModels;
|
||||||
|
|
||||||
namespace MieSystem.Controllers
|
namespace MieSystem.Controllers
|
||||||
{
|
{
|
||||||
@@ -25,104 +22,22 @@ namespace MieSystem.Controllers
|
|||||||
_expedienteRepository = expedienteRepository;
|
_expedienteRepository = expedienteRepository;
|
||||||
|
|
||||||
// Datos de prueba iniciales
|
// Datos de prueba iniciales
|
||||||
if (_expedientes.Count == 0)
|
/*if (_expedientes.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
||||||
_ = InitializeTestData();
|
_ = InitializeTestData();
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task InitializeTestData()
|
private async Task InitializeTestData()
|
||||||
{
|
{
|
||||||
var today = DateTime.Today;
|
var today = DateTime.Today;
|
||||||
var lst = await _expedienteRepository.GetAllAsync();
|
var lst = await _expedienteRepository.GetAllAsync();
|
||||||
_expedientes =new List<Expediente>(lst);
|
_expedientes = [.. lst];
|
||||||
|
|
||||||
/*_expedientes.Add(new Expediente
|
|
||||||
{
|
|
||||||
Id = _idCounter++,
|
|
||||||
Nombre = "Juan",
|
|
||||||
Apellidos = "Pérez García",
|
|
||||||
FechaNacimiento = new DateTime(2015, 5, 10),
|
|
||||||
NombrePadre = "Carlos Pérez",
|
|
||||||
NombreMadre = "María García",
|
|
||||||
NombreResponsable = "Carlos Pérez",
|
|
||||||
ParentescoResponsable = "Padre",
|
|
||||||
Sexo = "M",
|
|
||||||
Direccion = "Calle Principal 123, Ciudad",
|
|
||||||
Telefono = "555-1234",
|
|
||||||
Observaciones = "Niño tranquilo",
|
|
||||||
FotoUrl = "/images/default-avatar.png",
|
|
||||||
FechaCreacion = DateTime.Now.AddDays(-30),
|
|
||||||
FechaActualizacion = DateTime.Now.AddDays(-30),
|
|
||||||
Activo = true
|
|
||||||
});
|
|
||||||
|
|
||||||
_expedientes.Add(new Expediente
|
|
||||||
{
|
|
||||||
Id = _idCounter++,
|
|
||||||
Nombre = "Ana",
|
|
||||||
Apellidos = "López Martínez",
|
|
||||||
FechaNacimiento = new DateTime(2016, today.Month, 15), // Cumple este mes
|
|
||||||
NombrePadre = "Pedro López",
|
|
||||||
NombreMadre = "Laura Martínez",
|
|
||||||
NombreResponsable = "Laura Martínez",
|
|
||||||
ParentescoResponsable = "Madre",
|
|
||||||
Sexo = "F",
|
|
||||||
Direccion = "Avenida Central 456, Ciudad",
|
|
||||||
Telefono = "555-5678",
|
|
||||||
Observaciones = "Alergia a los frutos secos",
|
|
||||||
FotoUrl = "/images/default-avatar.png",
|
|
||||||
FechaCreacion = DateTime.Now.AddDays(-15),
|
|
||||||
FechaActualizacion = DateTime.Now.AddDays(-15),
|
|
||||||
Activo = true
|
|
||||||
});
|
|
||||||
|
|
||||||
_expedientes.Add(new Expediente
|
|
||||||
{
|
|
||||||
Id = _idCounter++,
|
|
||||||
Nombre = "Carlos",
|
|
||||||
Apellidos = "Rodríguez Sánchez",
|
|
||||||
FechaNacimiento = new DateTime(2008, 8, 20), // Mayor de 14 años
|
|
||||||
NombrePadre = "Javier Rodríguez",
|
|
||||||
NombreMadre = "Carmen Sánchez",
|
|
||||||
NombreResponsable = "Javier Rodríguez",
|
|
||||||
ParentescoResponsable = "Padre",
|
|
||||||
Sexo = "M",
|
|
||||||
Direccion = "Plaza Mayor 789, Ciudad",
|
|
||||||
Telefono = "555-9012",
|
|
||||||
Observaciones = "Excelente estudiante",
|
|
||||||
FotoUrl = "/images/default-avatar.png",
|
|
||||||
FechaCreacion = DateTime.Now.AddDays(-60),
|
|
||||||
FechaActualizacion = DateTime.Now.AddDays(-60),
|
|
||||||
Activo = true
|
|
||||||
});
|
|
||||||
|
|
||||||
_expedientes.Add(new Expediente
|
|
||||||
{
|
|
||||||
Id = _idCounter++,
|
|
||||||
Nombre = "María",
|
|
||||||
Apellidos = "Gómez Fernández",
|
|
||||||
FechaNacimiento = new DateTime(2017, 11, 5),
|
|
||||||
NombrePadre = "Luis Gómez",
|
|
||||||
NombreMadre = "Sofía Fernández",
|
|
||||||
NombreResponsable = "Sofía Fernández",
|
|
||||||
ParentescoResponsable = "Madre",
|
|
||||||
Sexo = "F",
|
|
||||||
Direccion = "Calle Secundaria 101, Ciudad",
|
|
||||||
Telefono = "555-3456",
|
|
||||||
Observaciones = "Necesita atención especial en matemáticas",
|
|
||||||
FotoUrl = "/images/default-avatar.png",
|
|
||||||
FechaCreacion = DateTime.Now.AddDays(-45),
|
|
||||||
FechaActualizacion = DateTime.Now.AddDays(-45),
|
|
||||||
Activo = true
|
|
||||||
});*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: Expedientes
|
// GET: Expedientes
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
_ = InitializeTestData();
|
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +45,7 @@ namespace MieSystem.Controllers
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult GetDashboardStats()
|
public IActionResult GetDashboardStats()
|
||||||
{
|
{
|
||||||
|
_ = InitializeTestData();
|
||||||
var totalNinos = _expedientes.Count;
|
var totalNinos = _expedientes.Count;
|
||||||
var mesActual = DateTime.Now.Month;
|
var mesActual = DateTime.Now.Month;
|
||||||
|
|
||||||
@@ -162,6 +78,9 @@ namespace MieSystem.Controllers
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult GetExpedientes(int page = 1, int pageSize = 10)
|
public IActionResult GetExpedientes(int page = 1, int pageSize = 10)
|
||||||
{
|
{
|
||||||
|
if (_expedientes.Count == 0)
|
||||||
|
_ = InitializeTestData();
|
||||||
|
|
||||||
var query = _expedientes
|
var query = _expedientes
|
||||||
.Where(e => e.Activo)
|
.Where(e => e.Activo)
|
||||||
.OrderBy(e => e.Apellidos)
|
.OrderBy(e => e.Apellidos)
|
||||||
@@ -316,7 +235,8 @@ namespace MieSystem.Controllers
|
|||||||
ModelState.Remove("Observaciones");
|
ModelState.Remove("Observaciones");
|
||||||
if (ModelState.IsValid)
|
if (ModelState.IsValid)
|
||||||
{
|
{
|
||||||
var expediente = _expedientes.FirstOrDefault(e => e.Id == id);
|
var expediente = await _expedienteRepository.GetByIdAsync(id);
|
||||||
|
//var expediente = _expedientes.FirstOrDefault(e => e.Id == id);
|
||||||
if (expediente == null)
|
if (expediente == null)
|
||||||
{
|
{
|
||||||
return Json(new { success = false, message = "Expediente no encontrado" });
|
return Json(new { success = false, message = "Expediente no encontrado" });
|
||||||
@@ -359,6 +279,7 @@ namespace MieSystem.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar datos
|
// Actualizar datos
|
||||||
|
expediente.Id = id;
|
||||||
expediente.Nombre = model.Nombre;
|
expediente.Nombre = model.Nombre;
|
||||||
expediente.Apellidos = model.Apellidos;
|
expediente.Apellidos = model.Apellidos;
|
||||||
expediente.FechaNacimiento = model.FechaNacimiento;
|
expediente.FechaNacimiento = model.FechaNacimiento;
|
||||||
@@ -373,7 +294,16 @@ namespace MieSystem.Controllers
|
|||||||
expediente.FotoUrl = fotoUrlActual;
|
expediente.FotoUrl = fotoUrlActual;
|
||||||
expediente.FechaActualizacion = DateTime.Now;
|
expediente.FechaActualizacion = DateTime.Now;
|
||||||
|
|
||||||
return Json(new { success = true, message = "Expediente actualizado exitosamente" });
|
try
|
||||||
|
{
|
||||||
|
await _expedienteRepository.UpdateAsync(expediente);
|
||||||
|
return Json(new { success = true, message = "Expediente actualizado exitosamente" });
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = ex.Message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Json(new { success = false, message = "Error en los datos del formulario" });
|
return Json(new { success = false, message = "Error en los datos del formulario" });
|
||||||
|
|||||||
22
MieSystem/Data/Interfaces/IAsistenciaRepository.cs
Normal file
22
MieSystem/Data/Interfaces/IAsistenciaRepository.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using MieSystem.Models;
|
||||||
|
|
||||||
|
namespace MieSystem.Data.Interfaces
|
||||||
|
{
|
||||||
|
public interface IAsistenciaRepository
|
||||||
|
{
|
||||||
|
// CRUD básico
|
||||||
|
Task<Asistencia> GetByIdAsync(int id);
|
||||||
|
Task<IEnumerable<Asistencia>> GetByExpedienteAsync(int expedienteId, DateTime? fechaDesde = null, DateTime? fechaHasta = null);
|
||||||
|
Task<IEnumerable<Asistencia>> GetAsistenciasPorMesAsync(int año, int mes);
|
||||||
|
Task<bool> GuardarAsistenciaAsync(Asistencia asistencia);
|
||||||
|
Task<bool> EliminarAsistenciaAsync(int id);
|
||||||
|
|
||||||
|
// Estadísticas
|
||||||
|
Task<EstadisticasMes> GetEstadisticasMesAsync(int año, int mes);
|
||||||
|
Task<Dictionary<int, decimal>> GetPorcentajesAsistenciaAsync(int año, int mes);
|
||||||
|
|
||||||
|
// Operaciones masivas
|
||||||
|
Task<bool> GuardarAsistenciasMasivasAsync(IEnumerable<Asistencia> asistencias);
|
||||||
|
Task<bool> EliminarAsistenciasPorFechaAsync(DateTime fecha);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,5 +9,6 @@ namespace MieSystem.Data.Interfaces
|
|||||||
Task<int> CreateAsync(Expediente expediente);
|
Task<int> CreateAsync(Expediente expediente);
|
||||||
Task<bool> UpdateAsync(Expediente expediente);
|
Task<bool> UpdateAsync(Expediente expediente);
|
||||||
Task<bool> DeleteAsync(int id);
|
Task<bool> DeleteAsync(int id);
|
||||||
|
Task<IEnumerable<Expediente?>> GetActivosAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
442
MieSystem/Data/Repositories/AsistenciaRepository.cs
Normal file
442
MieSystem/Data/Repositories/AsistenciaRepository.cs
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
using Dapper;
|
||||||
|
using Microsoft.AspNetCore.Connections;
|
||||||
|
using MieSystem.Data.Interfaces;
|
||||||
|
using MieSystem.Models;
|
||||||
|
|
||||||
|
namespace MieSystem.Data.Repositories
|
||||||
|
{
|
||||||
|
public class AsistenciaRepository : IAsistenciaRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly IDatabaseConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
public AsistenciaRepository(IDatabaseConnectionFactory databaseConnectionFactory)
|
||||||
|
{
|
||||||
|
_connectionFactory = databaseConnectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region CRUD Básico
|
||||||
|
|
||||||
|
public async Task<Asistencia?> GetByIdAsync(int id)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
expediente_id as ExpedienteId,
|
||||||
|
fecha,
|
||||||
|
estado,
|
||||||
|
hora_entrada as HoraEntrada,
|
||||||
|
hora_salida as HoraSalida,
|
||||||
|
observaciones,
|
||||||
|
fecha_registro as FechaRegistro,
|
||||||
|
usuario_registro as UsuarioRegistro
|
||||||
|
FROM asistencia
|
||||||
|
WHERE id = @Id";
|
||||||
|
|
||||||
|
return await connection.QueryFirstOrDefaultAsync<Asistencia>(sql, new { Id = id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Asistencia?>> GetAllAsync()
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
expediente_id as ExpedienteId,
|
||||||
|
fecha,
|
||||||
|
estado,
|
||||||
|
hora_entrada as HoraEntrada,
|
||||||
|
hora_salida as HoraSalida,
|
||||||
|
observaciones,
|
||||||
|
fecha_registro as FechaRegistro,
|
||||||
|
usuario_registro as UsuarioRegistro
|
||||||
|
FROM asistencia
|
||||||
|
ORDER BY fecha DESC, expediente_id";
|
||||||
|
|
||||||
|
return await connection.QueryAsync<Asistencia>(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> CreateAsync(Asistencia asistencia)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
INSERT INTO asistencia (
|
||||||
|
expediente_id,
|
||||||
|
fecha,
|
||||||
|
estado,
|
||||||
|
hora_entrada,
|
||||||
|
hora_salida,
|
||||||
|
observaciones,
|
||||||
|
usuario_registro
|
||||||
|
) VALUES (
|
||||||
|
@ExpedienteId,
|
||||||
|
@Fecha,
|
||||||
|
@Estado,
|
||||||
|
@HoraEntrada,
|
||||||
|
@HoraSalida,
|
||||||
|
@Observaciones,
|
||||||
|
@UsuarioRegistro
|
||||||
|
)
|
||||||
|
RETURNING id";
|
||||||
|
|
||||||
|
var parameters = new
|
||||||
|
{
|
||||||
|
asistencia.ExpedienteId,
|
||||||
|
Fecha = asistencia.Fecha.Date,
|
||||||
|
asistencia.Estado,
|
||||||
|
asistencia.HoraEntrada,
|
||||||
|
asistencia.HoraSalida,
|
||||||
|
asistencia.Observaciones,
|
||||||
|
asistencia.UsuarioRegistro
|
||||||
|
};
|
||||||
|
|
||||||
|
return await connection.ExecuteScalarAsync<int>(sql, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateAsync(Asistencia asistencia)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
UPDATE asistencia SET
|
||||||
|
estado = @Estado,
|
||||||
|
hora_entrada = @HoraEntrada,
|
||||||
|
hora_salida = @HoraSalida,
|
||||||
|
observaciones = @Observaciones,
|
||||||
|
usuario_registro = @UsuarioRegistro
|
||||||
|
WHERE id = @Id";
|
||||||
|
|
||||||
|
var parameters = new
|
||||||
|
{
|
||||||
|
asistencia.Id,
|
||||||
|
asistencia.Estado,
|
||||||
|
asistencia.HoraEntrada,
|
||||||
|
asistencia.HoraSalida,
|
||||||
|
asistencia.Observaciones,
|
||||||
|
asistencia.UsuarioRegistro
|
||||||
|
};
|
||||||
|
|
||||||
|
var affectedRows = await connection.ExecuteAsync(sql, parameters);
|
||||||
|
return affectedRows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteAsync(int id)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = "DELETE FROM asistencia WHERE id = @Id";
|
||||||
|
var affected = await connection.ExecuteAsync(sql, new { Id = id });
|
||||||
|
|
||||||
|
return affected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Métodos específicos de la interfaz
|
||||||
|
|
||||||
|
public async Task<bool> GuardarAsistenciaAsync(Asistencia asistencia)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
// Usar UPSERT (INSERT ... ON CONFLICT ... UPDATE)
|
||||||
|
var sql = @"
|
||||||
|
INSERT INTO asistencia (
|
||||||
|
expediente_id,
|
||||||
|
fecha,
|
||||||
|
estado,
|
||||||
|
hora_entrada,
|
||||||
|
hora_salida,
|
||||||
|
observaciones,
|
||||||
|
usuario_registro
|
||||||
|
) VALUES (
|
||||||
|
@ExpedienteId,
|
||||||
|
@Fecha,
|
||||||
|
@Estado,
|
||||||
|
@HoraEntrada,
|
||||||
|
@HoraSalida,
|
||||||
|
@Observaciones,
|
||||||
|
@UsuarioRegistro
|
||||||
|
)
|
||||||
|
ON CONFLICT (expediente_id, fecha)
|
||||||
|
DO UPDATE SET
|
||||||
|
estado = EXCLUDED.estado,
|
||||||
|
hora_entrada = EXCLUDED.hora_entrada,
|
||||||
|
hora_salida = EXCLUDED.hora_salida,
|
||||||
|
observaciones = EXCLUDED.observaciones,
|
||||||
|
usuario_registro = EXCLUDED.usuario_registro,
|
||||||
|
fecha_registro = CURRENT_TIMESTAMP
|
||||||
|
RETURNING id";
|
||||||
|
|
||||||
|
var parameters = new
|
||||||
|
{
|
||||||
|
asistencia.ExpedienteId,
|
||||||
|
Fecha = asistencia.Fecha.Date,
|
||||||
|
asistencia.Estado,
|
||||||
|
asistencia.HoraEntrada,
|
||||||
|
asistencia.HoraSalida,
|
||||||
|
asistencia.Observaciones,
|
||||||
|
asistencia.UsuarioRegistro
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await connection.ExecuteScalarAsync<int?>(sql, parameters);
|
||||||
|
return result.HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Asistencia>> GetByExpedienteAsync(int expedienteId, DateTime? fechaDesde = null, DateTime? fechaHasta = null)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
expediente_id as ExpedienteId,
|
||||||
|
fecha,
|
||||||
|
estado,
|
||||||
|
hora_entrada as HoraEntrada,
|
||||||
|
hora_salida as HoraSalida,
|
||||||
|
observaciones,
|
||||||
|
fecha_registro as FechaRegistro,
|
||||||
|
usuario_registro as UsuarioRegistro
|
||||||
|
FROM asistencia
|
||||||
|
WHERE expediente_id = @ExpedienteId";
|
||||||
|
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
parameters.Add("ExpedienteId", expedienteId);
|
||||||
|
|
||||||
|
if (fechaDesde.HasValue)
|
||||||
|
{
|
||||||
|
sql += " AND fecha >= @FechaDesde";
|
||||||
|
parameters.Add("FechaDesde", fechaDesde.Value.Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fechaHasta.HasValue)
|
||||||
|
{
|
||||||
|
sql += " AND fecha <= @FechaHasta";
|
||||||
|
parameters.Add("FechaHasta", fechaHasta.Value.Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += " ORDER BY fecha DESC";
|
||||||
|
|
||||||
|
return await connection.QueryAsync<Asistencia>(sql, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Asistencia>> GetAsistenciasPorMesAsync(int año, int mes)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
expediente_id as ExpedienteId,
|
||||||
|
fecha,
|
||||||
|
estado,
|
||||||
|
hora_entrada as HoraEntrada,
|
||||||
|
hora_salida as HoraSalida,
|
||||||
|
observaciones,
|
||||||
|
fecha_registro as FechaRegistro,
|
||||||
|
usuario_registro as UsuarioRegistro
|
||||||
|
FROM asistencia
|
||||||
|
WHERE EXTRACT(YEAR FROM fecha) = @Año
|
||||||
|
AND EXTRACT(MONTH FROM fecha) = @Mes
|
||||||
|
ORDER BY fecha, expediente_id";
|
||||||
|
|
||||||
|
return await connection.QueryAsync<Asistencia>(sql, new { Año = año, Mes = mes });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> GuardarAsistenciasMasivasAsync(IEnumerable<Asistencia> asistencias)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
INSERT INTO asistencia (
|
||||||
|
expediente_id,
|
||||||
|
fecha,
|
||||||
|
estado,
|
||||||
|
hora_entrada,
|
||||||
|
hora_salida,
|
||||||
|
observaciones,
|
||||||
|
usuario_registro
|
||||||
|
) VALUES (
|
||||||
|
@ExpedienteId,
|
||||||
|
@Fecha,
|
||||||
|
@Estado,
|
||||||
|
@HoraEntrada,
|
||||||
|
@HoraSalida,
|
||||||
|
@Observaciones,
|
||||||
|
@UsuarioRegistro
|
||||||
|
)
|
||||||
|
ON CONFLICT (expediente_id, fecha)
|
||||||
|
DO UPDATE SET
|
||||||
|
estado = EXCLUDED.estado,
|
||||||
|
hora_entrada = EXCLUDED.hora_entrada,
|
||||||
|
hora_salida = EXCLUDED.hora_salida,
|
||||||
|
observaciones = EXCLUDED.observaciones,
|
||||||
|
usuario_registro = EXCLUDED.usuario_registro,
|
||||||
|
fecha_registro = CURRENT_TIMESTAMP";
|
||||||
|
|
||||||
|
var exitosas = 0;
|
||||||
|
foreach (var asistencia in asistencias)
|
||||||
|
{
|
||||||
|
var parameters = new
|
||||||
|
{
|
||||||
|
asistencia.ExpedienteId,
|
||||||
|
Fecha = asistencia.Fecha.Date,
|
||||||
|
asistencia.Estado,
|
||||||
|
asistencia.HoraEntrada,
|
||||||
|
asistencia.HoraSalida,
|
||||||
|
asistencia.Observaciones,
|
||||||
|
asistencia.UsuarioRegistro
|
||||||
|
};
|
||||||
|
|
||||||
|
var affected = await connection.ExecuteAsync(sql, parameters);
|
||||||
|
if (affected > 0) exitosas++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exitosas > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> EliminarAsistenciaAsync(int id)
|
||||||
|
{
|
||||||
|
return await DeleteAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> EliminarAsistenciasPorFechaAsync(DateTime fecha)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = "DELETE FROM asistencia WHERE fecha = @Fecha";
|
||||||
|
var affected = await connection.ExecuteAsync(sql, new { Fecha = fecha.Date });
|
||||||
|
|
||||||
|
return affected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EstadisticasMes> GetEstadisticasMesAsync(int año, int mes)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT
|
||||||
|
COALESCE(COUNT(*), 0) as Total,
|
||||||
|
COALESCE(COUNT(CASE WHEN estado = 'P' THEN 1 END), 0) as Presentes,
|
||||||
|
COALESCE(COUNT(CASE WHEN estado = 'T' THEN 1 END), 0) as Tardes,
|
||||||
|
COALESCE(COUNT(CASE WHEN estado = 'F' THEN 1 END), 0) as Faltas,
|
||||||
|
COALESCE(ROUND(
|
||||||
|
COUNT(CASE WHEN estado = 'P' THEN 1 END) * 100.0 /
|
||||||
|
NULLIF(COUNT(*), 0),
|
||||||
|
2
|
||||||
|
), 0) as PorcentajePresentes,
|
||||||
|
COALESCE(ROUND(
|
||||||
|
COUNT(CASE WHEN estado = 'T' THEN 1 END) * 100.0 /
|
||||||
|
NULLIF(COUNT(*), 0),
|
||||||
|
2
|
||||||
|
), 0) as PorcentajeTardes,
|
||||||
|
COALESCE(ROUND(
|
||||||
|
COUNT(CASE WHEN estado = 'F' THEN 1 END) * 100.0 /
|
||||||
|
NULLIF(COUNT(*), 0),
|
||||||
|
2
|
||||||
|
), 0) as PorcentajeFaltas
|
||||||
|
FROM asistencia
|
||||||
|
WHERE EXTRACT(YEAR FROM fecha) = @Año
|
||||||
|
AND EXTRACT(MONTH FROM fecha) = @Mes";
|
||||||
|
|
||||||
|
var result = await connection.QueryFirstOrDefaultAsync<EstadisticasMes>(sql, new { Año = año, Mes = mes });
|
||||||
|
|
||||||
|
// Siempre devolver un objeto, incluso si es nulo
|
||||||
|
return result ?? new EstadisticasMes();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<Dictionary<int, decimal>> GetPorcentajesAsistenciaAsync(int año, int mes)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT
|
||||||
|
expediente_id,
|
||||||
|
ROUND(
|
||||||
|
COUNT(CASE WHEN estado = 'P' THEN 1 END) * 100.0 /
|
||||||
|
NULLIF(COUNT(*), 0),
|
||||||
|
2
|
||||||
|
) as porcentaje_asistencia
|
||||||
|
FROM asistencia
|
||||||
|
WHERE EXTRACT(YEAR FROM fecha) = @Año
|
||||||
|
AND EXTRACT(MONTH FROM fecha) = @Mes
|
||||||
|
GROUP BY expediente_id";
|
||||||
|
|
||||||
|
var resultados = await connection.QueryAsync<(int, decimal)>(sql, new { Año = año, Mes = mes });
|
||||||
|
|
||||||
|
return resultados.ToDictionary(
|
||||||
|
r => r.Item1,
|
||||||
|
r => r.Item2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Métodos adicionales útiles
|
||||||
|
|
||||||
|
public async Task<bool> ExisteAsistenciaAsync(int expedienteId, DateTime fecha)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = "SELECT COUNT(1) FROM asistencia WHERE expediente_id = @ExpedienteId AND fecha = @Fecha";
|
||||||
|
var count = await connection.ExecuteScalarAsync<int>(sql, new
|
||||||
|
{
|
||||||
|
ExpedienteId = expedienteId,
|
||||||
|
Fecha = fecha.Date
|
||||||
|
});
|
||||||
|
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Asistencia>> GetAsistenciasPorRangoAsync(DateTime fechaDesde, DateTime fechaHasta)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
expediente_id as ExpedienteId,
|
||||||
|
fecha,
|
||||||
|
estado,
|
||||||
|
hora_entrada as HoraEntrada,
|
||||||
|
hora_salida as HoraSalida,
|
||||||
|
observaciones,
|
||||||
|
fecha_registro as FechaRegistro,
|
||||||
|
usuario_registro as UsuarioRegistro
|
||||||
|
FROM asistencia
|
||||||
|
WHERE fecha BETWEEN @FechaDesde AND @FechaHasta
|
||||||
|
ORDER BY fecha, expediente_id";
|
||||||
|
|
||||||
|
return await connection.QueryAsync<Asistencia>(sql, new
|
||||||
|
{
|
||||||
|
FechaDesde = fechaDesde.Date,
|
||||||
|
FechaHasta = fechaHasta.Date
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalAsistenciasPorExpedienteAsync(int expedienteId)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = "SELECT COUNT(*) FROM asistencia WHERE expediente_id = @ExpedienteId";
|
||||||
|
return await connection.ExecuteScalarAsync<int>(sql, new { ExpedienteId = expedienteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetAsistenciasPresentesPorExpedienteAsync(int expedienteId)
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
var sql = "SELECT COUNT(*) FROM asistencia WHERE expediente_id = @ExpedienteId AND estado = 'P'";
|
||||||
|
return await connection.ExecuteScalarAsync<int>(sql, new { ExpedienteId = expedienteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Dapper;
|
using MieSystem.Data.Interfaces;
|
||||||
using MieSystem.Data.Interfaces;
|
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
||||||
@@ -18,7 +17,6 @@ namespace MieSystem.Data.Repositories
|
|||||||
// Mapear tipos de PostgreSQL a .NET
|
// Mapear tipos de PostgreSQL a .NET
|
||||||
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||||
NpgsqlConnection.GlobalTypeMapper.UseJsonNet();
|
NpgsqlConnection.GlobalTypeMapper.UseJsonNet();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDbConnection CreateConnection()
|
public IDbConnection CreateConnection()
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ namespace MieSystem.Data.Repositories
|
|||||||
_connectionFactory = databaseConnectionFactory;
|
_connectionFactory = databaseConnectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*public async Task<int> CreateAsync(Expediente expediente)
|
|
||||||
{
|
|
||||||
using var connection = await _connectionFactory.CreateConnectionAsync();
|
|
||||||
return await connection.InsertAsync(expediente);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
public async Task<int> CreateAsync(Expediente expediente)
|
public async Task<int> CreateAsync(Expediente expediente)
|
||||||
{
|
{
|
||||||
using var connection = await _connectionFactory.CreateConnectionAsync();
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
@@ -92,34 +86,27 @@ namespace MieSystem.Data.Repositories
|
|||||||
return affected > 0;
|
return affected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Expediente?>> GetActivosAsync()
|
||||||
|
{
|
||||||
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
|
if (connection.State == System.Data.ConnectionState.Open)
|
||||||
|
{
|
||||||
|
var result = await connection.QueryAsync<Expediente?>(@"SELECT * FROM expedientes where activo = True ORDER BY nombre");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine("Estado de la conexion es:" + connection.State);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Expediente?>> GetAllAsync()
|
public async Task<IEnumerable<Expediente?>> GetAllAsync()
|
||||||
{
|
{
|
||||||
using var connection = await _connectionFactory.CreateConnectionAsync();
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
if (connection.State == System.Data.ConnectionState.Open) {
|
if (connection.State == System.Data.ConnectionState.Open) {
|
||||||
/*var result = await connection.QueryAsync<Expediente>(
|
|
||||||
@"SELECT
|
|
||||||
id,
|
|
||||||
nombre,
|
|
||||||
apellidos,
|
|
||||||
fecha_nacimiento::timestamp as fecha_nacimiento, -- Convertir a timestamp
|
|
||||||
sexo,
|
|
||||||
nombre_padre,
|
|
||||||
nombre_madre,
|
|
||||||
nombre_responsable,
|
|
||||||
parentesco_responsable,
|
|
||||||
direccion,
|
|
||||||
telefono,
|
|
||||||
observaciones,
|
|
||||||
foto_url,
|
|
||||||
fecha_creacion,
|
|
||||||
fecha_actualizacion,
|
|
||||||
activo
|
|
||||||
FROM expedientes
|
|
||||||
ORDER BY nombre"
|
|
||||||
);*/
|
|
||||||
|
|
||||||
|
|
||||||
var result = await connection.QueryAsync<Expediente>(
|
var result = await connection.QueryAsync<Expediente>(
|
||||||
@"SELECT *
|
@"SELECT *
|
||||||
FROM expedientes
|
FROM expedientes
|
||||||
@@ -148,8 +135,44 @@ namespace MieSystem.Data.Repositories
|
|||||||
{
|
{
|
||||||
using var connection = await _connectionFactory.CreateConnectionAsync();
|
using var connection = await _connectionFactory.CreateConnectionAsync();
|
||||||
|
|
||||||
// Con Dapper.Contrib
|
var sql = @"
|
||||||
return await connection.UpdateAsync(expediente);
|
UPDATE expedientes SET
|
||||||
|
nombre = @Nombre,
|
||||||
|
apellidos = @Apellidos,
|
||||||
|
fecha_nacimiento = @FechaNacimiento,
|
||||||
|
sexo = @Sexo,
|
||||||
|
nombre_padre = @NombrePadre,
|
||||||
|
nombre_madre = @NombreMadre,
|
||||||
|
nombre_responsable = @NombreResponsable,
|
||||||
|
parentesco_responsable = @ParentescoResponsable,
|
||||||
|
direccion = @Direccion,
|
||||||
|
telefono = @Telefono,
|
||||||
|
observaciones = @Observaciones,
|
||||||
|
foto_url = @FotoUrl,
|
||||||
|
fecha_actualizacion = CURRENT_TIMESTAMP,
|
||||||
|
activo = @Activo
|
||||||
|
WHERE id = @Id";
|
||||||
|
|
||||||
|
var parameters = new
|
||||||
|
{
|
||||||
|
expediente.Id,
|
||||||
|
expediente.Nombre,
|
||||||
|
expediente.Apellidos,
|
||||||
|
FechaNacimiento = expediente.FechaNacimiento.Date, // Solo fecha
|
||||||
|
expediente.Sexo,
|
||||||
|
expediente.NombrePadre,
|
||||||
|
expediente.NombreMadre,
|
||||||
|
expediente.NombreResponsable,
|
||||||
|
expediente.ParentescoResponsable,
|
||||||
|
expediente.Direccion,
|
||||||
|
expediente.Telefono,
|
||||||
|
expediente.Observaciones,
|
||||||
|
expediente.FotoUrl,
|
||||||
|
expediente.Activo
|
||||||
|
};
|
||||||
|
|
||||||
|
var affectedRows = await connection.ExecuteAsync(sql, parameters);
|
||||||
|
return affectedRows > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Folder Include="wwwroot\lib\bootstrap\dist\css\fonts\" />
|
||||||
<Folder Include="wwwroot\uploads\fotos\" />
|
<Folder Include="wwwroot\uploads\fotos\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
34
MieSystem/Models/Asistencia.cs
Normal file
34
MieSystem/Models/Asistencia.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MieSystem.Models
|
||||||
|
{
|
||||||
|
public class Asistencia
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int ExpedienteId { get; set; }
|
||||||
|
public DateTime Fecha { get; set; }
|
||||||
|
public string Estado { get; set; } // P, T, F
|
||||||
|
public TimeSpan? HoraEntrada { get; set; }
|
||||||
|
public TimeSpan? HoraSalida { get; set; }
|
||||||
|
public string Observaciones { get; set; }
|
||||||
|
public DateTime FechaRegistro { get; set; }
|
||||||
|
public string UsuarioRegistro { get; set; }
|
||||||
|
|
||||||
|
// Propiedades calculadas
|
||||||
|
public string EstadoTexto => Estado switch
|
||||||
|
{
|
||||||
|
"P" => "Presente",
|
||||||
|
"T" => "Tarde",
|
||||||
|
"F" => "Falto",
|
||||||
|
_ => "Desconocido"
|
||||||
|
};
|
||||||
|
|
||||||
|
public string ColorEstado => Estado switch
|
||||||
|
{
|
||||||
|
"P" => "success",
|
||||||
|
"T" => "warning",
|
||||||
|
"F" => "danger",
|
||||||
|
_ => "secondary"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
13
MieSystem/Models/EstadisticasMes.cs
Normal file
13
MieSystem/Models/EstadisticasMes.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace MieSystem.Models
|
||||||
|
{
|
||||||
|
public class EstadisticasMes
|
||||||
|
{
|
||||||
|
public int Total { get; set; }
|
||||||
|
public int Presentes { get; set; }
|
||||||
|
public int Tardes { get; set; }
|
||||||
|
public int Faltas { get; set; }
|
||||||
|
public decimal PorcentajePresentes { get; set; }
|
||||||
|
public decimal PorcentajeTardes { get; set; }
|
||||||
|
public decimal PorcentajeFaltas { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,5 +51,57 @@ namespace MieSystem.Models
|
|||||||
|
|
||||||
[Column("activo")]
|
[Column("activo")]
|
||||||
public bool Activo { get; set; }
|
public bool Activo { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Propiedades calculadas (solo lectura)
|
||||||
|
public string NombreCompleto => $"{Nombre} {Apellidos}".Trim();
|
||||||
|
|
||||||
|
public int Edad
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var today = DateTime.Today;
|
||||||
|
var age = today.Year - FechaNacimiento.Year;
|
||||||
|
|
||||||
|
// Si aún no ha cumplido años este año, restar 1
|
||||||
|
if (FechaNacimiento.Date > today.AddYears(-age))
|
||||||
|
{
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otra propiedad útil para mostrar
|
||||||
|
public string EdadConMeses
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var today = DateTime.Today;
|
||||||
|
var age = today.Year - FechaNacimiento.Year;
|
||||||
|
var months = today.Month - FechaNacimiento.Month;
|
||||||
|
|
||||||
|
if (today.Day < FechaNacimiento.Day)
|
||||||
|
{
|
||||||
|
months--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (months < 0)
|
||||||
|
{
|
||||||
|
age--;
|
||||||
|
months += 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{age} años, {months} meses";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para mostrar en selectores
|
||||||
|
public string NombreConEdad => $"{NombreCompleto} ({Edad} años)";
|
||||||
|
|
||||||
|
// Para mostrar en listas
|
||||||
|
public string InformacionBasica => $"{NombreCompleto} | {Edad} años | {Sexo}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
MieSystem/Models/ViewModels/AsistenciaViewModel.cs
Normal file
20
MieSystem/Models/ViewModels/AsistenciaViewModel.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace MieSystem.Models.ViewModels
|
||||||
|
{
|
||||||
|
public class AsistenciaViewModel
|
||||||
|
{
|
||||||
|
public int Año { get; set; }
|
||||||
|
public int Mes { get; set; }
|
||||||
|
public string NombreMes { get; set; }
|
||||||
|
public string DiasSemanaSeleccionados { get; set; }
|
||||||
|
public List<Expediente> Expedientes { get; set; }
|
||||||
|
public List<DateTime> DiasDelMes { get; set; }
|
||||||
|
public Dictionary<string, string> Asistencias { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AsistenciaMasivaDto
|
||||||
|
{
|
||||||
|
public int ExpedienteId { get; set; }
|
||||||
|
public DateTime Fecha { get; set; }
|
||||||
|
public string Estado { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace MieSystem.Models
|
namespace MieSystem.Models.ViewModels
|
||||||
{
|
{
|
||||||
public class ExpedienteViewModel
|
public class ExpedienteViewModel
|
||||||
{
|
{
|
||||||
@@ -15,6 +15,7 @@ namespace MieSystem
|
|||||||
builder.Services.AddSingleton<IDatabaseConnectionFactory, DatabaseConnectionFactory>();
|
builder.Services.AddSingleton<IDatabaseConnectionFactory, DatabaseConnectionFactory>();
|
||||||
|
|
||||||
builder.Services.AddScoped<IExpedienteRepository, ExpedienteRepository>();
|
builder.Services.AddScoped<IExpedienteRepository, ExpedienteRepository>();
|
||||||
|
builder.Services.AddScoped<IAsistenciaRepository, AsistenciaRepository>();
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
@@ -1,5 +1,495 @@
|
|||||||
@*
|
@model MieSystem.Models.ViewModels.AsistenciaViewModel
|
||||||
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
|
||||||
*@
|
|
||||||
@{
|
@{
|
||||||
|
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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@model ExpedienteViewModel
|
@using MieSystem.Models.ViewModels
|
||||||
|
@model ExpedienteViewModel
|
||||||
@{
|
@{
|
||||||
Layout = null;
|
Layout = null;
|
||||||
ViewData["Title"] = "Expediente - " + Model.NombreCompleto;
|
ViewData["Title"] = "Expediente - " + Model.NombreCompleto;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@{
|
@using MieSystem.Models.ViewModels
|
||||||
|
@{
|
||||||
ViewData["Title"] = "Expedientes";
|
ViewData["Title"] = "Expedientes";
|
||||||
ViewData["ActionButtons"] = @"<button class='btn btn-primary' data-bs-toggle='modal' data-bs-target='#createModal'>
|
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
|
<i class='bi bi-plus-circle me-1'></i> Nuevo Expediente
|
||||||
@@ -104,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal para crear nuevo expediente -->
|
<!-- 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-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -262,7 +263,7 @@
|
|||||||
<button class="btn btn-sm btn-outline-warning" onclick="editExpediente(${expediente.id})" title="Editar">
|
<button class="btn btn-sm btn-outline-warning" onclick="editExpediente(${expediente.id})" title="Editar">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</button>
|
</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>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@using MieSystem.Models;
|
@using MieSystem.Models;
|
||||||
|
@using MieSystem.Models.ViewModels
|
||||||
|
|
||||||
@model ExpedienteViewModel
|
@model ExpedienteViewModel
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,13 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>@ViewData["Title"] - MieSystem</title>
|
<title>@ViewData["Title"] - MieSystem</title>
|
||||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
<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="~/css/site.css" asp-append-version="true" />
|
||||||
<link rel="stylesheet" href="~/MieSystem.styles.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>
|
<style>
|
||||||
/* Estilos personalizados para el menú lateral */
|
/* Estilos personalizados para el menú lateral */
|
||||||
body {
|
body {
|
||||||
@@ -200,6 +204,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow">
|
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<button class="toggle-sidebar-btn" id="toggleSidebar">
|
<button class="toggle-sidebar-btn" id="toggleSidebar">
|
||||||
@@ -319,10 +324,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.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="~/js/site.js" asp-append-version="true"></script>
|
||||||
|
<script src="~/lib/bootstrap/dist/js/toastr.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Control del menú lateral
|
// Control del menú lateral
|
||||||
|
|||||||
2078
MieSystem/wwwroot/lib/bootstrap/dist/css/bootstrap-icons.css
vendored
Normal file
2078
MieSystem/wwwroot/lib/bootstrap/dist/css/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
MieSystem/wwwroot/lib/bootstrap/dist/css/fonts/bootstrap-icons.woff
vendored
Normal file
BIN
MieSystem/wwwroot/lib/bootstrap/dist/css/fonts/bootstrap-icons.woff
vendored
Normal file
Binary file not shown.
BIN
MieSystem/wwwroot/lib/bootstrap/dist/css/fonts/bootstrap-icons.woff2
vendored
Normal file
BIN
MieSystem/wwwroot/lib/bootstrap/dist/css/fonts/bootstrap-icons.woff2
vendored
Normal file
Binary file not shown.
1
MieSystem/wwwroot/lib/bootstrap/dist/css/toastr.min.css
vendored
Normal file
1
MieSystem/wwwroot/lib/bootstrap/dist/css/toastr.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
MieSystem/wwwroot/lib/bootstrap/dist/js/toastr.min.js
vendored
Normal file
7
MieSystem/wwwroot/lib/bootstrap/dist/js/toastr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user