From ac96cb1f23fccae2c5b77a908191f4a1dafcffa6 Mon Sep 17 00:00:00 2001 From: adalberto Date: Fri, 26 Dec 2025 22:27:20 -0600 Subject: [PATCH] Nueva mejoras Y estabilidad --- MicroORM/DapperTypeMapRegistrar.cs | 46 + MicroORM/DbConnectionExtensions.cs | 18 + MicroORM/Exceptions/OrmException.cs | 7 + MicroORM/ExpressionToSqlTranslator.cs | 68 ++ MicroORM/Interfaces/IRepository.cs | 20 + MicroORM/MicroORM.cs | 7 + MicroORM/MicroORM.csproj | 15 + MicroORM/OrmMetadata.cs | 63 ++ MicroORM/Repository.cs | 610 +++++++++++++ MieSystem.sln | 6 + MieSystem/Controllers/AsistenciaController.cs | 39 +- .../Controllers/ExpedientesController.cs | 49 +- MieSystem/Data/NpgsqlConnectionFactory.cs | 21 + MieSystem/MieSystem.csproj | 5 + MieSystem/Models/Asistencia.cs | 61 +- MieSystem/Models/Expediente.cs | 100 ++- MieSystem/Models/Persona.cs | 43 + MieSystem/Program.cs | 25 +- MieSystem/Properties/launchSettings.json | 4 +- MieSystem/Services/AsistenciaService.cs | 93 ++ MieSystem/Services/ExpedienteService.cs | 35 + MieSystem/Views/Asistencia/Index.cshtml | 849 +++++++++++------- .../Views/Expedientes/_CreateOrEdit.cshtml | 137 ++- 23 files changed, 1841 insertions(+), 480 deletions(-) create mode 100644 MicroORM/DapperTypeMapRegistrar.cs create mode 100644 MicroORM/DbConnectionExtensions.cs create mode 100644 MicroORM/Exceptions/OrmException.cs create mode 100644 MicroORM/ExpressionToSqlTranslator.cs create mode 100644 MicroORM/Interfaces/IRepository.cs create mode 100644 MicroORM/MicroORM.cs create mode 100644 MicroORM/MicroORM.csproj create mode 100644 MicroORM/OrmMetadata.cs create mode 100644 MicroORM/Repository.cs create mode 100644 MieSystem/Data/NpgsqlConnectionFactory.cs create mode 100644 MieSystem/Models/Persona.cs create mode 100644 MieSystem/Services/AsistenciaService.cs create mode 100644 MieSystem/Services/ExpedienteService.cs diff --git a/MicroORM/DapperTypeMapRegistrar.cs b/MicroORM/DapperTypeMapRegistrar.cs new file mode 100644 index 0000000..968d5d4 --- /dev/null +++ b/MicroORM/DapperTypeMapRegistrar.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; +using Dapper; + +namespace MicroORM; + +public static class DapperTypeMapRegistrar +{ + public static void RegisterFor(Type type) + { + SqlMapper.SetTypeMap( + type, + new CustomPropertyTypeMap( + type, + (t, columnName) => + { + // [Column] + var prop = t.GetProperties() + .FirstOrDefault(p => + p.GetCustomAttribute()?.Name + .Equals(columnName, StringComparison.OrdinalIgnoreCase) == true + ); + + if (prop != null) + return prop; + + // Directo + prop = t.GetProperty(columnName, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (prop != null) + return prop; + + // snake_case + var pascal = string.Concat( + columnName.Split('_') + .Select(s => char.ToUpperInvariant(s[0]) + s[1..]) + ); + + return t.GetProperty(pascal, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + } + ) + ); + } +} \ No newline at end of file diff --git a/MicroORM/DbConnectionExtensions.cs b/MicroORM/DbConnectionExtensions.cs new file mode 100644 index 0000000..6421301 --- /dev/null +++ b/MicroORM/DbConnectionExtensions.cs @@ -0,0 +1,18 @@ +using System.Data; +using System.Data.Common; + +namespace MicroORM; + +public static class DbConnectionExtensions +{ + public static async Task EnsureOpenAsync(this IDbConnection connection) + { + if (connection.State != ConnectionState.Open) + { + if (connection is DbConnection db) + await db.OpenAsync(); + else + connection.Open(); + } + } +} \ No newline at end of file diff --git a/MicroORM/Exceptions/OrmException.cs b/MicroORM/Exceptions/OrmException.cs new file mode 100644 index 0000000..57d472e --- /dev/null +++ b/MicroORM/Exceptions/OrmException.cs @@ -0,0 +1,7 @@ +namespace MicroORM.Exceptions; + +public class OrmException : Exception +{ + public OrmException(string message, Exception? inner = null) + : base(message, inner) { } +} \ No newline at end of file diff --git a/MicroORM/ExpressionToSqlTranslator.cs b/MicroORM/ExpressionToSqlTranslator.cs new file mode 100644 index 0000000..310f785 --- /dev/null +++ b/MicroORM/ExpressionToSqlTranslator.cs @@ -0,0 +1,68 @@ +using System.Linq.Expressions; +using System.Reflection; +using Dapper; + +namespace MicroORM; + +public static class ExpressionToSqlTranslator +{ + public static string Translate( + Expression> expression, + DynamicParameters parameters) + { + return Visit(expression.Body, parameters); + } + + private static string Visit(Expression exp, DynamicParameters parameters) + { + return exp switch + { + BinaryExpression b => VisitBinary(b, parameters), + MemberExpression m => GetColumnName(m), + ConstantExpression c => AddParameter(c, parameters), + UnaryExpression u => Visit(u.Operand, parameters), + _ => throw new NotSupportedException($"Expression no soportada: {exp.NodeType}") + }; + } + + private static string VisitBinary(BinaryExpression b, DynamicParameters parameters) + { + var left = Visit(b.Left, parameters); + var right = Visit(b.Right, parameters); + + var op = b.NodeType switch + { + ExpressionType.Equal => "=", + ExpressionType.NotEqual => "<>", + ExpressionType.GreaterThan => ">", + ExpressionType.GreaterThanOrEqual => ">=", + ExpressionType.LessThan => "<", + ExpressionType.LessThanOrEqual => "<=", + ExpressionType.AndAlso => "AND", + ExpressionType.OrElse => "OR", + _ => throw new NotSupportedException($"Operador no soportado: {b.NodeType}") + }; + + return $"({left} {op} {right})"; + } + + private static string GetColumnName(MemberExpression m) + { + var prop = (PropertyInfo)m.Member; + return OrmMetadata.GetColumnName(prop); + } + + private static string AddParameter(ConstantExpression c, DynamicParameters parameters) + { + var name = $"@p{parameters.ParameterNames.Count()}"; + parameters.Add(name, c.Value); + return name; + } + + public static string GetOrderByColumn(Expression> exp) + { + var body = exp.Body is UnaryExpression u ? u.Operand : exp.Body; + var member = (MemberExpression)body; + return OrmMetadata.GetColumnName((PropertyInfo)member.Member); + } +} diff --git a/MicroORM/Interfaces/IRepository.cs b/MicroORM/Interfaces/IRepository.cs new file mode 100644 index 0000000..32f95ec --- /dev/null +++ b/MicroORM/Interfaces/IRepository.cs @@ -0,0 +1,20 @@ +using System.Data; +using System.Linq.Expressions; + +namespace MicroORM.Interfaces; + +public interface IRepository +{ + Task InsertAsync(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null); + Task UpdateAsync(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null); + Task GetByIdAsync(object id); + + Task> GetAllAsync(Expression>? filter = null, + Expression>? orderBy = null, bool descending = false); + Task DeleteAsync(object id); + Task SaveAsync(T entity); + Task> SaveAsyncList(IEnumerable entities); + Task QuerySingleAsync(string sql, object? parameters = null); + Task> QueryAsync(string sql, object? parameters = null); + Task ExecuteScalarAsync(string sql, object? parameters = null); +} \ No newline at end of file diff --git a/MicroORM/MicroORM.cs b/MicroORM/MicroORM.cs new file mode 100644 index 0000000..e2cb6b8 --- /dev/null +++ b/MicroORM/MicroORM.cs @@ -0,0 +1,7 @@ +namespace MicroORM; + +public class MicroORM +{ + + +} \ No newline at end of file diff --git a/MicroORM/MicroORM.csproj b/MicroORM/MicroORM.csproj new file mode 100644 index 0000000..3e33dd9 --- /dev/null +++ b/MicroORM/MicroORM.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + enable + enable + + + + + + + + + diff --git a/MicroORM/OrmMetadata.cs b/MicroORM/OrmMetadata.cs new file mode 100644 index 0000000..6c8e0e3 --- /dev/null +++ b/MicroORM/OrmMetadata.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Reflection; + +namespace MicroORM; + +public class OrmMetadata +{ + public static string GetTableName() + { + var table = typeof(T).GetCustomAttribute(); + if (table == null) + throw new InvalidOperationException("La entidad no tiene [Table]"); + + return string.IsNullOrEmpty(table.Schema) + ? table.Name + : $"{table.Schema}.{table.Name}"; + } + + public static PropertyInfo GetKeyProperty() + { + var key = typeof(T) + .GetProperties() + .FirstOrDefault(p => p.GetCustomAttribute() != null); + + if (key == null) + throw new InvalidOperationException("La entidad no tiene [Key]"); + + return key; + } + + public static string GetColumnName(PropertyInfo prop) + { + var columnAttr = prop.GetCustomAttribute(); + return columnAttr?.Name ?? prop.Name; + } + + public static bool IsIdentity(PropertyInfo prop) + { + var key = prop.GetCustomAttribute() != null; + var dbGenerated = prop.GetCustomAttribute(); + + return key && + dbGenerated?.DatabaseGeneratedOption == DatabaseGeneratedOption.Identity; + } + + public static IEnumerable GetScaffoldProperties() + { + var type = typeof(T); + var key = GetKeyProperty(); + + return type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => + p.CanWrite && // tiene setter + p.GetCustomAttribute() == null && + ( + p.GetCustomAttribute() != null || + p == key + ) + ); + } + +} \ No newline at end of file diff --git a/MicroORM/Repository.cs b/MicroORM/Repository.cs new file mode 100644 index 0000000..e49b1ef --- /dev/null +++ b/MicroORM/Repository.cs @@ -0,0 +1,610 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Data; +using System.Linq.Expressions; +using System.Reflection; +using Dapper; +using MicroORM.Exceptions; +using MicroORM.Interfaces; + +namespace MicroORM; + +public class Repository: IRepository +{ + private readonly Func> _connectionFactory; + private IRepository _repositoryImplementation; + + public Repository(Func> connectionFactory) + { + _connectionFactory = connectionFactory; + DapperTypeMapRegistrar.RegisterFor(typeof(T)); + } + + static Repository() + { + DapperTypeMapRegistrar.RegisterFor(typeof(T)); + } + + public async Task SaveAsync(T entity) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + + using var conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + using var tx = conn.BeginTransaction(); + + try + { + await ResolveRelationsAsync(entity, conn, tx); + + int id; + if (GetId(entity) == 0) + id = await InsertAsync(entity, conn, tx); + else + { + await UpdateAsync(entity, conn, tx); + id = GetId(entity); + } + + tx.Commit(); + return id; + } + catch + { + tx.Rollback(); + throw; + } + } + + + public async Task> SaveAsyncList(IEnumerable entities) + { + if (entities == null) + throw new ArgumentNullException(nameof(entities)); + + var list = entities as IList ?? entities.ToList(); + if (list.Count == 0) + return Array.Empty(); + + using var conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + using var tx = conn.BeginTransaction(); + + try + { + var ids = new List(list.Count); + + foreach (var entity in list) + { + if (entity == null) + throw new OrmException("La lista contiene una entidad nula"); + + await ResolveRelationsAsync(entity, conn, tx); + + int id; + if (GetId(entity) == 0) + { + id = await InsertAsync(entity, conn, tx); + SetId(entity, id); // MUY IMPORTANTE + } + else + { + await UpdateAsync(entity, conn, tx); + id = GetId(entity); + } + + ids.Add(id); + } + + tx.Commit(); + return ids; + } + catch + { + tx.Rollback(); + throw; + } + } + + + public async Task InsertAsync(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + + bool ownsConnection = connection == null; + bool ownsTransaction = transaction == null; + + IDbConnection? conn = connection; + IDbTransaction? tx = transaction; + + try + { + var table = OrmMetadata.GetTableName(); + var key = OrmMetadata.GetKeyProperty(); + var props = OrmMetadata.GetScaffoldProperties().Where(p => !OrmMetadata.IsIdentity(p)).ToArray(); + + if (!props.Any()) + throw new OrmException($"No hay columnas insertables para {typeof(T).Name}"); + + var columns = props.Select(p => OrmMetadata.GetColumnName(p)); + var parameters = props.Select(p => "@" + p.Name); + + var sql = $@" + INSERT INTO {table} + ({string.Join(", ", columns)}) + VALUES ({string.Join(", ", parameters)}) + RETURNING {OrmMetadata.GetColumnName(key)}; + "; + + if (conn == null) + { + conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + } + + if (tx == null) + { + tx = conn.BeginTransaction(); + } + + var id = await conn.ExecuteScalarAsync( + sql, + entity, + tx + ); + + if (ownsTransaction) + tx.Commit(); + + return id; + } + catch (Exception ex) + { + if (ownsTransaction) + try { tx?.Rollback(); } catch { /* swallow */ } + + throw new OrmException( + $"Error INSERT en {typeof(T).Name}", + ex + ); + } + finally + { + if (ownsTransaction) + tx?.Dispose(); + + if (ownsConnection) + conn?.Dispose(); + } + } + + public async Task UpdateAsync(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + + bool ownsConnection = connection == null; + bool ownsTransaction = transaction == null; + + IDbConnection? conn = connection; + IDbTransaction? tx = transaction; + + try + { + var table = OrmMetadata.GetTableName(); + var key = OrmMetadata.GetKeyProperty(); + var keyValue = key.GetValue(entity); + + if (keyValue == null) + throw new OrmException($"La entidad {typeof(T).Name} no tiene valor en la clave primaria"); + + var props = OrmMetadata.GetScaffoldProperties().Where(p => !OrmMetadata.IsIdentity(p)).ToArray(); + if (!props.Any()) + return false; + + var sets = props.Select(p => + $"{OrmMetadata.GetColumnName(p)} = @{p.Name}"); + + var sql = $@" + UPDATE {table} + SET {string.Join(", ", sets)} + WHERE {OrmMetadata.GetColumnName(key)} = @{key.Name}; + "; + + if (conn == null) + { + conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + } + + if (tx == null) + { + tx = conn.BeginTransaction(); + } + + var affected = await conn.ExecuteAsync( + sql, + entity, + tx + ); + + if (ownsTransaction) + tx.Commit(); + + return affected > 0; + } + catch (Exception ex) + { + if (ownsTransaction) + try { tx?.Rollback(); } catch { /* ignore */ } + + throw new OrmException( + $"Error UPDATE en {typeof(T).Name}", + ex + ); + } + finally + { + if (ownsTransaction) + tx?.Dispose(); + + if (ownsConnection) + conn?.Dispose(); + } + } + + public async Task GetByIdAsync(object id) + { + if (id == null) + throw new ArgumentNullException(nameof(id)); + + try + { + var table = OrmMetadata.GetTableName(); + var key = OrmMetadata.GetKeyProperty(); + + var sql = $@" + SELECT * + FROM {table} + WHERE {OrmMetadata.GetColumnName(key)} = @Id; + "; + + using var conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + + return await conn.QueryFirstOrDefaultAsync(sql, new { Id = id }); + } + catch (Exception ex) + { + throw new OrmException( + $"Error SELECT por ID en {typeof(T).Name}", + ex + ); + } + } + + public async Task> GetAllAsync(Expression>? filter = null, Expression>? orderBy = null, bool descending = false) + { + try + { + var table = OrmMetadata.GetTableName(); + var parameters = new DynamicParameters(); + + var sql = $"SELECT * FROM {table}"; + + // WHERE dinámico + if (filter != null) + { + var where = ExpressionToSqlTranslator.Translate(filter, parameters); + sql += $" WHERE {where}"; + } + + // ORDER BY dinámico + if (orderBy != null) + { + var column = ExpressionToSqlTranslator.GetOrderByColumn(orderBy); + sql += $" ORDER BY {column} {(descending ? "DESC" : "ASC")}"; + } + + sql += ";"; + + using var conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + + return await conn.QueryAsync(sql, parameters); + } + catch (Exception ex) + { + throw new OrmException( + $"Error SELECT dinámico en {typeof(T).Name}", + ex + ); + } + } + + + + public async Task DeleteAsync(object id) + { + if (id == null) + throw new ArgumentNullException(nameof(id)); + + try + { + var table = OrmMetadata.GetTableName(); + var key = OrmMetadata.GetKeyProperty(); + + var sql = $@" + DELETE FROM {table} + WHERE {OrmMetadata.GetColumnName(key)} = @Id; + "; + + using var conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + + return await conn.ExecuteAsync(sql, new { Id = id }) > 0; + } + catch (Exception ex) + { + throw new OrmException( + $"Error DELETE en {typeof(T).Name}", + ex + ); + } + } + + public async Task QuerySingleAsync(string sql, object? parameters = null) + { + if (string.IsNullOrWhiteSpace(sql)) + throw new ArgumentException("SQL inválido", nameof(sql)); + + try + { + using var conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + + return await conn.QueryFirstOrDefaultAsync( + sql, + parameters + ); + } + catch (Exception ex) + { + throw new OrmException( + $"Error en QuerySingle<{typeof(T).Name}>", + ex + ); + } + } + + public async Task> QueryAsync(string sql, object? parameters = null) + { + if (string.IsNullOrWhiteSpace(sql)) + throw new ArgumentException("SQL inválido", nameof(sql)); + + try + { + using var conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + + return await conn.QueryAsync(sql, parameters); + } + catch (Exception ex) + { + throw new OrmException( + $"Error en Query<{typeof(T).Name}>", + ex + ); + } + } + + + public async Task ExecuteScalarAsync(string sql, object? parameters = null) + { + if (string.IsNullOrWhiteSpace(sql)) + throw new ArgumentException("SQL inválido", nameof(sql)); + + try + { + using var conn = await _connectionFactory(); + await conn.EnsureOpenAsync(); + + return await conn.ExecuteScalarAsync( + sql, + parameters + ); + } + catch (Exception ex) + { + throw new OrmException( + $"Error en ExecuteScalar<{typeof(T).Name}>", + ex + ); + } + } + + + private async Task ResolveRelationsAsync( + T entity, + IDbConnection conn, + IDbTransaction tx) + { + var navProps = typeof(T) + .GetProperties() + .Where(p => + p.PropertyType.IsClass && + p.PropertyType != typeof(string) && + p.GetCustomAttribute() == null); + + foreach (var nav in navProps) + { + var related = nav.GetValue(entity); + if (related == null) + continue; + + var relatedType = nav.PropertyType; + var relatedId = GetId(related); + + if (relatedId == 0) + relatedId = await InsertDynamicAsync(related, conn, tx); + else + await UpdateDynamicAsync(related, conn, tx); + + SetForeignKey(entity, nav, relatedId); + } + } + + private void SetForeignKey( + T entity, + PropertyInfo navProp, + int fkValue) + { + var fkAttr = navProp.GetCustomAttribute(); + if (fkAttr != null) + { + var fkProp = typeof(T).GetProperty(fkAttr.Name); + fkProp?.SetValue(entity, fkValue); + return; + } + + var fkName = navProp.Name + "Id"; + var prop = typeof(T).GetProperty(fkName); + + if (prop == null) + throw new OrmException($"No se pudo resolver FK para {navProp.Name}"); + + prop.SetValue(entity, fkValue); + } + + + private async Task InsertDynamicAsync( + object entity, + IDbConnection conn, + IDbTransaction tx) + { + var method = typeof(Repository<>) + .GetMethod(nameof(InsertAsync))! + .MakeGenericMethod(entity.GetType()); + + return await (Task)method.Invoke(this, new[] { entity, conn, tx })!; + } + + + private async Task UpdateDynamicAsync( + object entity, + IDbConnection conn, + IDbTransaction tx) + { + var method = typeof(Repository<>) + .GetMethod(nameof(UpdateAsync))! + .MakeGenericMethod(entity.GetType()); + + return await (Task)method.Invoke(this, new[] { entity, conn, tx })!; + } + + + private static int GetId(object entity) + { + if (entity == null) throw new ArgumentNullException(nameof(entity)); + + var type = entity.GetType(); + + var keyProp = type.GetProperties() + .FirstOrDefault(p => p.GetCustomAttribute() != null); + + if (keyProp == null) + { + keyProp = type.GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) + ?? type.GetProperty(type.Name + "Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + } + + if (keyProp == null) + throw new OrmException($"No se encontró la propiedad clave (Key/Id/{type.Name}Id) en el tipo {type.FullName}"); + + var value = keyProp.GetValue(entity); + if (value == null) return 0; + if (value is int i) return i; + + if (value is long l) + { + if (l > int.MaxValue || l < int.MinValue) + throw new OrmException($"El valor de la clave primaria ({l}) excede el rango de int para {type.FullName}"); + return (int)l; + } + + try + { + var converted = Convert.ToInt32(value); + return converted; + } + catch (Exception) + { + if (value is Guid) + throw new OrmException($"La clave primaria de {type.FullName} es un GUID; el flujo actual espera claves numéricas. Ajusta el método si deseas soportar GUIDs."); + + throw new OrmException($"No fue posible convertir la clave primaria de {type.FullName} (tipo {value.GetType().Name}) a int."); + } + } + + private static void SetId(object entity, int id) + { + if (entity == null) throw new ArgumentNullException(nameof(entity)); + + var type = entity.GetType(); + + // Buscar la propiedad clave (igual que en GetId) + var keyProp = type.GetProperties() + .FirstOrDefault(p => p.GetCustomAttribute() != null); + + if (keyProp == null) + { + keyProp = type.GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase) + ?? type.GetProperty(type.Name + "Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + } + + if (keyProp == null) + throw new OrmException($"No se encontró la propiedad clave (Key/Id/{type.Name}Id) en el tipo {type.FullName}"); + + if (!keyProp.CanWrite) + throw new OrmException($"La propiedad clave {keyProp.Name} en {type.FullName} no tiene setter público."); + + var targetType = Nullable.GetUnderlyingType(keyProp.PropertyType) ?? keyProp.PropertyType; + + object valueToSet; + if (targetType == typeof(int)) + { + valueToSet = id; + } + else if (targetType == typeof(long)) + { + valueToSet = (long)id; + } + else if (targetType == typeof(short)) + { + valueToSet = (short)id; + } + else if (targetType == typeof(byte)) + { + valueToSet = (byte)id; + } + else if (targetType == typeof(Guid)) + { + throw new OrmException($"La clave primaria de {type.FullName} es GUID; SetId con int no es aplicable."); + } + else + { + try + { + valueToSet = Convert.ChangeType(id, targetType); + } + catch (Exception ex) + { + throw new OrmException($"No fue posible convertir int a {targetType.Name} para la clave de {type.FullName}.", ex); + } + } + + keyProp.SetValue(entity, valueToSet); + } +} \ No newline at end of file diff --git a/MieSystem.sln b/MieSystem.sln index 8510846..98bbdf9 100644 --- a/MieSystem.sln +++ b/MieSystem.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.14.36327.8 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MieSystem", "MieSystem\MieSystem.csproj", "{5D8C00CA-A04E-4414-AC79-195A25059557}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroORM", "MicroORM\MicroORM.csproj", "{43F35A16-C2FC-48D5-B2E9-5432CEDFD451}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {5D8C00CA-A04E-4414-AC79-195A25059557}.Debug|Any CPU.Build.0 = Debug|Any CPU {5D8C00CA-A04E-4414-AC79-195A25059557}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D8C00CA-A04E-4414-AC79-195A25059557}.Release|Any CPU.Build.0 = Release|Any CPU + {43F35A16-C2FC-48D5-B2E9-5432CEDFD451}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43F35A16-C2FC-48D5-B2E9-5432CEDFD451}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43F35A16-C2FC-48D5-B2E9-5432CEDFD451}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43F35A16-C2FC-48D5-B2E9-5432CEDFD451}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MieSystem/Controllers/AsistenciaController.cs b/MieSystem/Controllers/AsistenciaController.cs index 464c8b9..922c1ba 100644 --- a/MieSystem/Controllers/AsistenciaController.cs +++ b/MieSystem/Controllers/AsistenciaController.cs @@ -1,41 +1,30 @@ 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; +using MieSystem.Services; namespace MieSystem.Controllers { public class AsistenciaController : Controller { - private readonly IExpedienteRepository _expedienteRepository; - private readonly IAsistenciaRepository _asistenciaRepository; - - public AsistenciaController( - IExpedienteRepository expedienteRepository, - IAsistenciaRepository asistenciaRepository) + private readonly ExpedienteService expedienteService; + private readonly AsistenciaService asistenciaService; + public AsistenciaController(ExpedienteService expedienteService, AsistenciaService asistenciaService) { - _expedienteRepository = expedienteRepository; - _asistenciaRepository = asistenciaRepository; + this.expedienteService = expedienteService; + this.asistenciaService = asistenciaService; } 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(); + var expedientes = await expedienteService.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(',') @@ -45,7 +34,7 @@ namespace MieSystem.Controllers } // Obtener asistencias para el mes - var asistencias = await _asistenciaRepository.GetAsistenciasPorMesAsync( + var asistencias = await asistenciaService.GetAsistenciasPorMesAsync( añoSeleccionado, mesSeleccionado); // Crear diccionario para acceso rápido @@ -53,7 +42,7 @@ namespace MieSystem.Controllers foreach (var asistencia in asistencias) { var key = $"{asistencia.ExpedienteId}_{asistencia.Fecha:yyyy-MM-dd}"; - dictAsistencias[key] = asistencia.Estado; + dictAsistencias[key] = asistencia.Estado.ToString(); } // Crear modelo de vista @@ -89,11 +78,11 @@ namespace MieSystem.Controllers { ExpedienteId = expedienteId, Fecha = fechaDate, - Estado = estado, + Estado = estado[0], UsuarioRegistro = User.Identity?.Name ?? "Sistema" }; - var resultado = await _asistenciaRepository.GuardarAsistenciaAsync(asistencia); + var resultado = await asistenciaService.SaveAsync(asistencia); return Json(new { success = resultado, message = "Asistencia guardada" }); } @@ -116,13 +105,13 @@ namespace MieSystem.Controllers { ExpedienteId = asistenciaDto.ExpedienteId, Fecha = asistenciaDto.Fecha, - Estado = asistenciaDto.Estado, + Estado = asistenciaDto.Estado[0], UsuarioRegistro = User.Identity?.Name ?? "Sistema" }; asistenciasModel.Add(asistencia); } - var resultado = await _asistenciaRepository.GuardarAsistenciasMasivasAsync(asistenciasModel); + var resultado = asistenciaService.GuardarAsistenciasMasivasAsync(asistenciasModel); return Json(new { @@ -141,7 +130,7 @@ namespace MieSystem.Controllers { try { - var estadisticas = await _asistenciaRepository.GetEstadisticasMesAsync(año, mes); + var estadisticas = await asistenciaService.GetEstadisticasMesAsync(año, mes); return Json(estadisticas); } catch (Exception ex) diff --git a/MieSystem/Controllers/ExpedientesController.cs b/MieSystem/Controllers/ExpedientesController.cs index 63a84ee..7111181 100644 --- a/MieSystem/Controllers/ExpedientesController.cs +++ b/MieSystem/Controllers/ExpedientesController.cs @@ -1,29 +1,27 @@ using Microsoft.AspNetCore.Mvc; -using MieSystem.Data.Interfaces; using MieSystem.Models; using MieSystem.Models.ViewModels; +using MieSystem.Services; namespace MieSystem.Controllers { public class ExpedientesController : Controller { private static List _expedientes = new List(); - private static int _idCounter = 1; + //private static int _idCounter = 1; private readonly IWebHostEnvironment _hostingEnvironment; - - private readonly IExpedienteRepository _expedienteRepository; - public ExpedientesController(IExpedienteRepository expedienteRepository, IWebHostEnvironment hostingEnvironment) + private readonly ExpedienteService expedienteService; + + public ExpedientesController(ExpedienteService expedienteService, IWebHostEnvironment hostingEnvironment) { _hostingEnvironment = hostingEnvironment; - - _expedienteRepository = expedienteRepository; + this.expedienteService = expedienteService; } // GET: Expedientes public async Task Index() { - var today = DateTime.Today; - var lst = await _expedienteRepository.GetAllAsync(); + var lst = await expedienteService.GetAllAsync(); _expedientes = [.. lst]; return View(); } @@ -83,7 +81,7 @@ namespace MieSystem.Controllers FechaNacimiento = e.FechaNacimiento, Sexo = e.Sexo, NombreResponsable = e.NombreResponsable, - FotoUrl = e.FotoUrl + FotoUrl = ExisteFoto(e.FotoUrl) }) .ToList(); @@ -140,7 +138,6 @@ namespace MieSystem.Controllers // Crear nuevo expediente var expediente = new Expediente { - Id = _idCounter++, Nombre = model.Nombre, Apellidos = model.Apellidos, FechaNacimiento = model.FechaNacimiento, @@ -160,7 +157,8 @@ namespace MieSystem.Controllers try { - await _expedienteRepository.CreateAsync(expediente); + //await _expedienteRepository.CreateAsync(expediente); + await expedienteService.SaveAsync(expediente); return Json(new { success = true, message = "Expediente creado exitosamente" }); } catch (Exception ex) @@ -217,7 +215,8 @@ namespace MieSystem.Controllers ModelState.Remove("Observaciones"); if (ModelState.IsValid) { - var expediente = await _expedienteRepository.GetByIdAsync(id); + //var expediente = await _expedienteRepository.GetByIdAsync(id); + var expediente = await expedienteService.GetByIdAsync(id); //var expediente = _expedientes.FirstOrDefault(e => e.Id == id); if (expediente == null) { @@ -278,7 +277,8 @@ namespace MieSystem.Controllers try { - await _expedienteRepository.UpdateAsync(expediente); + //await _expedienteRepository.UpdateAsync(expediente); + await expedienteService.SaveAsync(expediente); return Json(new { success = true, message = "Expediente actualizado exitosamente" }); } @@ -292,7 +292,7 @@ namespace MieSystem.Controllers } // DELETE: Expedientes/Delete/5 - [HttpDelete] + /*[HttpDelete] public async Task Delete(int id) { var expediente = _expedientes.FirstOrDefault(e => e.Id == id); @@ -318,7 +318,7 @@ namespace MieSystem.Controllers return Json(new { success = true, message = "Expediente eliminado exitosamente" }); } - +*/ // GET: Expedientes/Details/5 (Opcional) public IActionResult Details(int id) { @@ -410,6 +410,23 @@ namespace MieSystem.Controllers } } + public string ExisteFoto(string url) + { + if(string.IsNullOrEmpty(url)) + return Path.Combine("images", "default-avatar.png"); + + var uploadsFolder = Path.Combine(_hostingEnvironment.WebRootPath, "uploads", "fotos"); + string[] parts = url.Split('/'); + string name = parts[^1]; + + string fullpath = Path.Combine(uploadsFolder, name); + + if (System.IO.File.Exists(fullpath)) + return url; + + return Path.Combine("images", "default-avatar.png"); + } + // GET: Expedientes/DeleteImage [HttpDelete] public IActionResult DeleteImage(string imageUrl) diff --git a/MieSystem/Data/NpgsqlConnectionFactory.cs b/MieSystem/Data/NpgsqlConnectionFactory.cs new file mode 100644 index 0000000..04cb127 --- /dev/null +++ b/MieSystem/Data/NpgsqlConnectionFactory.cs @@ -0,0 +1,21 @@ +using System.Data; +using Npgsql; + +namespace MieSystem.Data; + +public class NpgsqlConnectionFactory +{ + private readonly string _connectionString; + + public NpgsqlConnectionFactory(IConfiguration configuration) + { + _connectionString = configuration.GetConnectionString("PostgreSQL") + ?? throw new InvalidOperationException("ConnectionString PostgreSQL no configurada"); + } + + public Task CreateAsync() + { + IDbConnection conn = new NpgsqlConnection(_connectionString); + return Task.FromResult(conn); + } +} \ No newline at end of file diff --git a/MieSystem/MieSystem.csproj b/MieSystem/MieSystem.csproj index e494daf..fd081a8 100644 --- a/MieSystem/MieSystem.csproj +++ b/MieSystem/MieSystem.csproj @@ -12,6 +12,7 @@ + @@ -25,4 +26,8 @@ + + + + diff --git a/MieSystem/Models/Asistencia.cs b/MieSystem/Models/Asistencia.cs index 5c4a86e..2c90ea2 100644 --- a/MieSystem/Models/Asistencia.cs +++ b/MieSystem/Models/Asistencia.cs @@ -1,33 +1,64 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace MieSystem.Models { + [Table("asistencia", Schema = "public")] public class Asistencia { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Column("id")] 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 + [Required] + [Column("expediente_id")] + public int ExpedienteId { get; set; } + + [Required] + [Column("fecha")] + public DateTime Fecha { get; set; } + + [Required] + [Column("estado")] + public char Estado { get; set; } // P, T, F + + [Column("hora_entrada")] + public TimeSpan? HoraEntrada { get; set; } + + [Column("hora_salida")] + public TimeSpan? HoraSalida { get; set; } + + [Column("observaciones")] + public string? Observaciones { get; set; } + + [Required] + [Column("fecha_registro")] + public DateTime FechaRegistro { get; set; } + + [StringLength(100)] + [Column("usuario_registro")] + public string? UsuarioRegistro { get; set; } + + // ========================== + // PROPIEDADES CALCULADAS + // ========================== + + [NotMapped] public string EstadoTexto => Estado switch { - "P" => "Presente", - "T" => "Tarde", - "F" => "Falto", + 'P' => "Presente", + 'T' => "Tarde", + 'F' => "Faltó", _ => "Desconocido" }; + [NotMapped] public string ColorEstado => Estado switch { - "P" => "success", - "T" => "warning", - "F" => "danger", + 'P' => "success", + 'T' => "warning", + 'F' => "danger", _ => "secondary" }; } diff --git a/MieSystem/Models/Expediente.cs b/MieSystem/Models/Expediente.cs index 4cd1cde..d327063 100644 --- a/MieSystem/Models/Expediente.cs +++ b/MieSystem/Models/Expediente.cs @@ -1,62 +1,85 @@ -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace MieSystem.Models { + [Table("expedientes")] public class Expediente { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] [Column("id")] public int Id { get; set; } - + + [Required] + [StringLength(100)] [Column("nombre")] - public string Nombre { get; set; } - + public string Nombre { get; set; } = string.Empty; + + [Required] + [StringLength(150)] [Column("apellidos")] - public string Apellidos { get; set; } - + public string Apellidos { get; set; } = string.Empty; + + [Required] [Column("fecha_nacimiento")] public DateTime FechaNacimiento { get; set; } - - [Column("nombre_padre")] - public string NombrePadre { get; set; } - - [Column("nombre_madre")] - public string NombreMadre { get; set; } - - [Column("nombre_responsable")] - public string NombreResponsable { get; set; } - - [Column("parentesco_responsable")] - public string ParentescoResponsable { get; set; } - + + [Required] + [StringLength(1)] [Column("sexo")] - public string Sexo { get; set; } - + public string Sexo { get; set; } = string.Empty; + + [StringLength(150)] + [Column("nombre_padre")] + public string? NombrePadre { get; set; } + + [StringLength(150)] + [Column("nombre_madre")] + public string? NombreMadre { get; set; } + + [StringLength(150)] + [Column("nombre_responsable")] + public string? NombreResponsable { get; set; } + + [StringLength(50)] + [Column("parentesco_responsable")] + public string? ParentescoResponsable { get; set; } + [Column("direccion")] - public string Direccion { get; set; } - + public string? Direccion { get; set; } + + [StringLength(20)] [Column("telefono")] - public string Telefono { get; set; } - + public string? Telefono { get; set; } + [Column("observaciones")] - public string Observaciones { get; set; } - + public string? Observaciones { get; set; } + + [StringLength(500)] [Column("foto_url")] - public string FotoUrl { get; set; } - + public string? FotoUrl { get; set; } + + [Required] [Column("fecha_creacion")] public DateTime FechaCreacion { get; set; } - + + [Required] [Column("fecha_actualizacion")] public DateTime FechaActualizacion { get; set; } - + + [Required] [Column("activo")] public bool Activo { get; set; } + // ============================ + // 🔹 Propiedades calculadas + // ============================ - - // Propiedades calculadas (solo lectura) + [NotMapped] public string NombreCompleto => $"{Nombre} {Apellidos}".Trim(); + [NotMapped] public int Edad { get @@ -64,17 +87,14 @@ namespace MieSystem.Models 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 + [NotMapped] public string EdadConMeses { get @@ -84,9 +104,7 @@ namespace MieSystem.Models var months = today.Month - FechaNacimiento.Month; if (today.Day < FechaNacimiento.Day) - { months--; - } if (months < 0) { @@ -98,10 +116,10 @@ namespace MieSystem.Models } } - // Para mostrar en selectores + [NotMapped] public string NombreConEdad => $"{NombreCompleto} ({Edad} años)"; - // Para mostrar en listas + [NotMapped] public string InformacionBasica => $"{NombreCompleto} | {Edad} años | {Sexo}"; } } diff --git a/MieSystem/Models/Persona.cs b/MieSystem/Models/Persona.cs new file mode 100644 index 0000000..e0a4c37 --- /dev/null +++ b/MieSystem/Models/Persona.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MieSystem.Models; + +[Table("Personas", Schema = "public")] +public class Persona +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Column("Id")] + public int Id { get; set; } + + [Required] + [StringLength(250)] + [Column("Nombres")] + public string Nombres { get; set; } = string.Empty; + + [Required] + [StringLength(250)] + [Column("Apellidos")] + public string Apellidos { get; set; } = string.Empty; + + [StringLength(50)] + [Column("DUI")] + public string? Dui { get; set; } + + [StringLength(1000)] + [Column("Direccion")] + public string? Direccion { get; set; } + + [Required] + [Column("sexo")] + public char Sexo { get; set; } + + [Required] + [Column("FechaNacimiento")] + public DateTime FechaNacimiento { get; set; } + + [StringLength(1000)] + [Column("FotoUrl")] + public string? FotoUrl { get; set; } +} \ No newline at end of file diff --git a/MieSystem/Program.cs b/MieSystem/Program.cs index 96de74e..977cfa9 100644 --- a/MieSystem/Program.cs +++ b/MieSystem/Program.cs @@ -1,5 +1,10 @@ +using System.Data; +using MicroORM; +using MicroORM.Interfaces; +using MieSystem.Data; using MieSystem.Data.Interfaces; using MieSystem.Data.Repositories; +using MieSystem.Services; namespace MieSystem { @@ -10,11 +15,27 @@ namespace MieSystem var builder = WebApplication.CreateBuilder(args); // Add services to the container. - builder.Services.AddControllersWithViews(); + builder.Services.AddControllersWithViews().AddRazorRuntimeCompilation(); builder.Services.AddSingleton(); + + /* Esto es lo nuevo */ + builder.Services.AddSingleton(); + + builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); - builder.Services.AddScoped(); + // Delegate que consume el Repository + builder.Services.AddScoped>>(sp => + { + var factory = sp.GetRequiredService(); + return factory.CreateAsync; + }); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + /***********/ + + //builder.Services.AddScoped(); builder.Services.AddScoped(); var app = builder.Build(); app.UseStaticFiles(); diff --git a/MieSystem/Properties/launchSettings.json b/MieSystem/Properties/launchSettings.json index 1678432..d11a0b5 100644 --- a/MieSystem/Properties/launchSettings.json +++ b/MieSystem/Properties/launchSettings.json @@ -7,7 +7,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, - "applicationUrl": "http://localhost:5037" + "applicationUrl": "http://localhost:8090" }, "https": { "commandName": "Project", @@ -16,7 +16,7 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, - "applicationUrl": "https://localhost:7037;http://localhost:5037" + "applicationUrl": "https://localhost:7037;http://localhost:8090" }, "Container (Dockerfile)": { "commandName": "Docker", diff --git a/MieSystem/Services/AsistenciaService.cs b/MieSystem/Services/AsistenciaService.cs new file mode 100644 index 0000000..3d715cb --- /dev/null +++ b/MieSystem/Services/AsistenciaService.cs @@ -0,0 +1,93 @@ +using System.Diagnostics.Tracing; +using MicroORM.Interfaces; +using MieSystem.Models; + +namespace MieSystem.Services; + +public class AsistenciaService +{ + private readonly IRepository _repo; + + public AsistenciaService(IRepository expe) + { + _repo = expe; + } + public Task SaveAsync(Asistencia expediente) + { + return _repo.SaveAsync(expediente); + } + + public Task GetByIdAsync(object id) + { + return _repo.GetByIdAsync(id); + } + + public Task> GetAllAsync() + { + return _repo.GetAllAsync(); + } + + public Task> GetAsistenciasPorMesAsync(int añoSeleccionado, int mesSeleccionado) + { + 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) = @Anio + AND EXTRACT(MONTH FROM fecha) = @Mes + ORDER BY fecha, expediente_id"; + + return _repo.QueryAsync(sql, new {Anio = añoSeleccionado, Mes = mesSeleccionado}); + } + + public Task GetEstadisticasMesAsync(int año, int mes) + { + 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) = @Anio + AND EXTRACT(MONTH FROM fecha) = @Mes"; + + return _repo.QuerySingleAsync(sql, new { Anio = año, Mes = mes }); + } + public bool GuardarAsistenciasMasivasAsync(List lst) + { + try + { + _repo.SaveAsyncList(lst); + return true; + } + catch (Exception e) + { + Console.WriteLine(e); + return true; + } + } +} \ No newline at end of file diff --git a/MieSystem/Services/ExpedienteService.cs b/MieSystem/Services/ExpedienteService.cs new file mode 100644 index 0000000..2538050 --- /dev/null +++ b/MieSystem/Services/ExpedienteService.cs @@ -0,0 +1,35 @@ +using MicroORM.Interfaces; +using MieSystem.Models; + +namespace MieSystem.Services; + +public class ExpedienteService +{ + private readonly IRepository _repo; + + public ExpedienteService(IRepository expe) + { + _repo = expe; + } + + public Task SaveAsync(Expediente expediente) + { + return _repo.SaveAsync(expediente); + } + + public Task GetByIdAsync(object id) + { + return _repo.GetByIdAsync(id); + } + + public Task> GetAllAsync() + { + return _repo.GetAllAsync(); + } + + public Task> GetActivosAsync() + { + return _repo.GetAllAsync(e=> e.Activo == true); + } + +} \ No newline at end of file diff --git a/MieSystem/Views/Asistencia/Index.cshtml b/MieSystem/Views/Asistencia/Index.cshtml index 3a8262d..549f2d2 100644 --- a/MieSystem/Views/Asistencia/Index.cshtml +++ b/MieSystem/Views/Asistencia/Index.cshtml @@ -1,7 +1,7 @@ @model MieSystem.Models.ViewModels.AsistenciaViewModel @{ ViewData["Title"] = "Control de Asistencia"; - + var diasSeleccionadosList = new List(); if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados)) { @@ -10,10 +10,10 @@ .Select(d => d.Trim()) .ToList(); } - + var diasAMostrar = Model.DiasDelMes - .Where(d => diasSeleccionadosList.Count == 0 || - diasSeleccionadosList.Contains(((int)d.DayOfWeek).ToString())) + .Where(d => diasSeleccionadosList.Count == 0 || + diasSeleccionadosList.Contains(((int)d.DayOfWeek).ToString())) .ToList(); } @@ -35,7 +35,7 @@ } - +
- +
@@ -55,9 +55,9 @@ { var isChecked = diasSeleccionadosList.Contains(dia.Value);
-
} -
- +
-
@@ -85,7 +86,7 @@
- + Asistencia - @Model.NombreMes @Model.Año @Model.Expedientes.Count niños
@@ -98,131 +99,186 @@
- +
- - - - - @foreach (var dia in Model.DiasDelMes) - { - var diaSemana = ((int)dia.DayOfWeek).ToString(); - var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true; - - if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked) - { - continue; - } - - var nombreDia = dia.ToString("ddd", new System.Globalization.CultureInfo("es-ES")); - var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday; - - - } - - - - @foreach (var expediente in Model.Expedientes) - { - - - var nombreCompleto = $"{expediente.Nombre} {expediente.Apellidos}".Trim(); - var edad = 0; - if (expediente.FechaNacimiento != DateTime.MinValue) - { - var today = DateTime.Today; - edad = today.Year - expediente.FechaNacimiento.Year; - if (expediente.FechaNacimiento.Date > today.AddYears(-edad)) +
-
Nombre
-
Edad
-
-
@dia.Day
-
@nombreDia
-
+ + + + @foreach (var dia in Model.DiasDelMes) + { + var diaSemana = ((int)dia.DayOfWeek).ToString(); + var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true; + + if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked) + { + continue; + } + + var nombreDia = dia.ToString("ddd", new System.Globalization.CultureInfo("es-ES")); + var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday; + + + } + + + + @foreach (var expediente in Model.Expedientes) { - edad--; + + + var nombreCompleto = $"{expediente.Nombre} {expediente.Apellidos}".Trim(); + var edad = 0; + if (expediente.FechaNacimiento != DateTime.MinValue) + { + var today = DateTime.Today; + edad = today.Year - expediente.FechaNacimiento.Year; + if (expediente.FechaNacimiento.Date > today.AddYears(-edad)) + { + edad--; + } + } + + + + + @foreach (var dia in Model.DiasDelMes) + { + var diaSemana = ((int)dia.DayOfWeek).ToString(); + var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true; + + if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked) + { + continue; + } + + var key = $"{expediente.Id}_{dia:yyyy-MM-dd}"; + var estadoActual = Model.Asistencias.ContainsKey(key) + ? Model.Asistencias[key] + : ""; + + // Determinar clase CSS según estado + var claseEstado = estadoActual switch + { + "P" => "celda-presente", + "T" => "celda-tarde", + "F" => "celda-falta", + _ => "" + }; + + var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday; + + + } + } - } - - - - - @foreach (var dia in Model.DiasDelMes) - { - var diaSemana = ((int)dia.DayOfWeek).ToString(); - var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true; - - if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked) - { - continue; - } - - var key = $"{expediente.Id}_{dia:yyyy-MM-dd}"; - var estadoActual = Model.Asistencias.ContainsKey(key) - ? Model.Asistencias[key] - : ""; - - // Determinar clase CSS según estado - var claseEstado = estadoActual switch - { - "P" => "celda-presente", - "T" => "celda-tarde", - "F" => "celda-falta", - _ => "" - }; - - var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday; - - + + + + + @foreach (var dia in Model.DiasDelMes) + { + var diaSemana = ((int)dia.DayOfWeek).ToString(); + var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true; + + if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked) { - + continue; } - else + + var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday; + + // Calcular totales para este día + var totalPresente = 0; + var totalTarde = 0; + var totalFalta = 0; + + foreach (var expediente in Model.Expedientes) { - + var key = $"{expediente.Id}_{dia:yyyy-MM-dd}"; + if (Model.Asistencias.ContainsKey(key)) + { + var estado = Model.Asistencias[key]; + switch (estado) + { + case "P": totalPresente++; break; + case "T": totalTarde++; break; + case "F": totalFalta++; break; + } + } } - - @if (estadoActual == "T") - { - - } - else - { - - } - - @if (estadoActual == "F") - { - - } - else - { - - } - - - } - - } - -
+
Nombre
+
Edad
+
+
@dia.Day
+
@nombreDia
+
+
@nombreCompleto
+
Edad: @edad años
+
+ + +
-
@nombreCompleto
-
Edad: @edad años
-
- -
+
TOTALES POR DÍA
+
P/T/F
+
+ + var totalDia = totalPresente + totalTarde + totalFalta; + + +
@totalDia
+
+ @totalPresente + @totalTarde + @totalFalta +
+ + } + + + + +
- +