diff --git a/.gitignore b/.gitignore index 5459f64..a86be46 100644 --- a/.gitignore +++ b/.gitignore @@ -361,4 +361,4 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd -/MieSystem/wwwroot/uploads/fotos +/MieSystem/wwwroot/uploads diff --git a/MieSystem/Controllers/AsistenciaController.cs b/MieSystem/Controllers/AsistenciaController.cs index d5a4475..464c8b9 100644 --- a/MieSystem/Controllers/AsistenciaController.cs +++ b/MieSystem/Controllers/AsistenciaController.cs @@ -1,12 +1,261 @@ 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 { 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 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(); + 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 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 GuardarAsistenciasMasivas([FromBody] List asistencias) + { + try + { + var asistenciasModel = new List(); + + 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 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 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 GetDiasDelMes(int año, int mes) + { + var dias = new List(); + 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 GetListaMeses() + { + return new List + { + 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 GetListaAños() + { + var años = new List(); + 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 GetDiasSemana() + { + return new List + { + 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 } diff --git a/MieSystem/Controllers/ExpedientesController.cs b/MieSystem/Controllers/ExpedientesController.cs index def7318..302e880 100644 --- a/MieSystem/Controllers/ExpedientesController.cs +++ b/MieSystem/Controllers/ExpedientesController.cs @@ -2,12 +2,9 @@ using System.Linq; using System.Threading.Tasks; using MieSystem.Models; -using System.Collections.Generic; -using System; -using System.IO; -using Microsoft.AspNetCore.Hosting; -using MieSystem.Models; using MieSystem.Data.Interfaces; +using AspNetCoreGeneratedDocument; +using MieSystem.Models.ViewModels; namespace MieSystem.Controllers { @@ -25,104 +22,22 @@ namespace MieSystem.Controllers _expedienteRepository = expedienteRepository; // Datos de prueba iniciales - if (_expedientes.Count == 0) + /*if (_expedientes.Count == 0) { - _ = InitializeTestData(); - } + }*/ } private async Task InitializeTestData() { var today = DateTime.Today; var lst = await _expedienteRepository.GetAllAsync(); - _expedientes =new List(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 - });*/ + _expedientes = [.. lst]; } // GET: Expedientes public IActionResult Index() { - _ = InitializeTestData(); return View(); } @@ -130,6 +45,7 @@ namespace MieSystem.Controllers [HttpGet] public IActionResult GetDashboardStats() { + _ = InitializeTestData(); var totalNinos = _expedientes.Count; var mesActual = DateTime.Now.Month; @@ -162,6 +78,9 @@ namespace MieSystem.Controllers [HttpGet] public IActionResult GetExpedientes(int page = 1, int pageSize = 10) { + if (_expedientes.Count == 0) + _ = InitializeTestData(); + var query = _expedientes .Where(e => e.Activo) .OrderBy(e => e.Apellidos) @@ -316,7 +235,8 @@ namespace MieSystem.Controllers ModelState.Remove("Observaciones"); 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) { return Json(new { success = false, message = "Expediente no encontrado" }); @@ -359,6 +279,7 @@ namespace MieSystem.Controllers } // Actualizar datos + expediente.Id = id; expediente.Nombre = model.Nombre; expediente.Apellidos = model.Apellidos; expediente.FechaNacimiento = model.FechaNacimiento; @@ -373,7 +294,16 @@ namespace MieSystem.Controllers expediente.FotoUrl = fotoUrlActual; 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" }); diff --git a/MieSystem/Data/Interfaces/IAsistenciaRepository.cs b/MieSystem/Data/Interfaces/IAsistenciaRepository.cs new file mode 100644 index 0000000..1ef7a2f --- /dev/null +++ b/MieSystem/Data/Interfaces/IAsistenciaRepository.cs @@ -0,0 +1,22 @@ +using MieSystem.Models; + +namespace MieSystem.Data.Interfaces +{ + public interface IAsistenciaRepository + { + // CRUD básico + Task GetByIdAsync(int id); + Task> GetByExpedienteAsync(int expedienteId, DateTime? fechaDesde = null, DateTime? fechaHasta = null); + Task> GetAsistenciasPorMesAsync(int año, int mes); + Task GuardarAsistenciaAsync(Asistencia asistencia); + Task EliminarAsistenciaAsync(int id); + + // Estadísticas + Task GetEstadisticasMesAsync(int año, int mes); + Task> GetPorcentajesAsistenciaAsync(int año, int mes); + + // Operaciones masivas + Task GuardarAsistenciasMasivasAsync(IEnumerable asistencias); + Task EliminarAsistenciasPorFechaAsync(DateTime fecha); + } +} diff --git a/MieSystem/Data/Interfaces/IExpedienteRepository.cs b/MieSystem/Data/Interfaces/IExpedienteRepository.cs index edcd1e8..4153e7f 100644 --- a/MieSystem/Data/Interfaces/IExpedienteRepository.cs +++ b/MieSystem/Data/Interfaces/IExpedienteRepository.cs @@ -9,5 +9,6 @@ namespace MieSystem.Data.Interfaces Task CreateAsync(Expediente expediente); Task UpdateAsync(Expediente expediente); Task DeleteAsync(int id); + Task> GetActivosAsync(); } } diff --git a/MieSystem/Data/Repositories/AsistenciaRepository.cs b/MieSystem/Data/Repositories/AsistenciaRepository.cs new file mode 100644 index 0000000..05214cc --- /dev/null +++ b/MieSystem/Data/Repositories/AsistenciaRepository.cs @@ -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 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(sql, new { Id = id }); + } + + public async Task> 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(sql); + } + + public async Task 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(sql, parameters); + } + + public async Task 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 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 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(sql, parameters); + return result.HasValue; + } + + public async Task> 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(sql, parameters); + } + + public async Task> 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(sql, new { Año = año, Mes = mes }); + } + + public async Task GuardarAsistenciasMasivasAsync(IEnumerable 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 EliminarAsistenciaAsync(int id) + { + return await DeleteAsync(id); + } + + public async Task 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 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(sql, new { Año = año, Mes = mes }); + + // Siempre devolver un objeto, incluso si es nulo + return result ?? new EstadisticasMes(); + } + + + + public async Task> 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 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(sql, new + { + ExpedienteId = expedienteId, + Fecha = fecha.Date + }); + + return count > 0; + } + + public async Task> 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(sql, new + { + FechaDesde = fechaDesde.Date, + FechaHasta = fechaHasta.Date + }); + } + + public async Task GetTotalAsistenciasPorExpedienteAsync(int expedienteId) + { + using var connection = await _connectionFactory.CreateConnectionAsync(); + + var sql = "SELECT COUNT(*) FROM asistencia WHERE expediente_id = @ExpedienteId"; + return await connection.ExecuteScalarAsync(sql, new { ExpedienteId = expedienteId }); + } + + public async Task 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(sql, new { ExpedienteId = expedienteId }); + } + + #endregion + } + +} diff --git a/MieSystem/Data/Repositories/DatabaseConnectionFactory.cs b/MieSystem/Data/Repositories/DatabaseConnectionFactory.cs index b4a9dc6..912c0d9 100644 --- a/MieSystem/Data/Repositories/DatabaseConnectionFactory.cs +++ b/MieSystem/Data/Repositories/DatabaseConnectionFactory.cs @@ -1,5 +1,4 @@ -using Dapper; -using MieSystem.Data.Interfaces; +using MieSystem.Data.Interfaces; using Npgsql; using System.Data; @@ -18,7 +17,6 @@ namespace MieSystem.Data.Repositories // Mapear tipos de PostgreSQL a .NET Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; NpgsqlConnection.GlobalTypeMapper.UseJsonNet(); - } public IDbConnection CreateConnection() diff --git a/MieSystem/Data/Repositories/ExpedienteRepository.cs b/MieSystem/Data/Repositories/ExpedienteRepository.cs index 826bb5a..b9f9396 100644 --- a/MieSystem/Data/Repositories/ExpedienteRepository.cs +++ b/MieSystem/Data/Repositories/ExpedienteRepository.cs @@ -15,12 +15,6 @@ namespace MieSystem.Data.Repositories _connectionFactory = databaseConnectionFactory; } - /*public async Task CreateAsync(Expediente expediente) - { - using var connection = await _connectionFactory.CreateConnectionAsync(); - return await connection.InsertAsync(expediente); - }*/ - public async Task CreateAsync(Expediente expediente) { using var connection = await _connectionFactory.CreateConnectionAsync(); @@ -92,34 +86,27 @@ namespace MieSystem.Data.Repositories return affected > 0; } + public async Task> GetActivosAsync() + { + using var connection = await _connectionFactory.CreateConnectionAsync(); + + if (connection.State == System.Data.ConnectionState.Open) + { + var result = await connection.QueryAsync(@"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> GetAllAsync() { using var connection = await _connectionFactory.CreateConnectionAsync(); if (connection.State == System.Data.ConnectionState.Open) { - /*var result = await connection.QueryAsync( - @"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( @"SELECT * FROM expedientes @@ -148,8 +135,44 @@ namespace MieSystem.Data.Repositories { using var connection = await _connectionFactory.CreateConnectionAsync(); - // Con Dapper.Contrib - return await connection.UpdateAsync(expediente); + var sql = @" + 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; } } } diff --git a/MieSystem/MieSystem.csproj b/MieSystem/MieSystem.csproj index 1043e69..e494daf 100644 --- a/MieSystem/MieSystem.csproj +++ b/MieSystem/MieSystem.csproj @@ -21,6 +21,7 @@ + diff --git a/MieSystem/Models/Asistencia.cs b/MieSystem/Models/Asistencia.cs new file mode 100644 index 0000000..5c4a86e --- /dev/null +++ b/MieSystem/Models/Asistencia.cs @@ -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" + }; + } +} diff --git a/MieSystem/Models/EstadisticasMes.cs b/MieSystem/Models/EstadisticasMes.cs new file mode 100644 index 0000000..344dae4 --- /dev/null +++ b/MieSystem/Models/EstadisticasMes.cs @@ -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; } + } +} diff --git a/MieSystem/Models/Expediente.cs b/MieSystem/Models/Expediente.cs index e2de5a2..4cd1cde 100644 --- a/MieSystem/Models/Expediente.cs +++ b/MieSystem/Models/Expediente.cs @@ -51,5 +51,57 @@ namespace MieSystem.Models [Column("activo")] 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}"; } } diff --git a/MieSystem/Models/ViewModels/AsistenciaViewModel.cs b/MieSystem/Models/ViewModels/AsistenciaViewModel.cs new file mode 100644 index 0000000..385fc26 --- /dev/null +++ b/MieSystem/Models/ViewModels/AsistenciaViewModel.cs @@ -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 Expedientes { get; set; } + public List DiasDelMes { get; set; } + public Dictionary Asistencias { get; set; } + } + + public class AsistenciaMasivaDto + { + public int ExpedienteId { get; set; } + public DateTime Fecha { get; set; } + public string Estado { get; set; } + } +} diff --git a/MieSystem/Models/ErrorViewModel.cs b/MieSystem/Models/ViewModels/ErrorViewModel.cs similarity index 100% rename from MieSystem/Models/ErrorViewModel.cs rename to MieSystem/Models/ViewModels/ErrorViewModel.cs diff --git a/MieSystem/Models/ExpedienteViewModel.cs b/MieSystem/Models/ViewModels/ExpedienteViewModel.cs similarity index 98% rename from MieSystem/Models/ExpedienteViewModel.cs rename to MieSystem/Models/ViewModels/ExpedienteViewModel.cs index 3cce929..c7d4f9c 100644 --- a/MieSystem/Models/ExpedienteViewModel.cs +++ b/MieSystem/Models/ViewModels/ExpedienteViewModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace MieSystem.Models +namespace MieSystem.Models.ViewModels { public class ExpedienteViewModel { diff --git a/MieSystem/Program.cs b/MieSystem/Program.cs index a12577d..96de74e 100644 --- a/MieSystem/Program.cs +++ b/MieSystem/Program.cs @@ -15,6 +15,7 @@ namespace MieSystem builder.Services.AddSingleton(); builder.Services.AddScoped(); + builder.Services.AddScoped(); var app = builder.Build(); app.UseStaticFiles(); // Configure the HTTP request pipeline. diff --git a/MieSystem/Views/Asistencia/Index.cshtml b/MieSystem/Views/Asistencia/Index.cshtml index e1dd794..99a9270 100644 --- a/MieSystem/Views/Asistencia/Index.cshtml +++ b/MieSystem/Views/Asistencia/Index.cshtml @@ -1,5 +1,495 @@ -@* - For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 -*@ +@model MieSystem.Models.ViewModels.AsistenciaViewModel @{ + ViewData["Title"] = "Control de Asistencia"; + + var diasSeleccionadosList = new List(); + 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(); +} + +
+
+
+
Filtros
+
+
+
+
+ + +
+ +
+ + +
+ +
+ +
+ @foreach (var dia in ViewBag.DiasSemana) + { + var isChecked = diasSeleccionadosList.Contains(dia.Value); +
+ + +
+ } + +
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+ + Asistencia - @Model.NombreMes @Model.Año + @Model.Expedientes.Count niños +
+
+ + +
+
+ +
+
+ + + + + + @foreach (var dia in diasAMostrar) + { + var esFinSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday; + + + } + + + + @foreach (var expediente in Model.Expedientes) + { + + + + @foreach (var dia in diasAMostrar) + { + var key = $"{expediente.Id}_{dia:yyyy-MM-dd}"; + var estadoActual = Model.Asistencias.ContainsKey(key) + ? Model.Asistencias[key] + : ""; + + + } + + } + +
+ Niño + +
@dia.ToString("ddd")
+
@dia.Day
+
+
@expediente.NombreCompleto
+
Edad: @expediente.Edad años
+
+ + +
+
+
+ + +
+
+ + + +@section Styles { + +} + +@section Scripts { + } diff --git a/MieSystem/Views/Expedientes/Details.cshtml b/MieSystem/Views/Expedientes/Details.cshtml index 7ef5a1f..d875137 100644 --- a/MieSystem/Views/Expedientes/Details.cshtml +++ b/MieSystem/Views/Expedientes/Details.cshtml @@ -1,4 +1,5 @@ -@model ExpedienteViewModel +@using MieSystem.Models.ViewModels +@model ExpedienteViewModel @{ Layout = null; ViewData["Title"] = "Expediente - " + Model.NombreCompleto; diff --git a/MieSystem/Views/Expedientes/Index.cshtml b/MieSystem/Views/Expedientes/Index.cshtml index 910632b..4f76755 100644 --- a/MieSystem/Views/Expedientes/Index.cshtml +++ b/MieSystem/Views/Expedientes/Index.cshtml @@ -1,4 +1,5 @@ -@{ +@using MieSystem.Models.ViewModels +@{ ViewData["Title"] = "Expedientes"; ViewData["ActionButtons"] = @" - diff --git a/MieSystem/Views/Expedientes/_CreateOrEdit.cshtml b/MieSystem/Views/Expedientes/_CreateOrEdit.cshtml index c838215..6838de6 100644 --- a/MieSystem/Views/Expedientes/_CreateOrEdit.cshtml +++ b/MieSystem/Views/Expedientes/_CreateOrEdit.cshtml @@ -1,4 +1,5 @@ @using MieSystem.Models; +@using MieSystem.Models.ViewModels @model ExpedienteViewModel diff --git a/MieSystem/Views/Shared/_Layout.cshtml b/MieSystem/Views/Shared/_Layout.cshtml index 8a9fe06..7607026 100644 --- a/MieSystem/Views/Shared/_Layout.cshtml +++ b/MieSystem/Views/Shared/_Layout.cshtml @@ -5,9 +5,13 @@ @ViewData["Title"] - MieSystem - + + + + + @await RenderSectionAsync("Styles", required: false)