Compare commits
2 Commits
3457721238
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ac96cb1f23 | |||
| 203859b22a |
46
MicroORM/DapperTypeMapRegistrar.cs
Normal file
46
MicroORM/DapperTypeMapRegistrar.cs
Normal 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);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
MicroORM/DbConnectionExtensions.cs
Normal file
18
MicroORM/DbConnectionExtensions.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
MicroORM/Exceptions/OrmException.cs
Normal file
7
MicroORM/Exceptions/OrmException.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MicroORM.Exceptions;
|
||||||
|
|
||||||
|
public class OrmException : Exception
|
||||||
|
{
|
||||||
|
public OrmException(string message, Exception? inner = null)
|
||||||
|
: base(message, inner) { }
|
||||||
|
}
|
||||||
68
MicroORM/ExpressionToSqlTranslator.cs
Normal file
68
MicroORM/ExpressionToSqlTranslator.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
MicroORM/Interfaces/IRepository.cs
Normal file
20
MicroORM/Interfaces/IRepository.cs
Normal 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
7
MicroORM/MicroORM.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace MicroORM;
|
||||||
|
|
||||||
|
public class MicroORM
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
15
MicroORM/MicroORM.csproj
Normal file
15
MicroORM/MicroORM.csproj
Normal 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
63
MicroORM/OrmMetadata.cs
Normal 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
610
MicroORM/Repository.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,43 +1,28 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using MieSystem.Models;
|
using MieSystem.Models;
|
||||||
using MieSystem.Data.Interfaces;
|
|
||||||
using AspNetCoreGeneratedDocument;
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Datos de prueba iniciales
|
|
||||||
/*if (_expedientes.Count == 0)
|
|
||||||
{
|
|
||||||
_ = InitializeTestData();
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitializeTestData()
|
|
||||||
{
|
|
||||||
var today = DateTime.Today;
|
|
||||||
var lst = await _expedienteRepository.GetAllAsync();
|
|
||||||
_expedientes = [.. lst];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: Expedientes
|
// GET: Expedientes
|
||||||
public IActionResult Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
|
var lst = await expedienteService.GetAllAsync();
|
||||||
|
_expedientes = [.. lst];
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +30,6 @@ namespace MieSystem.Controllers
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult GetDashboardStats()
|
public IActionResult GetDashboardStats()
|
||||||
{
|
{
|
||||||
_ = InitializeTestData();
|
|
||||||
var totalNinos = _expedientes.Count;
|
var totalNinos = _expedientes.Count;
|
||||||
var mesActual = DateTime.Now.Month;
|
var mesActual = DateTime.Now.Month;
|
||||||
|
|
||||||
@@ -78,9 +62,6 @@ namespace MieSystem.Controllers
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult GetExpedientes(int page = 1, int pageSize = 10)
|
public IActionResult GetExpedientes(int page = 1, int pageSize = 10)
|
||||||
{
|
{
|
||||||
if (_expedientes.Count == 0)
|
|
||||||
_ = InitializeTestData();
|
|
||||||
|
|
||||||
var query = _expedientes
|
var query = _expedientes
|
||||||
.Where(e => e.Activo)
|
.Where(e => e.Activo)
|
||||||
.OrderBy(e => e.Apellidos)
|
.OrderBy(e => e.Apellidos)
|
||||||
@@ -100,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();
|
||||||
|
|
||||||
@@ -122,7 +103,6 @@ namespace MieSystem.Controllers
|
|||||||
|
|
||||||
// POST: Expedientes/Create
|
// POST: Expedientes/Create
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
// [ValidateAntiForgeryToken]
|
|
||||||
public async Task<IActionResult> Create(ExpedienteViewModel model)
|
public async Task<IActionResult> Create(ExpedienteViewModel model)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -158,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,
|
||||||
@@ -178,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)
|
||||||
@@ -235,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)
|
||||||
{
|
{
|
||||||
@@ -296,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" });
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -310,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);
|
||||||
@@ -336,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)
|
||||||
{
|
{
|
||||||
@@ -428,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)
|
||||||
|
|||||||
21
MieSystem/Data/NpgsqlConnectionFactory.cs
Normal file
21
MieSystem/Data/NpgsqlConnectionFactory.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
MieSystem/Models/Persona.cs
Normal file
43
MieSystem/Models/Persona.cs
Normal 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; }
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
93
MieSystem/Services/AsistenciaService.cs
Normal file
93
MieSystem/Services/AsistenciaService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
MieSystem/Services/ExpedienteService.cs
Normal file
35
MieSystem/Services/ExpedienteService.cs
Normal 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
@@ -530,7 +530,7 @@
|
|||||||
<button onclick="window.print()" class="btn btn-primary">
|
<button onclick="window.print()" class="btn btn-primary">
|
||||||
<i class="bi bi-printer"></i> Imprimir Documento
|
<i class="bi bi-printer"></i> Imprimir Documento
|
||||||
</button>
|
</button>
|
||||||
<button onclick="downloadPDF()" class="btn btn-info">
|
<button onclick="downloadPDF()" style="visibility:hidden" class="btn btn-info">
|
||||||
<i class="bi bi-download"></i> Guardar como PDF
|
<i class="bi bi-download"></i> Guardar como PDF
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -323,10 +323,8 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
renderExpedientesTable(filtered);
|
renderExpedientesTable(filtered);
|
||||||
$('#pagination').empty(); // Limpiar paginación en búsqueda
|
$('#pagination').empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardar nuevo expediente
|
|
||||||
function saveExpediente() {
|
function saveExpediente() {
|
||||||
const formData = new FormData($('#createForm')[0]);
|
const formData = new FormData($('#createForm')[0]);
|
||||||
|
|
||||||
@@ -337,7 +335,6 @@
|
|||||||
processData: false,
|
processData: false,
|
||||||
contentType: false,
|
contentType: false,
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
// Mostrar siempre el mensaje que viene en response.message
|
|
||||||
const message = response.message ||
|
const message = response.message ||
|
||||||
(response.success ? 'Operación completada' : 'Error en la operación');
|
(response.success ? 'Operación completada' : 'Error en la operación');
|
||||||
|
|
||||||
@@ -351,7 +348,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(xhr) {
|
error: function(xhr) {
|
||||||
// Extraer el message del JSON si existe
|
|
||||||
let message = 'Error en la solicitud';
|
let message = 'Error en la solicitud';
|
||||||
try {
|
try {
|
||||||
const jsonResponse = JSON.parse(xhr.responseText);
|
const jsonResponse = JSON.parse(xhr.responseText);
|
||||||
@@ -359,7 +355,6 @@
|
|||||||
message = jsonResponse.message;
|
message = jsonResponse.message;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Si no es JSON válido, usar el texto de respuesta
|
|
||||||
if (xhr.responseText) {
|
if (xhr.responseText) {
|
||||||
message = xhr.responseText;
|
message = xhr.responseText;
|
||||||
}
|
}
|
||||||
@@ -378,7 +373,6 @@
|
|||||||
$('#editModalBody').html(data);
|
$('#editModalBody').html(data);
|
||||||
$('#editModal').modal('show');
|
$('#editModal').modal('show');
|
||||||
|
|
||||||
// Mostrar/ocultar botón de eliminar imagen según si hay imagen personalizada
|
|
||||||
const fotoUrl = $('#editForm input[name="FotoUrl"]').val();
|
const fotoUrl = $('#editForm input[name="FotoUrl"]').val();
|
||||||
const deleteBtn = $('#deleteFotoEdit');
|
const deleteBtn = $('#deleteFotoEdit');
|
||||||
if (fotoUrl && fotoUrl !== '/images/default-avatar.png') {
|
if (fotoUrl && fotoUrl !== '/images/default-avatar.png') {
|
||||||
@@ -441,8 +435,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== FUNCIONES PARA MANEJO DE IMÁGENES ==========
|
|
||||||
|
|
||||||
// Mostrar vista previa de imagen
|
// Mostrar vista previa de imagen
|
||||||
function previewImage(input, previewId) {
|
function previewImage(input, previewId) {
|
||||||
const preview = document.getElementById(previewId);
|
const preview = document.getElementById(previewId);
|
||||||
@@ -519,16 +511,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
showAlert('error', message);
|
showAlert('error', message);
|
||||||
// Restaurar imagen predeterminada
|
|
||||||
preview.src = '/images/default-avatar.png';
|
preview.src = '/images/default-avatar.png';
|
||||||
// Ocultar botón de eliminar
|
|
||||||
const deleteBtnId = isEdit ? 'deleteFotoEdit' : 'deleteFoto';
|
const deleteBtnId = isEdit ? 'deleteFotoEdit' : 'deleteFoto';
|
||||||
$(`#${deleteBtnId}`).hide();
|
$(`#${deleteBtnId}`).hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar imagen
|
|
||||||
function deleteImage(isEdit = false) {
|
function deleteImage(isEdit = false) {
|
||||||
const formId = isEdit ? 'editForm' : 'createForm';
|
const formId = isEdit ? 'editForm' : 'createForm';
|
||||||
const hiddenInput = $(`#${formId} input[name="FotoUrl"]`);
|
const hiddenInput = $(`#${formId} input[name="FotoUrl"]`);
|
||||||
@@ -544,7 +533,6 @@
|
|||||||
data: { imageUrl: imageUrl },
|
data: { imageUrl: imageUrl },
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// Restaurar imagen predeterminada
|
|
||||||
$(`#${previewId}`).attr('src', '/images/default-avatar.png');
|
$(`#${previewId}`).attr('src', '/images/default-avatar.png');
|
||||||
hiddenInput.val('/images/default-avatar.png');
|
hiddenInput.val('/images/default-avatar.png');
|
||||||
$(`#${deleteBtnId}`).hide();
|
$(`#${deleteBtnId}`).hide();
|
||||||
@@ -561,9 +549,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== FUNCIONES UTILITARIAS ==========
|
|
||||||
|
|
||||||
// Calcular edad
|
|
||||||
function calculateAge(birthDate) {
|
function calculateAge(birthDate) {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const birth = new Date(birthDate);
|
const birth = new Date(birthDate);
|
||||||
@@ -577,7 +562,6 @@
|
|||||||
return age;
|
return age;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formatear fecha
|
|
||||||
function formatDate(dateString) {
|
function formatDate(dateString) {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.toLocaleDateString('es-ES', {
|
return date.toLocaleDateString('es-ES', {
|
||||||
@@ -587,9 +571,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mostrar alerta
|
|
||||||
function showAlert(type, message) {
|
function showAlert(type, message) {
|
||||||
// Crear elemento de alerta
|
|
||||||
const alertDiv = document.createElement('div');
|
const alertDiv = document.createElement('div');
|
||||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||||
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;';
|
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1050; min-width: 300px;';
|
||||||
@@ -598,10 +580,8 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Agregar al body
|
|
||||||
document.body.appendChild(alertDiv);
|
document.body.appendChild(alertDiv);
|
||||||
|
|
||||||
// Auto-eliminar después de 5 segundos
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (alertDiv.parentNode) {
|
if (alertDiv.parentNode) {
|
||||||
alertDiv.remove();
|
alertDiv.remove();
|
||||||
|
|||||||
@@ -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")"
|
||||||
alt="Foto del niño"
|
class="rounded-circle border mb-2"
|
||||||
class="rounded-circle border"
|
style="width:120px;height:120px;object-fit:cover;" />
|
||||||
style="width: 120px; height: 120px; object-fit: cover;">
|
|
||||||
</div>
|
<div class="d-flex justify-content-center gap-2">
|
||||||
<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" id="sexoM">
|
<input class="form-check-input" type="radio" asp-for="Sexo" value="M" />
|
||||||
<label class="form-check-label" for="sexoM">Masculino</label>
|
<label class="form-check-label">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" id="sexoF">
|
<input class="form-check-input" type="radio" asp-for="Sexo" value="F" />
|
||||||
<label class="form-check-label" for="sexoF">Femenino</label>
|
<label class="form-check-label">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>
|
||||||
|
|||||||
@@ -257,32 +257,32 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- Separador y sección de administración -->
|
<!-- Separador y sección de administración -->
|
||||||
<li class="mt-4">
|
<li class="mt-4" style="visibility:hidden">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h6 class="text-uppercase text-muted small">Administración</h6>
|
<h6 class="text-uppercase text-muted small" >Administración</h6>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li style="visibility:hidden">
|
||||||
<a href="#" class="nav-link">
|
<a href="#" class="nav-link">
|
||||||
<i class="bi bi-people"></i> Niños
|
<i class="bi bi-people"></i> Niños
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li style="visibility:hidden">
|
||||||
<a href="#" class="nav-link">
|
<a href="#" class="nav-link">
|
||||||
<i class="bi bi-person-badge"></i> Maestros
|
<i class="bi bi-person-badge"></i> Maestros
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li style ="visibility:hidden">
|
||||||
<a href="#" class="nav-link">
|
<a href="#" class="nav-link">
|
||||||
<i class="bi bi-person-workspace"></i> Personal
|
<i class="bi bi-person-workspace" ></i> Personal
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li style="visibility:hidden">
|
||||||
<a href="#" class="nav-link">
|
<a href="#" class="nav-link">
|
||||||
<i class="bi bi-bar-chart"></i> Reportes
|
<i class="bi bi-bar-chart"></i> Reportes
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li style="visibility:hidden">
|
||||||
<a asp-area="" asp-controller="Home" asp-action="Privacy" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Home" && ViewContext.RouteData.Values["Action"]?.ToString() == "Privacy" ? "active" : "")">
|
<a asp-area="" asp-controller="Home" asp-action="Privacy" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Home" && ViewContext.RouteData.Values["Action"]?.ToString() == "Privacy" ? "active" : "")">
|
||||||
<i class="bi bi-shield-check"></i> Privacidad
|
<i class="bi bi-shield-check"></i> Privacidad
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user