Nueva mejoras Y estabilidad

This commit is contained in:
2025-12-26 22:27:20 -06:00
parent 203859b22a
commit ac96cb1f23
23 changed files with 1841 additions and 480 deletions

View File

@@ -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<ColumnAttribute>()?.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);
}
)
);
}
}

View File

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

View File

@@ -0,0 +1,7 @@
namespace MicroORM.Exceptions;
public class OrmException : Exception
{
public OrmException(string message, Exception? inner = null)
: base(message, inner) { }
}

View File

@@ -0,0 +1,68 @@
using System.Linq.Expressions;
using System.Reflection;
using Dapper;
namespace MicroORM;
public static class ExpressionToSqlTranslator
{
public static string Translate<T>(
Expression<Func<T, bool>> 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<T>(Expression<Func<T, object>> exp)
{
var body = exp.Body is UnaryExpression u ? u.Operand : exp.Body;
var member = (MemberExpression)body;
return OrmMetadata.GetColumnName((PropertyInfo)member.Member);
}
}

View File

@@ -0,0 +1,20 @@
using System.Data;
using System.Linq.Expressions;
namespace MicroORM.Interfaces;
public interface IRepository<T>
{
Task<int> InsertAsync<T>(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null);
Task<bool> UpdateAsync<T>(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null);
Task<T?> GetByIdAsync(object id);
Task<IEnumerable<T>> GetAllAsync(Expression<Func<T, bool>>? filter = null,
Expression<Func<T, object>>? orderBy = null, bool descending = false);
Task<bool> DeleteAsync(object id);
Task<int> SaveAsync<T>(T entity);
Task<IReadOnlyList<int>> SaveAsyncList<T>(IEnumerable<T> entities);
Task<T?> QuerySingleAsync<T>(string sql, object? parameters = null);
Task<IEnumerable<T>> QueryAsync<T>(string sql, object? parameters = null);
Task<T?> ExecuteScalarAsync<T>(string sql, object? parameters = null);
}

7
MicroORM/MicroORM.cs Normal file
View File

@@ -0,0 +1,7 @@
namespace MicroORM;
public class MicroORM
{
}

15
MicroORM/MicroORM.csproj Normal file
View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Dapper.Contrib" Version="2.0.78" />
<PackageReference Include="Dapper.SqlBuilder" Version="2.1.66" />
<PackageReference Include="Npgsql" Version="10.0.1" />
<PackageReference Include="Npgsql.Json.NET" Version="10.0.1" />
</ItemGroup>
</Project>

63
MicroORM/OrmMetadata.cs Normal file
View File

@@ -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<T>()
{
var table = typeof(T).GetCustomAttribute<TableAttribute>();
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<T>()
{
var key = typeof(T)
.GetProperties()
.FirstOrDefault(p => p.GetCustomAttribute<KeyAttribute>() != null);
if (key == null)
throw new InvalidOperationException("La entidad no tiene [Key]");
return key;
}
public static string GetColumnName(PropertyInfo prop)
{
var columnAttr = prop.GetCustomAttribute<ColumnAttribute>();
return columnAttr?.Name ?? prop.Name;
}
public static bool IsIdentity(PropertyInfo prop)
{
var key = prop.GetCustomAttribute<KeyAttribute>() != null;
var dbGenerated = prop.GetCustomAttribute<DatabaseGeneratedAttribute>();
return key &&
dbGenerated?.DatabaseGeneratedOption == DatabaseGeneratedOption.Identity;
}
public static IEnumerable<PropertyInfo> GetScaffoldProperties<T>()
{
var type = typeof(T);
var key = GetKeyProperty<T>();
return type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p =>
p.CanWrite && // tiene setter
p.GetCustomAttribute<NotMappedAttribute>() == null &&
(
p.GetCustomAttribute<ColumnAttribute>() != null ||
p == key
)
);
}
}

610
MicroORM/Repository.cs Normal file
View File

@@ -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<T>: IRepository<T>
{
private readonly Func<Task<IDbConnection>> _connectionFactory;
private IRepository<T> _repositoryImplementation;
public Repository(Func<Task<IDbConnection>> connectionFactory)
{
_connectionFactory = connectionFactory;
DapperTypeMapRegistrar.RegisterFor(typeof(T));
}
static Repository()
{
DapperTypeMapRegistrar.RegisterFor(typeof(T));
}
public async Task<int> SaveAsync<T>(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<IReadOnlyList<int>> SaveAsyncList<T>(IEnumerable<T> entities)
{
if (entities == null)
throw new ArgumentNullException(nameof(entities));
var list = entities as IList<T> ?? entities.ToList();
if (list.Count == 0)
return Array.Empty<int>();
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
using var tx = conn.BeginTransaction();
try
{
var ids = new List<int>(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<int> InsertAsync<T>(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<T>();
var key = OrmMetadata.GetKeyProperty<T>();
var props = OrmMetadata.GetScaffoldProperties<T>().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<int>(
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<bool> UpdateAsync<T>(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<T>();
var key = OrmMetadata.GetKeyProperty<T>();
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<T>().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<T?> GetByIdAsync(object id)
{
if (id == null)
throw new ArgumentNullException(nameof(id));
try
{
var table = OrmMetadata.GetTableName<T>();
var key = OrmMetadata.GetKeyProperty<T>();
var sql = $@"
SELECT *
FROM {table}
WHERE {OrmMetadata.GetColumnName(key)} = @Id;
";
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
return await conn.QueryFirstOrDefaultAsync<T>(sql, new { Id = id });
}
catch (Exception ex)
{
throw new OrmException(
$"Error SELECT por ID en {typeof(T).Name}",
ex
);
}
}
public async Task<IEnumerable<T>> GetAllAsync(Expression<Func<T, bool>>? filter = null, Expression<Func<T, object>>? orderBy = null, bool descending = false)
{
try
{
var table = OrmMetadata.GetTableName<T>();
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<T>(sql, parameters);
}
catch (Exception ex)
{
throw new OrmException(
$"Error SELECT dinámico en {typeof(T).Name}",
ex
);
}
}
public async Task<bool> DeleteAsync(object id)
{
if (id == null)
throw new ArgumentNullException(nameof(id));
try
{
var table = OrmMetadata.GetTableName<T>();
var key = OrmMetadata.GetKeyProperty<T>();
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<T?> QuerySingleAsync<T>(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<T>(
sql,
parameters
);
}
catch (Exception ex)
{
throw new OrmException(
$"Error en QuerySingle<{typeof(T).Name}>",
ex
);
}
}
public async Task<IEnumerable<T>> QueryAsync<T>(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<T>(sql, parameters);
}
catch (Exception ex)
{
throw new OrmException(
$"Error en Query<{typeof(T).Name}>",
ex
);
}
}
public async Task<T?> ExecuteScalarAsync<T>(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<T>(
sql,
parameters
);
}
catch (Exception ex)
{
throw new OrmException(
$"Error en ExecuteScalar<{typeof(T).Name}>",
ex
);
}
}
private async Task ResolveRelationsAsync<T>(
T entity,
IDbConnection conn,
IDbTransaction tx)
{
var navProps = typeof(T)
.GetProperties()
.Where(p =>
p.PropertyType.IsClass &&
p.PropertyType != typeof(string) &&
p.GetCustomAttribute<NotMappedAttribute>() == 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>(
T entity,
PropertyInfo navProp,
int fkValue)
{
var fkAttr = navProp.GetCustomAttribute<ForeignKeyAttribute>();
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<int> InsertDynamicAsync(
object entity,
IDbConnection conn,
IDbTransaction tx)
{
var method = typeof(Repository<>)
.GetMethod(nameof(InsertAsync))!
.MakeGenericMethod(entity.GetType());
return await (Task<int>)method.Invoke(this, new[] { entity, conn, tx })!;
}
private async Task<int> UpdateDynamicAsync(
object entity,
IDbConnection conn,
IDbTransaction tx)
{
var method = typeof(Repository<>)
.GetMethod(nameof(UpdateAsync))!
.MakeGenericMethod(entity.GetType());
return await (Task<int>)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<KeyAttribute>() != 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<KeyAttribute>() != 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);
}
}

View File

@@ -5,6 +5,8 @@ VisualStudioVersion = 17.14.36327.8 d17.14
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MieSystem", "MieSystem\MieSystem.csproj", "{5D8C00CA-A04E-4414-AC79-195A25059557}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MieSystem", "MieSystem\MieSystem.csproj", "{5D8C00CA-A04E-4414-AC79-195A25059557}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroORM", "MicroORM\MicroORM.csproj", "{43F35A16-C2FC-48D5-B2E9-5432CEDFD451}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{5D8C00CA-A04E-4414-AC79-195A25059557}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -1,41 +1,30 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MieSystem.Data.Interfaces;
using MieSystem.Models; using MieSystem.Models;
using MieSystem.Models.ViewModels; using MieSystem.Models.ViewModels;
using MieSystem.Services;
namespace MieSystem.Controllers namespace MieSystem.Controllers
{ {
public class AsistenciaController : Controller public class AsistenciaController : Controller
{ {
private readonly IExpedienteRepository _expedienteRepository; private readonly ExpedienteService expedienteService;
private readonly IAsistenciaRepository _asistenciaRepository; private readonly AsistenciaService asistenciaService;
public AsistenciaController(ExpedienteService expedienteService, AsistenciaService asistenciaService)
public AsistenciaController(
IExpedienteRepository expedienteRepository,
IAsistenciaRepository asistenciaRepository)
{ {
_expedienteRepository = expedienteRepository; this.expedienteService = expedienteService;
_asistenciaRepository = asistenciaRepository; this.asistenciaService = asistenciaService;
} }
public async Task<IActionResult> Index(int? año, int? mes, string diasSemana = null) public async Task<IActionResult> Index(int? año, int? mes, string diasSemana = null)
{ {
// Valores por defecto: mes actual
var fechaActual = DateTime.Now; var fechaActual = DateTime.Now;
var añoSeleccionado = año ?? fechaActual.Year; var añoSeleccionado = año ?? fechaActual.Year;
var mesSeleccionado = mes ?? fechaActual.Month; var mesSeleccionado = mes ?? fechaActual.Month;
// Obtener todos los niños activos var expedientes = await expedienteService.GetActivosAsync();
var expedientes = await _expedienteRepository.GetActivosAsync();
// Obtener días del mes seleccionado
var diasDelMes = GetDiasDelMes(añoSeleccionado, mesSeleccionado); var diasDelMes = GetDiasDelMes(añoSeleccionado, mesSeleccionado);
// Filtrar por días de semana si se especifica
if (!string.IsNullOrEmpty(diasSemana)) if (!string.IsNullOrEmpty(diasSemana))
{ {
var diasFiltro = diasSemana.Split(',') var diasFiltro = diasSemana.Split(',')
@@ -45,7 +34,7 @@ namespace MieSystem.Controllers
} }
// Obtener asistencias para el mes // Obtener asistencias para el mes
var asistencias = await _asistenciaRepository.GetAsistenciasPorMesAsync( var asistencias = await asistenciaService.GetAsistenciasPorMesAsync(
añoSeleccionado, mesSeleccionado); añoSeleccionado, mesSeleccionado);
// Crear diccionario para acceso rápido // Crear diccionario para acceso rápido
@@ -53,7 +42,7 @@ namespace MieSystem.Controllers
foreach (var asistencia in asistencias) foreach (var asistencia in asistencias)
{ {
var key = $"{asistencia.ExpedienteId}_{asistencia.Fecha:yyyy-MM-dd}"; var key = $"{asistencia.ExpedienteId}_{asistencia.Fecha:yyyy-MM-dd}";
dictAsistencias[key] = asistencia.Estado; dictAsistencias[key] = asistencia.Estado.ToString();
} }
// Crear modelo de vista // Crear modelo de vista
@@ -89,11 +78,11 @@ namespace MieSystem.Controllers
{ {
ExpedienteId = expedienteId, ExpedienteId = expedienteId,
Fecha = fechaDate, Fecha = fechaDate,
Estado = estado, Estado = estado[0],
UsuarioRegistro = User.Identity?.Name ?? "Sistema" 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" }); return Json(new { success = resultado, message = "Asistencia guardada" });
} }
@@ -116,13 +105,13 @@ namespace MieSystem.Controllers
{ {
ExpedienteId = asistenciaDto.ExpedienteId, ExpedienteId = asistenciaDto.ExpedienteId,
Fecha = asistenciaDto.Fecha, Fecha = asistenciaDto.Fecha,
Estado = asistenciaDto.Estado, Estado = asistenciaDto.Estado[0],
UsuarioRegistro = User.Identity?.Name ?? "Sistema" UsuarioRegistro = User.Identity?.Name ?? "Sistema"
}; };
asistenciasModel.Add(asistencia); asistenciasModel.Add(asistencia);
} }
var resultado = await _asistenciaRepository.GuardarAsistenciasMasivasAsync(asistenciasModel); var resultado = asistenciaService.GuardarAsistenciasMasivasAsync(asistenciasModel);
return Json(new return Json(new
{ {
@@ -141,7 +130,7 @@ namespace MieSystem.Controllers
{ {
try try
{ {
var estadisticas = await _asistenciaRepository.GetEstadisticasMesAsync(año, mes); var estadisticas = await asistenciaService.GetEstadisticasMesAsync(año, mes);
return Json(estadisticas); return Json(estadisticas);
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -1,29 +1,27 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using MieSystem.Data.Interfaces;
using MieSystem.Models; using MieSystem.Models;
using MieSystem.Models.ViewModels; using MieSystem.Models.ViewModels;
using MieSystem.Services;
namespace MieSystem.Controllers namespace MieSystem.Controllers
{ {
public class ExpedientesController : Controller public class ExpedientesController : Controller
{ {
private static List<Expediente> _expedientes = new List<Expediente>(); private static List<Expediente> _expedientes = new List<Expediente>();
private static int _idCounter = 1; //private static int _idCounter = 1;
private readonly IWebHostEnvironment _hostingEnvironment; private readonly IWebHostEnvironment _hostingEnvironment;
private readonly ExpedienteService expedienteService;
private readonly IExpedienteRepository _expedienteRepository; public ExpedientesController(ExpedienteService expedienteService, IWebHostEnvironment hostingEnvironment)
public ExpedientesController(IExpedienteRepository expedienteRepository, IWebHostEnvironment hostingEnvironment)
{ {
_hostingEnvironment = hostingEnvironment; _hostingEnvironment = hostingEnvironment;
this.expedienteService = expedienteService;
_expedienteRepository = expedienteRepository;
} }
// GET: Expedientes // GET: Expedientes
public async Task<IActionResult> Index() public async Task<IActionResult> Index()
{ {
var today = DateTime.Today; var lst = await expedienteService.GetAllAsync();
var lst = await _expedienteRepository.GetAllAsync();
_expedientes = [.. lst]; _expedientes = [.. lst];
return View(); return View();
} }
@@ -83,7 +81,7 @@ namespace MieSystem.Controllers
FechaNacimiento = e.FechaNacimiento, FechaNacimiento = e.FechaNacimiento,
Sexo = e.Sexo, Sexo = e.Sexo,
NombreResponsable = e.NombreResponsable, NombreResponsable = e.NombreResponsable,
FotoUrl = e.FotoUrl FotoUrl = ExisteFoto(e.FotoUrl)
}) })
.ToList(); .ToList();
@@ -140,7 +138,6 @@ namespace MieSystem.Controllers
// Crear nuevo expediente // Crear nuevo expediente
var expediente = new Expediente var expediente = new Expediente
{ {
Id = _idCounter++,
Nombre = model.Nombre, Nombre = model.Nombre,
Apellidos = model.Apellidos, Apellidos = model.Apellidos,
FechaNacimiento = model.FechaNacimiento, FechaNacimiento = model.FechaNacimiento,
@@ -160,7 +157,8 @@ namespace MieSystem.Controllers
try try
{ {
await _expedienteRepository.CreateAsync(expediente); //await _expedienteRepository.CreateAsync(expediente);
await expedienteService.SaveAsync(expediente);
return Json(new { success = true, message = "Expediente creado exitosamente" }); return Json(new { success = true, message = "Expediente creado exitosamente" });
} }
catch (Exception ex) catch (Exception ex)
@@ -217,7 +215,8 @@ namespace MieSystem.Controllers
ModelState.Remove("Observaciones"); ModelState.Remove("Observaciones");
if (ModelState.IsValid) 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); //var expediente = _expedientes.FirstOrDefault(e => e.Id == id);
if (expediente == null) if (expediente == null)
{ {
@@ -278,7 +277,8 @@ namespace MieSystem.Controllers
try try
{ {
await _expedienteRepository.UpdateAsync(expediente); //await _expedienteRepository.UpdateAsync(expediente);
await expedienteService.SaveAsync(expediente);
return Json(new { success = true, message = "Expediente actualizado exitosamente" }); return Json(new { success = true, message = "Expediente actualizado exitosamente" });
} }
@@ -292,7 +292,7 @@ namespace MieSystem.Controllers
} }
// DELETE: Expedientes/Delete/5 // DELETE: Expedientes/Delete/5
[HttpDelete] /*[HttpDelete]
public async Task<IActionResult> Delete(int id) public async Task<IActionResult> Delete(int id)
{ {
var expediente = _expedientes.FirstOrDefault(e => e.Id == 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" }); return Json(new { success = true, message = "Expediente eliminado exitosamente" });
} }
*/
// GET: Expedientes/Details/5 (Opcional) // GET: Expedientes/Details/5 (Opcional)
public IActionResult Details(int id) 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 // GET: Expedientes/DeleteImage
[HttpDelete] [HttpDelete]
public IActionResult DeleteImage(string imageUrl) public IActionResult DeleteImage(string imageUrl)

View File

@@ -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<IDbConnection> CreateAsync()
{
IDbConnection conn = new NpgsqlConnection(_connectionString);
return Task.FromResult(conn);
}
}

View File

@@ -12,6 +12,7 @@
<PackageReference Include="Dapper" Version="2.1.66" /> <PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Dapper.Contrib" Version="2.0.78" /> <PackageReference Include="Dapper.Contrib" Version="2.0.78" />
<PackageReference Include="Dapper.SqlBuilder" Version="2.1.66" /> <PackageReference Include="Dapper.SqlBuilder" Version="2.1.66" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
@@ -25,4 +26,8 @@
<Folder Include="wwwroot\uploads\fotos\" /> <Folder Include="wwwroot\uploads\fotos\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MicroORM\MicroORM.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,33 +1,64 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MieSystem.Models namespace MieSystem.Models
{ {
[Table("asistencia", Schema = "public")]
public class Asistencia public class Asistencia
{ {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("id")]
public int Id { get; set; } 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 public string EstadoTexto => Estado switch
{ {
"P" => "Presente", 'P' => "Presente",
"T" => "Tarde", 'T' => "Tarde",
"F" => "Falto", 'F' => "Faltó",
_ => "Desconocido" _ => "Desconocido"
}; };
[NotMapped]
public string ColorEstado => Estado switch public string ColorEstado => Estado switch
{ {
"P" => "success", 'P' => "success",
"T" => "warning", 'T' => "warning",
"F" => "danger", 'F' => "danger",
_ => "secondary" _ => "secondary"
}; };
} }

View File

@@ -1,62 +1,85 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MieSystem.Models namespace MieSystem.Models
{ {
[Table("expedientes")]
public class Expediente public class Expediente
{ {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("id")] [Column("id")]
public int Id { get; set; } public int Id { get; set; }
[Required]
[StringLength(100)]
[Column("nombre")] [Column("nombre")]
public string Nombre { get; set; } public string Nombre { get; set; } = string.Empty;
[Required]
[StringLength(150)]
[Column("apellidos")] [Column("apellidos")]
public string Apellidos { get; set; } public string Apellidos { get; set; } = string.Empty;
[Required]
[Column("fecha_nacimiento")] [Column("fecha_nacimiento")]
public DateTime FechaNacimiento { get; set; } public DateTime FechaNacimiento { get; set; }
[Column("nombre_padre")] [Required]
public string NombrePadre { get; set; } [StringLength(1)]
[Column("nombre_madre")]
public string NombreMadre { get; set; }
[Column("nombre_responsable")]
public string NombreResponsable { get; set; }
[Column("parentesco_responsable")]
public string ParentescoResponsable { get; set; }
[Column("sexo")] [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")] [Column("direccion")]
public string Direccion { get; set; } public string? Direccion { get; set; }
[StringLength(20)]
[Column("telefono")] [Column("telefono")]
public string Telefono { get; set; } public string? Telefono { get; set; }
[Column("observaciones")] [Column("observaciones")]
public string Observaciones { get; set; } public string? Observaciones { get; set; }
[StringLength(500)]
[Column("foto_url")] [Column("foto_url")]
public string FotoUrl { get; set; } public string? FotoUrl { get; set; }
[Required]
[Column("fecha_creacion")] [Column("fecha_creacion")]
public DateTime FechaCreacion { get; set; } public DateTime FechaCreacion { get; set; }
[Required]
[Column("fecha_actualizacion")] [Column("fecha_actualizacion")]
public DateTime FechaActualizacion { get; set; } public DateTime FechaActualizacion { get; set; }
[Required]
[Column("activo")] [Column("activo")]
public bool Activo { get; set; } public bool Activo { get; set; }
// ============================
// 🔹 Propiedades calculadas
// ============================
[NotMapped]
// Propiedades calculadas (solo lectura)
public string NombreCompleto => $"{Nombre} {Apellidos}".Trim(); public string NombreCompleto => $"{Nombre} {Apellidos}".Trim();
[NotMapped]
public int Edad public int Edad
{ {
get get
@@ -64,17 +87,14 @@ namespace MieSystem.Models
var today = DateTime.Today; var today = DateTime.Today;
var age = today.Year - FechaNacimiento.Year; 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)) if (FechaNacimiento.Date > today.AddYears(-age))
{
age--; age--;
}
return age; return age;
} }
} }
// Otra propiedad útil para mostrar [NotMapped]
public string EdadConMeses public string EdadConMeses
{ {
get get
@@ -84,9 +104,7 @@ namespace MieSystem.Models
var months = today.Month - FechaNacimiento.Month; var months = today.Month - FechaNacimiento.Month;
if (today.Day < FechaNacimiento.Day) if (today.Day < FechaNacimiento.Day)
{
months--; months--;
}
if (months < 0) if (months < 0)
{ {
@@ -98,10 +116,10 @@ namespace MieSystem.Models
} }
} }
// Para mostrar en selectores [NotMapped]
public string NombreConEdad => $"{NombreCompleto} ({Edad} años)"; public string NombreConEdad => $"{NombreCompleto} ({Edad} años)";
// Para mostrar en listas [NotMapped]
public string InformacionBasica => $"{NombreCompleto} | {Edad} años | {Sexo}"; public string InformacionBasica => $"{NombreCompleto} | {Edad} años | {Sexo}";
} }
} }

View File

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

View File

@@ -1,5 +1,10 @@
using System.Data;
using MicroORM;
using MicroORM.Interfaces;
using MieSystem.Data;
using MieSystem.Data.Interfaces; using MieSystem.Data.Interfaces;
using MieSystem.Data.Repositories; using MieSystem.Data.Repositories;
using MieSystem.Services;
namespace MieSystem namespace MieSystem
{ {
@@ -10,11 +15,27 @@ namespace MieSystem
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews().AddRazorRuntimeCompilation();
builder.Services.AddSingleton<IDatabaseConnectionFactory, DatabaseConnectionFactory>(); builder.Services.AddSingleton<IDatabaseConnectionFactory, DatabaseConnectionFactory>();
builder.Services.AddScoped<IExpedienteRepository, ExpedienteRepository>(); /* Esto es lo nuevo */
builder.Services.AddSingleton<NpgsqlConnectionFactory>();
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
// Delegate que consume el Repository<T>
builder.Services.AddScoped<Func<Task<IDbConnection>>>(sp =>
{
var factory = sp.GetRequiredService<NpgsqlConnectionFactory>();
return factory.CreateAsync;
});
builder.Services.AddScoped<ExpedienteService>();
builder.Services.AddScoped<AsistenciaService>();
/***********/
//builder.Services.AddScoped<IExpedienteRepository, ExpedienteRepository>();
builder.Services.AddScoped<IAsistenciaRepository, AsistenciaRepository>(); builder.Services.AddScoped<IAsistenciaRepository, AsistenciaRepository>();
var app = builder.Build(); var app = builder.Build();
app.UseStaticFiles(); app.UseStaticFiles();

View File

@@ -7,7 +7,7 @@
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
"applicationUrl": "http://localhost:5037" "applicationUrl": "http://localhost:8090"
}, },
"https": { "https": {
"commandName": "Project", "commandName": "Project",
@@ -16,7 +16,7 @@
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
"applicationUrl": "https://localhost:7037;http://localhost:5037" "applicationUrl": "https://localhost:7037;http://localhost:8090"
}, },
"Container (Dockerfile)": { "Container (Dockerfile)": {
"commandName": "Docker", "commandName": "Docker",

View File

@@ -0,0 +1,93 @@
using System.Diagnostics.Tracing;
using MicroORM.Interfaces;
using MieSystem.Models;
namespace MieSystem.Services;
public class AsistenciaService
{
private readonly IRepository<Asistencia> _repo;
public AsistenciaService(IRepository<Asistencia> expe)
{
_repo = expe;
}
public Task<int> SaveAsync(Asistencia expediente)
{
return _repo.SaveAsync(expediente);
}
public Task<Asistencia?> GetByIdAsync(object id)
{
return _repo.GetByIdAsync(id);
}
public Task<IEnumerable<Asistencia>> GetAllAsync()
{
return _repo.GetAllAsync();
}
public Task<IEnumerable<Asistencia>> 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<Asistencia>(sql, new {Anio = añoSeleccionado, Mes = mesSeleccionado});
}
public Task<EstadisticasMes?> 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<EstadisticasMes?>(sql, new { Anio = año, Mes = mes });
}
public bool GuardarAsistenciasMasivasAsync(List<Asistencia> lst)
{
try
{
_repo.SaveAsyncList(lst);
return true;
}
catch (Exception e)
{
Console.WriteLine(e);
return true;
}
}
}

View File

@@ -0,0 +1,35 @@
using MicroORM.Interfaces;
using MieSystem.Models;
namespace MieSystem.Services;
public class ExpedienteService
{
private readonly IRepository<Expediente> _repo;
public ExpedienteService(IRepository<Expediente> expe)
{
_repo = expe;
}
public Task<int> SaveAsync(Expediente expediente)
{
return _repo.SaveAsync(expediente);
}
public Task<Expediente?> GetByIdAsync(object id)
{
return _repo.GetByIdAsync(id);
}
public Task<IEnumerable<Expediente>> GetAllAsync()
{
return _repo.GetAllAsync();
}
public Task<IEnumerable<Expediente>> GetActivosAsync()
{
return _repo.GetAllAsync(e=> e.Activo == true);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,147 +1,139 @@
@using MieSystem.Models; @using MieSystem.Models
@using MieSystem.Models.ViewModels @using MieSystem.Models.ViewModels
@model ExpedienteViewModel @model ExpedienteViewModel
<form id="@(ViewData["IsEdit"] != null && (bool)ViewData["IsEdit"] ? "editForm" : "createForm")" @{
method="post" bool isEdit = ViewData["IsEdit"] != null && (bool)ViewData["IsEdit"];
enctype="multipart/form-data" }
data-id="@(Model?.Id ?? 0)">
@if (ViewData["IsEdit"] != null && (bool)ViewData["IsEdit"]) <form id="@(isEdit ? "editForm" : "createForm")"
method="post"
enctype="multipart/form-data">
@* SOLO EN EDIT SE ENVÍA EL ID *@
@if (isEdit)
{ {
<input type="hidden" asp-for="Id" /> <input type="hidden" asp-for="Id" />
} }
<!-- Campo oculto para almacenar la URL de la imagen subida --> @* Campo oculto para la URL de la imagen *@
<input type="hidden" asp-for="FotoUrl" /> <input type="hidden" asp-for="FotoUrl" value="@(Model?.FotoUrl ?? "/images/default-avatar.png")" />
<div class="row"> <div class="row">
<!-- Columna izquierda - Datos personales --> <!-- ================= COLUMNA IZQUIERDA ================= -->
<div class="col-md-6"> <div class="col-md-6">
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h6 class="mb-0">Datos Personales</h6> <h6 class="mb-0">Datos Personales</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<!-- Foto -->
<!-- FOTO -->
<div class="mb-3 text-center"> <div class="mb-3 text-center">
<div class="mb-2"> <img id="@(isEdit ? "fotoPreviewEdit" : "fotoPreview")"
<img id="@(ViewData["IsEdit"] != null && (bool)ViewData["IsEdit"] ? "fotoPreviewEdit" : "fotoPreview")" src="@(Model?.FotoUrl ?? "/images/default-avatar.png")"
src="@(Model?.FotoUrl ?? "/images/default-avatar.png")" class="rounded-circle border mb-2"
alt="Foto del niño" style="width:120px;height:120px;object-fit:cover;" />
class="rounded-circle border"
style="width: 120px; height: 120px; object-fit: cover;"> <div class="d-flex justify-content-center gap-2">
</div>
<div class="d-flex justify-content-center align-items-center gap-2">
<label class="btn btn-sm btn-outline-primary"> <label class="btn btn-sm btn-outline-primary">
<i class="bi bi-camera me-1"></i> Seleccionar Foto <i class="bi bi-camera me-1"></i> Seleccionar Foto
<input type="file" <input type="file"
asp-for="Foto" asp-for="Foto"
id="@(ViewData["IsEdit"] != null && (bool)ViewData["IsEdit"] ? "FotoEdit" : "Foto")" id="@(isEdit ? "FotoEdit" : "Foto")"
class="d-none" class="d-none"
accept="image/*"> accept="image/*" />
</label> </label>
<!-- Botón para eliminar imagen -->
<button type="button" <button type="button"
id="@(isEdit ? "deleteFotoEdit" : "deleteFoto")"
class="btn btn-sm btn-outline-danger" class="btn btn-sm btn-outline-danger"
id="@(ViewData["IsEdit"] != null && (bool)ViewData["IsEdit"] ? "deleteFotoEdit" : "deleteFoto")" style="display:@(Model?.FotoUrl != null && Model.FotoUrl != "/images/default-avatar.png" ? "block" : "none")">
style="display: @((Model?.FotoUrl != null && Model.FotoUrl != "/images/default-avatar.png") ? "block" : "none");">
<i class="bi bi-trash me-1"></i> Eliminar <i class="bi bi-trash me-1"></i> Eliminar
</button> </button>
</div> </div>
<div class="mt-2">
<small class="text-muted">Formatos permitidos: JPG, PNG, GIF, BMP. Tamaño máximo: 5MB</small>
</div>
<span asp-validation-for="Foto" class="text-danger"></span> <span asp-validation-for="Foto" class="text-danger"></span>
</div> </div>
<!-- Nombre --> <!-- NOMBRE -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="Nombre" class="form-label">Nombre *</label> <label asp-for="Nombre" class="form-label">Nombre *</label>
<input asp-for="Nombre" class="form-control" placeholder="Ingrese el nombre"> <input asp-for="Nombre" class="form-control" />
<span asp-validation-for="Nombre" class="text-danger"></span> <span asp-validation-for="Nombre" class="text-danger"></span>
</div> </div>
<!-- Apellidos --> <!-- APELLIDOS -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="Apellidos" class="form-label">Apellidos *</label> <label asp-for="Apellidos" class="form-label">Apellidos *</label>
<input asp-for="Apellidos" class="form-control" placeholder="Ingrese los apellidos"> <input asp-for="Apellidos" class="form-control" />
<span asp-validation-for="Apellidos" class="text-danger"></span> <span asp-validation-for="Apellidos" class="text-danger"></span>
</div> </div>
<!-- Fecha de Nacimiento --> <!-- FECHA NACIMIENTO -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="FechaNacimiento" class="form-label">Fecha de Nacimiento *</label> <label asp-for="FechaNacimiento" class="form-label">Fecha de Nacimiento *</label>
<input asp-for="FechaNacimiento" type="date" class="form-control"> <input asp-for="FechaNacimiento" type="date" class="form-control" />
<span asp-validation-for="FechaNacimiento" class="text-danger"></span> <span asp-validation-for="FechaNacimiento" class="text-danger"></span>
</div> </div>
<!-- Sexo --> <!-- SEXO -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="Sexo" class="form-label">Sexo *</label> <label class="form-label">Sexo *</label><br />
<div> <div class="form-check form-check-inline">
<div class="form-check form-check-inline"> <input class="form-check-input" type="radio" asp-for="Sexo" value="M" />
<input class="form-check-input" type="radio" asp-for="Sexo" value="M" id="sexoM"> <label class="form-check-label">Masculino</label>
<label class="form-check-label" for="sexoM">Masculino</label> </div>
</div> <div class="form-check form-check-inline">
<div class="form-check form-check-inline"> <input class="form-check-input" type="radio" asp-for="Sexo" value="F" />
<input class="form-check-input" type="radio" asp-for="Sexo" value="F" id="sexoF"> <label class="form-check-label">Femenino</label>
<label class="form-check-label" for="sexoF">Femenino</label>
</div>
</div> </div>
<span asp-validation-for="Sexo" class="text-danger"></span> <span asp-validation-for="Sexo" class="text-danger"></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Columna derecha - Datos familiares y dirección --> <!-- ================= COLUMNA DERECHA ================= -->
<div class="col-md-6"> <div class="col-md-6">
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header bg-light"> <div class="card-header bg-light">
<h6 class="mb-0">Datos Familiares</h6> <h6 class="mb-0">Datos Familiares</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<!-- Nombre del Padre -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="NombrePadre" class="form-label">Nombre del Padre</label> <label asp-for="NombrePadre" class="form-label">Nombre del Padre</label>
<input asp-for="NombrePadre" class="form-control" placeholder="Ingrese nombre del padre"> <input asp-for="NombrePadre" class="form-control" />
<span asp-validation-for="NombrePadre" class="text-danger"></span>
</div> </div>
<!-- Nombre de la Madre -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="NombreMadre" class="form-label">Nombre de la Madre</label> <label asp-for="NombreMadre" class="form-label">Nombre de la Madre</label>
<input asp-for="NombreMadre" class="form-control" placeholder="Ingrese nombre de la madre"> <input asp-for="NombreMadre" class="form-control" />
<span asp-validation-for="NombreMadre" class="text-danger"></span>
</div> </div>
<!-- Nombre del Responsable -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="NombreResponsable" class="form-label">Nombre del Responsable *</label> <label asp-for="NombreResponsable" class="form-label">Nombre del Responsable *</label>
<input asp-for="NombreResponsable" class="form-control" placeholder="Ingrese nombre del responsable"> <input asp-for="NombreResponsable" class="form-control" />
<span asp-validation-for="NombreResponsable" class="text-danger"></span> <span asp-validation-for="NombreResponsable" class="text-danger"></span>
</div> </div>
<!-- Parentesco del Responsable -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="ParentescoResponsable" class="form-label">Parentesco del Responsable</label> <label asp-for="ParentescoResponsable" class="form-label">Parentesco</label>
<select asp-for="ParentescoResponsable" class="form-select"> <select asp-for="ParentescoResponsable" class="form-select">
<option value="">Seleccione parentesco</option> <option value="">Seleccione</option>
<option value="Padre">Padre</option> <option>Padre</option>
<option value="Madre">Madre</option> <option>Madre</option>
<option value="Abuelo">Abuelo</option> <option>Abuelo</option>
<option value="Abuela">Abuela</option> <option>Abuela</option>
<option value="Tío">Tío</option> <option>Tío</option>
<option value="Tía">Tía</option> <option>Tía</option>
<option value="Hermano">Hermano</option> <option>Otro</option>
<option value="Hermana">Hermana</option>
<option value="Otro">Otro</option>
</select> </select>
<span asp-validation-for="ParentescoResponsable" class="text-danger"></span>
</div> </div>
</div> </div>
</div> </div>
@@ -150,26 +142,23 @@
<h6 class="mb-0">Dirección y Contacto</h6> <h6 class="mb-0">Dirección y Contacto</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<!-- Dirección -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="Direccion" class="form-label">Dirección *</label> <label asp-for="Direccion" class="form-label">Dirección *</label>
<textarea asp-for="Direccion" class="form-control" rows="3" placeholder="Ingrese dirección completa"></textarea> <textarea asp-for="Direccion" class="form-control"></textarea>
<span asp-validation-for="Direccion" class="text-danger"></span> <span asp-validation-for="Direccion" class="text-danger"></span>
</div> </div>
<!-- Teléfono -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="Telefono" class="form-label">Teléfono de Contacto</label> <label asp-for="Telefono" class="form-label">Teléfono</label>
<input asp-for="Telefono" class="form-control" placeholder="Ingrese número telefónico"> <input asp-for="Telefono" class="form-control" />
<span asp-validation-for="Telefono" class="text-danger"></span>
</div> </div>
<!-- Observaciones -->
<div class="mb-3"> <div class="mb-3">
<label asp-for="Observaciones" class="form-label">Observaciones</label> <label asp-for="Observaciones" class="form-label">Observaciones</label>
<textarea asp-for="Observaciones" class="form-control" rows="2" placeholder="Observaciones adicionales"></textarea> <textarea asp-for="Observaciones" class="form-control"></textarea>
<span asp-validation-for="Observaciones" class="text-danger"></span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>