Compare commits

..

2 Commits

Author SHA1 Message Date
3457721238 Todo 2025-12-25 13:54:49 -06:00
d405b61ddd Modulo de Asistencia 2025-12-25 13:54:14 -06:00
26 changed files with 3510 additions and 139 deletions

1
.gitignore vendored
View File

@@ -361,3 +361,4 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/MieSystem/wwwroot/uploads

View File

@@ -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<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
}

View File

@@ -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<Expediente>(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;
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" });

View 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);
}
}

View File

@@ -9,5 +9,6 @@ namespace MieSystem.Data.Interfaces
Task<int> CreateAsync(Expediente expediente);
Task<bool> UpdateAsync(Expediente expediente);
Task<bool> DeleteAsync(int id);
Task<IEnumerable<Expediente?>> GetActivosAsync();
}
}

View 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
}
}

View File

@@ -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()

View File

@@ -15,12 +15,6 @@ namespace MieSystem.Data.Repositories
_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)
{
using var connection = await _connectionFactory.CreateConnectionAsync();
@@ -92,34 +86,27 @@ namespace MieSystem.Data.Repositories
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()
{
using var connection = await _connectionFactory.CreateConnectionAsync();
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>(
@"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;
}
}
}

View File

@@ -21,6 +21,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\lib\bootstrap\dist\css\fonts\" />
<Folder Include="wwwroot\uploads\fotos\" />
</ItemGroup>

View 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"
};
}
}

View 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; }
}
}

View File

@@ -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}";
}
}

View 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; }
}
}

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace MieSystem.Models
namespace MieSystem.Models.ViewModels
{
public class ExpedienteViewModel
{

View File

@@ -15,6 +15,7 @@ namespace MieSystem
builder.Services.AddSingleton<IDatabaseConnectionFactory, DatabaseConnectionFactory>();
builder.Services.AddScoped<IExpedienteRepository, ExpedienteRepository>();
builder.Services.AddScoped<IAsistenciaRepository, AsistenciaRepository>();
var app = builder.Build();
app.UseStaticFiles();
// Configure the HTTP request pipeline.

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long