segundo commit todos
This commit is contained in:
8
foundation_system/.dockerignore
Normal file
8
foundation_system/.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
**/.dockerignore
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/bin
|
||||
**/obj
|
||||
**/logs
|
||||
156
foundation_system/Controllers/AccountController.cs
Normal file
156
foundation_system/Controllers/AccountController.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using foundation_system.Models.ViewModels;
|
||||
using foundation_system.Services;
|
||||
|
||||
namespace foundation_system.Controllers;
|
||||
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ILogger<AccountController> _logger;
|
||||
|
||||
public AccountController(IAuthService authService, ILogger<AccountController> logger)
|
||||
{
|
||||
_authService = authService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// GET: /Account/Login
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Login(string? returnUrl = null)
|
||||
{
|
||||
if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: /Account/Login
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Login(LoginViewModel model, string? returnUrl = null)
|
||||
{
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var usuario = await _authService.ValidateUserAsync(model.NombreUsuario, model.Contrasena);
|
||||
|
||||
if (usuario == null)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Usuario o contraseña incorrectos");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// Get user roles
|
||||
var roles = await _authService.GetUserRolesAsync(usuario.Id);
|
||||
|
||||
// Create claims
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, usuario.Id.ToString()),
|
||||
new(ClaimTypes.Name, usuario.NombreUsuario),
|
||||
new(ClaimTypes.Email, usuario.Email),
|
||||
new("FullName", usuario.Persona?.NombreCompleto ?? usuario.NombreUsuario)
|
||||
};
|
||||
|
||||
// Add role claims
|
||||
foreach (var role in roles)
|
||||
{
|
||||
claims.Add(new Claim(ClaimTypes.Role, role));
|
||||
}
|
||||
|
||||
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
var authProperties = new AuthenticationProperties
|
||||
{
|
||||
IsPersistent = model.RecordarMe,
|
||||
ExpiresUtc = model.RecordarMe
|
||||
? DateTimeOffset.UtcNow.AddDays(30)
|
||||
: DateTimeOffset.UtcNow.AddHours(8)
|
||||
};
|
||||
|
||||
await HttpContext.SignInAsync(
|
||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(claimsIdentity),
|
||||
authProperties);
|
||||
|
||||
// Update last login
|
||||
await _authService.UpdateLastLoginAsync(usuario.Id);
|
||||
|
||||
_logger.LogInformation("User {Username} logged in", usuario.NombreUsuario);
|
||||
|
||||
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
|
||||
{
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
// GET: /Account/Register
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Register()
|
||||
{
|
||||
if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: /Account/Register
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Register(RegisterViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var (success, message, _) = await _authService.RegisterUserAsync(model);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, message);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
TempData["SuccessMessage"] = "¡Registro exitoso! Ahora puedes iniciar sesión.";
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
|
||||
// POST: /Account/Logout
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
_logger.LogInformation("User logged out");
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
// GET: /Account/AccessDenied
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult AccessDenied()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
125
foundation_system/Controllers/AntecedentesController.cs
Normal file
125
foundation_system/Controllers/AntecedentesController.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using foundation_system.Data;
|
||||
using foundation_system.Models;
|
||||
using foundation_system.Models.ViewModels;
|
||||
using foundation_system.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foundation_system.Controllers;
|
||||
|
||||
public class AntecedentesController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IAntecedentesService _antecedentesService;
|
||||
|
||||
public AntecedentesController(ApplicationDbContext context, IAntecedentesService antecedentesService)
|
||||
{
|
||||
_context = context;
|
||||
_antecedentesService = antecedentesService;
|
||||
}
|
||||
|
||||
// GET: Antecedentes
|
||||
public async Task<IActionResult> Index(string search = "", int page = 1, int pageSize = 10)
|
||||
{
|
||||
var query = _context.Ninos
|
||||
.Include(n => n.Persona)
|
||||
.Where(n => n.Activo);
|
||||
|
||||
List<long> searchIds = new();
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
searchIds = await PostgresQueryExecutor.ExecuteQueryAsync(
|
||||
_context,
|
||||
"SELECT id FROM buscar_personas_v2(@term, 'NINO')",
|
||||
reader => reader.GetInt64(0),
|
||||
p => p.AddWithValue("term", search)
|
||||
);
|
||||
|
||||
query = query.Where(n => searchIds.Contains(n.Id));
|
||||
}
|
||||
|
||||
var totalItems = await query.CountAsync();
|
||||
var totalPages = (int)Math.Ceiling(totalItems / (double)pageSize);
|
||||
page = Math.Max(1, Math.Min(page, totalPages > 0 ? totalPages : 1));
|
||||
|
||||
var ninos = await query
|
||||
.OrderByDescending(n => n.CreadoEn)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
// Preserve the order from the fuzzy search procedure
|
||||
ninos = ninos.OrderBy(n => searchIds.IndexOf(n.Id)).ToList();
|
||||
}
|
||||
|
||||
ViewBag.Search = search;
|
||||
ViewBag.CurrentPage = page;
|
||||
ViewBag.TotalPages = totalPages;
|
||||
|
||||
return View(ninos);
|
||||
}
|
||||
|
||||
// GET: Antecedentes/Manage/5
|
||||
public async Task<IActionResult> Manage(long id)
|
||||
{
|
||||
var nino = await _context.Ninos
|
||||
.Include(n => n.Persona)
|
||||
.FirstOrDefaultAsync(n => n.Id == id);
|
||||
|
||||
if (nino == null) return NotFound();
|
||||
|
||||
var historial = await _antecedentesService.GetHistorialByNinoIdAsync(id);
|
||||
|
||||
var viewModel = new AntecedentesViewModel
|
||||
{
|
||||
Nino = nino,
|
||||
Historial = historial
|
||||
};
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
// POST: Antecedentes/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(long ninoId, AntecedentesViewModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var antecedente = new AntecedenteNino
|
||||
{
|
||||
NinoId = ninoId,
|
||||
FechaIncidente = model.FechaIncidente,
|
||||
TipoAntecedente = model.TipoAntecedente,
|
||||
Descripcion = model.Descripcion,
|
||||
Gravedad = model.Gravedad,
|
||||
UsuarioRegistra = User.Identity?.Name ?? "Sistema",
|
||||
Activo = true,
|
||||
CreadoEn = DateTime.UtcNow,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
var success = await _antecedentesService.AddAntecedenteAsync(antecedente);
|
||||
if (success)
|
||||
{
|
||||
return RedirectToAction(nameof(Manage), new { id = ninoId });
|
||||
}
|
||||
|
||||
ModelState.AddModelError("", "Error al guardar el antecedente.");
|
||||
}
|
||||
|
||||
// Si falla, recargar la vista Manage con los datos actuales
|
||||
var nino = await _context.Ninos
|
||||
.Include(n => n.Persona)
|
||||
.FirstOrDefaultAsync(n => n.Id == ninoId);
|
||||
|
||||
if (nino == null) return NotFound();
|
||||
|
||||
model.Nino = nino;
|
||||
model.Historial = await _antecedentesService.GetHistorialByNinoIdAsync(ninoId);
|
||||
|
||||
return View("Manage", model);
|
||||
}
|
||||
}
|
||||
226
foundation_system/Controllers/AsistenciaController.cs
Normal file
226
foundation_system/Controllers/AsistenciaController.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using foundation_system.Data;
|
||||
using foundation_system.Models;
|
||||
using foundation_system.Models.ViewModels;
|
||||
|
||||
namespace foundation_system.Controllers;
|
||||
|
||||
public class AsistenciaController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public AsistenciaController(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// GET: Asistencia
|
||||
public async Task<IActionResult> Index(int? año, int? mes, string? diasSemana)
|
||||
{
|
||||
int targetAnio = año ?? DateTime.Now.Year;
|
||||
int targetMes = mes ?? DateTime.Now.Month;
|
||||
|
||||
var diasSeleccionadosList = string.IsNullOrEmpty(diasSemana)
|
||||
? new List<string> { "1", "2", "3", "4", "5" }
|
||||
: diasSemana.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
|
||||
var firstDayOfMonth = new DateTime(targetAnio, targetMes, 1);
|
||||
var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1);
|
||||
|
||||
var ninos = await _context.Ninos
|
||||
.Include(n => n.Persona)
|
||||
.Where(n => n.Activo && n.Estado == "ACTIVO")
|
||||
.OrderBy(n => n.Persona.Nombres)
|
||||
.ToListAsync();
|
||||
|
||||
var firstDateOnly = DateOnly.FromDateTime(firstDayOfMonth);
|
||||
var lastDateOnly = DateOnly.FromDateTime(lastDayOfMonth);
|
||||
|
||||
var asistencias = await _context.Asistencias
|
||||
.Where(a => a.Fecha >= firstDateOnly && a.Fecha <= lastDateOnly)
|
||||
.ToListAsync();
|
||||
|
||||
var viewModel = new AsistenciaGridViewModel
|
||||
{
|
||||
Año = targetAnio,
|
||||
Mes = targetMes,
|
||||
NombreMes = firstDayOfMonth.ToString("MMMM", new System.Globalization.CultureInfo("es-ES")).ToUpper(),
|
||||
DiasSemanaSeleccionados = string.Join(",", diasSeleccionadosList),
|
||||
DiasDelMes = Enumerable.Range(0, lastDayOfMonth.Day)
|
||||
.Select(day => firstDayOfMonth.AddDays(day))
|
||||
.ToList(),
|
||||
Expedientes = ninos,
|
||||
Asistencias = asistencias.ToDictionary(
|
||||
a => $"{a.NinoId}_{a.Fecha:yyyy-MM-dd}",
|
||||
a => a.Estado switch {
|
||||
"PRESENTE" => "P",
|
||||
"TARDE" => "T",
|
||||
"AUSENTE" => "F",
|
||||
"JUSTIFICADO" => "J",
|
||||
"ENFERMO" => "E",
|
||||
_ => ""
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
ViewBag.Años = Enumerable.Range(DateTime.Now.Year - 5, 10)
|
||||
.Select(a => new Microsoft.AspNetCore.Mvc.Rendering.SelectListItem
|
||||
{
|
||||
Value = a.ToString(),
|
||||
Text = a.ToString(),
|
||||
Selected = a == targetAnio
|
||||
}).ToList();
|
||||
|
||||
ViewBag.Meses = Enumerable.Range(1, 12)
|
||||
.Select(m => new Microsoft.AspNetCore.Mvc.Rendering.SelectListItem
|
||||
{
|
||||
Value = m.ToString(),
|
||||
Text = new DateTime(2000, m, 1).ToString("MMMM", new System.Globalization.CultureInfo("es-ES")).ToUpper(),
|
||||
Selected = m == targetMes
|
||||
}).ToList();
|
||||
|
||||
ViewBag.DiasSemana = new List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>
|
||||
{
|
||||
new() { Value = "1", Text = "Lunes" },
|
||||
new() { Value = "2", Text = "Martes" },
|
||||
new() { Value = "3", Text = "Miércoles" },
|
||||
new() { Value = "4", Text = "Jueves" },
|
||||
new() { Value = "5", Text = "Viernes" },
|
||||
new() { Value = "6", Text = "Sábado" },
|
||||
new() { Value = "0", Text = "Domingo" }
|
||||
};
|
||||
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<JsonResult> ObtenerEstadisticas(int año, int mes)
|
||||
{
|
||||
var firstDay = new DateOnly(año, mes, 1);
|
||||
var lastDay = firstDay.AddMonths(1).AddDays(-1);
|
||||
|
||||
var asistencias = await _context.Asistencias
|
||||
.Where(a => a.Fecha >= firstDay && a.Fecha <= lastDay)
|
||||
.ToListAsync();
|
||||
|
||||
return Json(new
|
||||
{
|
||||
total = asistencias.Count,
|
||||
presentes = asistencias.Count(a => a.Estado == "PRESENTE"),
|
||||
tardes = asistencias.Count(a => a.Estado == "TARDE"),
|
||||
faltas = asistencias.Count(a => a.Estado == "AUSENTE"),
|
||||
justificados = asistencias.Count(a => a.Estado == "JUSTIFICADO"),
|
||||
enfermos = asistencias.Count(a => a.Estado == "ENFERMO")
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> GuardarAsistencia(long expedienteId, DateOnly fecha, string estado)
|
||||
{
|
||||
string dbEstado = estado switch
|
||||
{
|
||||
"P" => "PRESENTE",
|
||||
"T" => "TARDE",
|
||||
"F" => "AUSENTE",
|
||||
"J" => "JUSTIFICADO",
|
||||
"E" => "ENFERMO",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(dbEstado))
|
||||
{
|
||||
// Si el estado es vacío, podríamos borrar el registro o dejarlo como pendiente
|
||||
var existing = await _context.Asistencias
|
||||
.FirstOrDefaultAsync(a => a.NinoId == expedienteId && a.Fecha == fecha);
|
||||
if (existing != null)
|
||||
{
|
||||
_context.Asistencias.Remove(existing);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
return Json(new { success = true });
|
||||
}
|
||||
|
||||
var asistencia = await _context.Asistencias
|
||||
.FirstOrDefaultAsync(a => a.NinoId == expedienteId && a.Fecha == fecha);
|
||||
|
||||
if (asistencia == null)
|
||||
{
|
||||
asistencia = new Asistencia
|
||||
{
|
||||
NinoId = expedienteId,
|
||||
Fecha = fecha,
|
||||
Estado = dbEstado,
|
||||
CreadoEn = DateTime.UtcNow
|
||||
};
|
||||
_context.Asistencias.Add(asistencia);
|
||||
}
|
||||
else
|
||||
{
|
||||
asistencia.Estado = dbEstado;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return Json(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> GuardarAsistenciasMasivas([FromBody] List<AsistenciaInputModel> cambios)
|
||||
{
|
||||
foreach (var cambio in cambios)
|
||||
{
|
||||
string dbEstado = cambio.Estado switch
|
||||
{
|
||||
"P" => "PRESENTE",
|
||||
"T" => "TARDE",
|
||||
"F" => "AUSENTE",
|
||||
"J" => "JUSTIFICADO",
|
||||
"E" => "ENFERMO",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
var asistencia = await _context.Asistencias
|
||||
.FirstOrDefaultAsync(a => a.NinoId == cambio.ExpedienteId && a.Fecha == cambio.Fecha);
|
||||
|
||||
if (string.IsNullOrEmpty(dbEstado))
|
||||
{
|
||||
if (asistencia != null) _context.Asistencias.Remove(asistencia);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (asistencia == null)
|
||||
{
|
||||
_context.Asistencias.Add(new Asistencia
|
||||
{
|
||||
NinoId = cambio.ExpedienteId,
|
||||
Fecha = cambio.Fecha,
|
||||
Estado = dbEstado,
|
||||
CreadoEn = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
asistencia.Estado = dbEstado;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return Json(new { success = true, message = "Cambios guardados correctamente" });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ExportarExcel(int año, int mes, string diasSemana)
|
||||
{
|
||||
// Placeholder for Excel export
|
||||
return File(new byte[0], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $"Asistencia_{año}_{mes}.xlsx");
|
||||
}
|
||||
}
|
||||
|
||||
public class AsistenciaInputModel
|
||||
{
|
||||
public long ExpedienteId { get; set; }
|
||||
public DateOnly Fecha { get; set; }
|
||||
public string Estado { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
85
foundation_system/Controllers/ConfiguracionController.cs
Normal file
85
foundation_system/Controllers/ConfiguracionController.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using foundation_system.Data;
|
||||
using foundation_system.Models;
|
||||
|
||||
namespace foundation_system.Controllers;
|
||||
|
||||
public class ConfiguracionController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public ConfiguracionController(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// GET: Configuracion
|
||||
public async Task<IActionResult> Index(string? categoria)
|
||||
{
|
||||
var query = _context.Configuraciones.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(categoria))
|
||||
{
|
||||
query = query.Where(c => c.Categoria == categoria);
|
||||
}
|
||||
|
||||
var configuraciones = await query
|
||||
.OrderBy(c => c.Categoria)
|
||||
.ThenBy(c => c.Grupo)
|
||||
.ThenBy(c => c.Orden)
|
||||
.ToListAsync();
|
||||
|
||||
ViewBag.Categorias = await _context.Configuraciones
|
||||
.Select(c => c.Categoria)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
ViewBag.SelectedCategoria = categoria;
|
||||
|
||||
return View(configuraciones);
|
||||
}
|
||||
|
||||
// GET: Configuracion/Edit/5
|
||||
public async Task<IActionResult> Edit(int? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var config = await _context.Configuraciones.FindAsync(id);
|
||||
if (config == null || !config.EsEditable) return NotFound();
|
||||
|
||||
return View(config);
|
||||
}
|
||||
|
||||
// POST: Configuracion/Edit/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(int id, [Bind("Id,Valor")] ConfiguracionSistema model)
|
||||
{
|
||||
if (id != model.Id) return NotFound();
|
||||
|
||||
var config = await _context.Configuraciones.FindAsync(id);
|
||||
if (config == null || !config.EsEditable) return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
config.Valor = model.Valor;
|
||||
config.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.Update(config);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return RedirectToAction(nameof(Index), new { categoria = config.Categoria });
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!ConfiguracionExists(model.Id)) return NotFound();
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ConfiguracionExists(int id)
|
||||
{
|
||||
return _context.Configuraciones.Any(e => e.Id == id);
|
||||
}
|
||||
}
|
||||
556
foundation_system/Controllers/ExpedienteController.cs
Normal file
556
foundation_system/Controllers/ExpedienteController.cs
Normal file
@@ -0,0 +1,556 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using foundation_system.Data;
|
||||
using foundation_system.Models;
|
||||
using foundation_system.Models.ViewModels;
|
||||
using foundation_system.Services;
|
||||
|
||||
namespace foundation_system.Controllers;
|
||||
|
||||
public class ExpedienteController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IWebHostEnvironment _hostingEnvironment;
|
||||
private readonly IAntecedentesService _antecedentesService;
|
||||
|
||||
public ExpedienteController(ApplicationDbContext context, IWebHostEnvironment hostingEnvironment, IAntecedentesService antecedentesService)
|
||||
{
|
||||
_context = context;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_antecedentesService = antecedentesService;
|
||||
}
|
||||
|
||||
// GET: Expediente
|
||||
public async Task<IActionResult> Index(string search = "", int page = 1, int pageSize = 10)
|
||||
{
|
||||
var query = _context.Ninos
|
||||
.Include(n => n.Persona)
|
||||
.Where(n => n.Activo);
|
||||
|
||||
List<long> searchIds = new();
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
searchIds = await PostgresQueryExecutor.ExecuteQueryAsync(
|
||||
_context,
|
||||
"SELECT id FROM buscar_personas_v2(@term, 'NINO')",
|
||||
reader => reader.GetInt64(0),
|
||||
p => p.AddWithValue("term", search)
|
||||
);
|
||||
|
||||
query = query.Where(n => searchIds.Contains(n.Id));
|
||||
}
|
||||
|
||||
var totalItems = await query.CountAsync();
|
||||
var totalPages = (int)Math.Ceiling(totalItems / (double)pageSize);
|
||||
|
||||
// Asegurar que la página esté en rango
|
||||
page = Math.Max(1, Math.Min(page, totalPages > 0 ? totalPages : 1));
|
||||
|
||||
var ninos = await query
|
||||
.OrderByDescending(n => n.CreadoEn)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
// Preserve the order from the fuzzy search procedure
|
||||
ninos = ninos.OrderBy(n => searchIds.IndexOf(n.Id)).ToList();
|
||||
}
|
||||
|
||||
ViewBag.Search = search;
|
||||
ViewBag.CurrentPage = page;
|
||||
ViewBag.TotalPages = totalPages;
|
||||
ViewBag.HasPreviousPage = page > 1;
|
||||
ViewBag.HasNextPage = page < totalPages;
|
||||
ViewBag.PageSize = pageSize;
|
||||
|
||||
return View(ninos);
|
||||
}
|
||||
|
||||
// GET: Expediente/Details/5
|
||||
public async Task<IActionResult> Details(long? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var nino = await _context.Ninos
|
||||
.Include(n => n.Persona)
|
||||
.FirstOrDefaultAsync(m => m.Id == id);
|
||||
|
||||
if (nino == null) return NotFound();
|
||||
|
||||
var model = await MapToExpedienteViewModel(nino);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// GET: Expediente/Print/5
|
||||
public async Task<IActionResult> Print(long? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var nino = await _context.Ninos
|
||||
.Include(n => n.Persona)
|
||||
.FirstOrDefaultAsync(m => m.Id == id);
|
||||
|
||||
if (nino == null) return NotFound();
|
||||
|
||||
var model = await MapToExpedienteViewModel(nino);
|
||||
var antecedentes = await _antecedentesService.GetHistorialByNinoIdAsync(id.Value);
|
||||
|
||||
ViewBag.Antecedentes = antecedentes;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private async Task<ExpedienteViewModel> MapToExpedienteViewModel(Nino nino)
|
||||
{
|
||||
var model = new ExpedienteViewModel
|
||||
{
|
||||
Id = nino.Id,
|
||||
Nombres = nino.Persona.Nombres,
|
||||
Apellidos = nino.Persona.Apellidos,
|
||||
Dui = nino.Persona.Dui,
|
||||
Nit = nino.Persona.Nit,
|
||||
FechaNacimiento = nino.Persona.FechaNacimiento,
|
||||
Genero = nino.Persona.Genero,
|
||||
Email = nino.Persona.Email,
|
||||
Telefono = nino.Persona.Telefono,
|
||||
Direccion = nino.Persona.Direccion,
|
||||
FotoUrl = nino.Persona.FotoUrl,
|
||||
CodigoInscripcion = nino.CodigoInscripcion,
|
||||
FechaInscripcion = nino.FechaInscripcion,
|
||||
Estado = nino.Estado,
|
||||
NivelGrado = nino.NivelGrado,
|
||||
Alergias = nino.Alergias,
|
||||
ContactoEmergenciaNombre = nino.ContactoEmergenciaNombre,
|
||||
ContactoEmergenciaTelefono = nino.ContactoEmergenciaTelefono
|
||||
};
|
||||
|
||||
var encargados = await _context.EncargadosNino
|
||||
.Include(e => e.Persona)
|
||||
.Where(e => e.NinoId == nino.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var padre = encargados.FirstOrDefault(e => e.Parentesco == "PADRE");
|
||||
if (padre != null)
|
||||
{
|
||||
model.PadreId = padre.PersonaId;
|
||||
model.PadreNombre = padre.Persona.NombreCompleto;
|
||||
}
|
||||
|
||||
var madre = encargados.FirstOrDefault(e => e.Parentesco == "MADRE");
|
||||
if (madre != null)
|
||||
{
|
||||
model.MadreId = madre.PersonaId;
|
||||
model.MadreNombre = madre.Persona.NombreCompleto;
|
||||
}
|
||||
|
||||
var encargado = encargados.FirstOrDefault(e => e.Parentesco == "ENCARGADO");
|
||||
if (encargado != null)
|
||||
{
|
||||
model.EncargadoId = encargado.PersonaId;
|
||||
model.EncargadoNombre = encargado.Persona.NombreCompleto;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
// GET: Expediente/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View(new ExpedienteViewModel());
|
||||
}
|
||||
|
||||
// POST: Expediente/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(ExpedienteViewModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var persona = new Persona
|
||||
{
|
||||
Nombres = model.Nombres,
|
||||
Apellidos = model.Apellidos,
|
||||
Dui = model.Dui,
|
||||
Nit = model.Nit,
|
||||
FechaNacimiento = model.FechaNacimiento,
|
||||
Genero = model.Genero,
|
||||
Email = model.Email,
|
||||
Telefono = null, // Los niños no tienen teléfono
|
||||
Direccion = model.Direccion,
|
||||
FotoUrl = string.IsNullOrEmpty(model.FotoUrl) ? "/Assets/default_avatar.png" : model.FotoUrl,
|
||||
Activo = true
|
||||
};
|
||||
|
||||
_context.Personas.Add(persona);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Generar Código de Inscripción automáticamente usando procedimiento almacenado
|
||||
var nuevoCodigo = await PostgresScalarExecutor.ExecuteAsync<string>(
|
||||
_context,
|
||||
"SELECT generar_codigo_inscripcion(@apellidos, @anio)",
|
||||
p =>
|
||||
{
|
||||
p.AddWithValue("apellidos", persona.Apellidos);
|
||||
p.AddWithValue("anio", model.FechaInscripcion.Year);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
var nino = new Nino
|
||||
{
|
||||
PersonaId = persona.Id,
|
||||
CodigoInscripcion = nuevoCodigo,
|
||||
FechaInscripcion = model.FechaInscripcion,
|
||||
Estado = model.Estado,
|
||||
NivelGrado = model.NivelGrado,
|
||||
Alergias = model.Alergias,
|
||||
ContactoEmergenciaNombre = model.ContactoEmergenciaNombre,
|
||||
ContactoEmergenciaTelefono = model.ContactoEmergenciaTelefono,
|
||||
Activo = true
|
||||
};
|
||||
|
||||
_context.Ninos.Add(nino);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Procesar foto si se subió una
|
||||
if (model.FotoFile != null)
|
||||
{
|
||||
var fotoUrl = await ProcessPhotoUpload(model.FotoFile, nino.CodigoInscripcion);
|
||||
persona.FotoUrl = fotoUrl;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Guardar Padres si se seleccionaron
|
||||
if (model.PadreId.HasValue)
|
||||
{
|
||||
_context.EncargadosNino.Add(new EncargadoNino
|
||||
{
|
||||
NinoId = nino.Id,
|
||||
PersonaId = model.PadreId.Value,
|
||||
Parentesco = "PADRE",
|
||||
EsPrincipal = false
|
||||
});
|
||||
}
|
||||
|
||||
if (model.MadreId.HasValue)
|
||||
{
|
||||
_context.EncargadosNino.Add(new EncargadoNino
|
||||
{
|
||||
NinoId = nino.Id,
|
||||
PersonaId = model.MadreId.Value,
|
||||
Parentesco = "MADRE",
|
||||
EsPrincipal = false
|
||||
});
|
||||
}
|
||||
|
||||
if (model.EncargadoId.HasValue)
|
||||
{
|
||||
_context.EncargadosNino.Add(new EncargadoNino
|
||||
{
|
||||
NinoId = nino.Id,
|
||||
PersonaId = model.EncargadoId.Value,
|
||||
Parentesco = "ENCARGADO",
|
||||
EsPrincipal = true
|
||||
});
|
||||
}
|
||||
|
||||
if (model.PadreId.HasValue || model.MadreId.HasValue || model.EncargadoId.HasValue)
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
ModelState.AddModelError("", "Ocurrió un error al guardar el expediente.\n" + ex.Message);
|
||||
}
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// GET: Expediente/Edit/5
|
||||
public async Task<IActionResult> Edit(long? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var nino = await _context.Ninos
|
||||
.Include(n => n.Persona)
|
||||
.FirstOrDefaultAsync(n => n.Id == id);
|
||||
|
||||
if (nino == null) return NotFound();
|
||||
|
||||
var model = new ExpedienteViewModel
|
||||
{
|
||||
Id = nino.Id,
|
||||
Nombres = nino.Persona.Nombres,
|
||||
Apellidos = nino.Persona.Apellidos,
|
||||
Dui = nino.Persona.Dui,
|
||||
Nit = nino.Persona.Nit,
|
||||
FechaNacimiento = nino.Persona.FechaNacimiento,
|
||||
Genero = nino.Persona.Genero,
|
||||
Email = nino.Persona.Email,
|
||||
Telefono = nino.Persona.Telefono,
|
||||
Direccion = nino.Persona.Direccion,
|
||||
FotoUrl = nino.Persona.FotoUrl,
|
||||
CodigoInscripcion = nino.CodigoInscripcion,
|
||||
FechaInscripcion = nino.FechaInscripcion,
|
||||
Estado = nino.Estado,
|
||||
NivelGrado = nino.NivelGrado,
|
||||
Alergias = nino.Alergias,
|
||||
ContactoEmergenciaNombre = nino.ContactoEmergenciaNombre,
|
||||
ContactoEmergenciaTelefono = nino.ContactoEmergenciaTelefono
|
||||
};
|
||||
|
||||
// Cargar Padres/Encargados
|
||||
var encargados = await _context.EncargadosNino
|
||||
.Include(e => e.Persona)
|
||||
.Where(e => e.NinoId == id)
|
||||
.ToListAsync();
|
||||
|
||||
var padre = encargados.FirstOrDefault(e => e.Parentesco == "PADRE");
|
||||
if (padre != null)
|
||||
{
|
||||
model.PadreId = padre.PersonaId;
|
||||
model.PadreNombre = $"{padre.Persona.Nombres} {padre.Persona.Apellidos}";
|
||||
}
|
||||
|
||||
var madre = encargados.FirstOrDefault(e => e.Parentesco == "MADRE");
|
||||
if (madre != null)
|
||||
{
|
||||
model.MadreId = madre.PersonaId;
|
||||
model.MadreNombre = $"{madre.Persona.Nombres} {madre.Persona.Apellidos}";
|
||||
}
|
||||
|
||||
var encargado = encargados.FirstOrDefault(e => e.Parentesco == "ENCARGADO");
|
||||
if (encargado != null)
|
||||
{
|
||||
model.EncargadoId = encargado.PersonaId;
|
||||
model.EncargadoNombre = $"{encargado.Persona.Nombres} {encargado.Persona.Apellidos}";
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// POST: Expediente/Edit/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(long id, ExpedienteViewModel model)
|
||||
{
|
||||
if (id != model.Id) return NotFound();
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var nino = await _context.Ninos
|
||||
.Include(n => n.Persona)
|
||||
.FirstOrDefaultAsync(n => n.Id == id);
|
||||
|
||||
if (nino == null) return NotFound();
|
||||
|
||||
// Update Persona
|
||||
nino.Persona.Nombres = model.Nombres;
|
||||
nino.Persona.Apellidos = model.Apellidos;
|
||||
nino.Persona.Dui = model.Dui;
|
||||
nino.Persona.Nit = model.Nit;
|
||||
nino.Persona.FechaNacimiento = model.FechaNacimiento;
|
||||
nino.Persona.Genero = model.Genero;
|
||||
nino.Persona.Email = model.Email;
|
||||
nino.Persona.Telefono = null; // Los niños no tienen teléfono
|
||||
nino.Persona.Direccion = model.Direccion;
|
||||
|
||||
// Procesar foto si se subió una nueva
|
||||
if (model.FotoFile != null)
|
||||
{
|
||||
nino.Persona.FotoUrl = await ProcessPhotoUpload(model.FotoFile, nino.CodigoInscripcion);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(nino.Persona.FotoUrl))
|
||||
{
|
||||
nino.Persona.FotoUrl = "/Assets/default_avatar.png";
|
||||
}
|
||||
|
||||
nino.Persona.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
// Update Nino
|
||||
nino.CodigoInscripcion = model.CodigoInscripcion;
|
||||
nino.FechaInscripcion = model.FechaInscripcion;
|
||||
nino.Estado = model.Estado;
|
||||
nino.NivelGrado = model.NivelGrado;
|
||||
nino.Alergias = model.Alergias;
|
||||
nino.ContactoEmergenciaNombre = model.ContactoEmergenciaNombre;
|
||||
nino.ContactoEmergenciaTelefono = model.ContactoEmergenciaTelefono;
|
||||
nino.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Actualizar Encargados (Eliminar y volver a agregar para simplicidad)
|
||||
var encargadosExistentes = await _context.EncargadosNino
|
||||
.Where(e => e.NinoId == id)
|
||||
.ToListAsync();
|
||||
_context.EncargadosNino.RemoveRange(encargadosExistentes);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
if (model.PadreId.HasValue)
|
||||
{
|
||||
_context.EncargadosNino.Add(new EncargadoNino
|
||||
{
|
||||
NinoId = nino.Id,
|
||||
PersonaId = model.PadreId.Value,
|
||||
Parentesco = "PADRE",
|
||||
EsPrincipal = false
|
||||
});
|
||||
}
|
||||
|
||||
if (model.MadreId.HasValue)
|
||||
{
|
||||
_context.EncargadosNino.Add(new EncargadoNino
|
||||
{
|
||||
NinoId = nino.Id,
|
||||
PersonaId = model.MadreId.Value,
|
||||
Parentesco = "MADRE",
|
||||
EsPrincipal = false
|
||||
});
|
||||
}
|
||||
|
||||
if (model.EncargadoId.HasValue)
|
||||
{
|
||||
_context.EncargadosNino.Add(new EncargadoNino
|
||||
{
|
||||
NinoId = nino.Id,
|
||||
PersonaId = model.EncargadoId.Value,
|
||||
Parentesco = "ENCARGADO",
|
||||
EsPrincipal = true
|
||||
});
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
ModelState.AddModelError("", "Ocurrió un error al actualizar el expediente.\n" + ex.Message);
|
||||
}
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// POST: Expediente/Desactivar/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Desactivar(long id)
|
||||
{
|
||||
var nino = await _context.Ninos.FindAsync(id);
|
||||
if (nino != null)
|
||||
{
|
||||
nino.Activo = false;
|
||||
nino.Estado = "INACTIVO";
|
||||
nino.ActualizadoEn = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
private bool NinoExists(long id)
|
||||
{
|
||||
return _context.Ninos.Any(e => e.Id == id);
|
||||
}
|
||||
|
||||
// AJAX Actions for Parents
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> SearchPersonas(string term, string type = "ENCARGADO")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(term)) return Json(new List<object>());
|
||||
|
||||
var personas = await PostgresQueryExecutor.ExecuteQueryAsync(
|
||||
_context,
|
||||
"SELECT id, text, score FROM buscar_personas_v2(@term, @type)",
|
||||
reader => new
|
||||
{
|
||||
id = reader.GetInt64(0),
|
||||
text = reader.GetString(1),
|
||||
score = reader.GetDouble(2)
|
||||
},
|
||||
paramsColl => {
|
||||
paramsColl.AddWithValue("term", term);
|
||||
paramsColl.AddWithValue("type", type);
|
||||
}
|
||||
);
|
||||
|
||||
return Json(personas);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CreatePersona([FromBody] Persona model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
model.Activo = true;
|
||||
model.CreadoEn = DateTime.UtcNow;
|
||||
model.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.Personas.Add(model);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Json(new { success = true, id = model.Id, text = $"{model.Nombres} {model.Apellidos} ({model.Dui ?? "Sin DUI"})" });
|
||||
}
|
||||
|
||||
return Json(new { success = false, message = "Datos inválidos" });
|
||||
}
|
||||
|
||||
private async Task<string> ProcessPhotoUpload(IFormFile file, string codigoExpediente)
|
||||
{
|
||||
if (file == null || file.Length == 0) return "/Assets/default_avatar.png";
|
||||
|
||||
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp" };
|
||||
var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
|
||||
if (!allowedExtensions.Contains(fileExtension)) return "/Assets/default_avatar.png";
|
||||
|
||||
var uploadsFolder = Path.Combine(_hostingEnvironment.WebRootPath, "uploads", "fotos");
|
||||
if (!Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
}
|
||||
|
||||
// Usar el código del expediente como nombre de archivo
|
||||
var fileName = codigoExpediente + fileExtension;
|
||||
var filePath = Path.Combine(uploadsFolder, fileName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
return $"/uploads/fotos/{fileName}";
|
||||
}
|
||||
|
||||
public string ExisteFoto(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(url))
|
||||
return "/Assets/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 "/Assets/default_avatar.png";
|
||||
}
|
||||
}
|
||||
191
foundation_system/Controllers/UsuarioController.cs
Normal file
191
foundation_system/Controllers/UsuarioController.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using foundation_system.Data;
|
||||
using foundation_system.Models;
|
||||
using foundation_system.Models.ViewModels;
|
||||
using BCrypt.Net;
|
||||
|
||||
namespace foundation_system.Controllers;
|
||||
|
||||
public class UsuarioController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public UsuarioController(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// GET: Usuario
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var usuarios = await _context.Usuarios
|
||||
.Include(u => u.Persona)
|
||||
.ToListAsync();
|
||||
return View(usuarios);
|
||||
}
|
||||
|
||||
// GET: Usuario/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View(new UsuarioViewModel());
|
||||
}
|
||||
|
||||
// POST: Usuario/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(UsuarioViewModel model)
|
||||
{
|
||||
if (string.IsNullOrEmpty(model.Contrasena))
|
||||
{
|
||||
ModelState.AddModelError("Contrasena", "La contraseña es requerida para nuevos usuarios");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
// Check if username or email already exists
|
||||
if (await _context.Usuarios.AnyAsync(u => u.NombreUsuario == model.NombreUsuario))
|
||||
{
|
||||
ModelState.AddModelError("NombreUsuario", "El nombre de usuario ya está en uso");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (await _context.Usuarios.AnyAsync(u => u.Email == model.Email))
|
||||
{
|
||||
ModelState.AddModelError("Email", "El correo electrónico ya está en uso");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var persona = new Persona
|
||||
{
|
||||
Nombres = model.Nombres,
|
||||
Apellidos = model.Apellidos,
|
||||
Email = model.Email,
|
||||
Telefono = model.Telefono,
|
||||
Activo = true
|
||||
};
|
||||
|
||||
_context.Personas.Add(persona);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var usuario = new Usuario
|
||||
{
|
||||
PersonaId = persona.Id,
|
||||
NombreUsuario = model.NombreUsuario,
|
||||
Email = model.Email,
|
||||
HashContrasena = BCrypt.Net.BCrypt.HashPassword(model.Contrasena),
|
||||
Activo = true,
|
||||
CreadoEn = DateTime.UtcNow,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.Usuarios.Add(usuario);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
ModelState.AddModelError("", "Ocurrió un error al crear el usuario.");
|
||||
}
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// GET: Usuario/Edit/5
|
||||
public async Task<IActionResult> Edit(long? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var usuario = await _context.Usuarios
|
||||
.Include(u => u.Persona)
|
||||
.FirstOrDefaultAsync(u => u.Id == id);
|
||||
|
||||
if (usuario == null) return NotFound();
|
||||
|
||||
var model = new UsuarioViewModel
|
||||
{
|
||||
Id = usuario.Id,
|
||||
Nombres = usuario.Persona.Nombres,
|
||||
Apellidos = usuario.Persona.Apellidos,
|
||||
NombreUsuario = usuario.NombreUsuario,
|
||||
Email = usuario.Email,
|
||||
Telefono = usuario.Persona.Telefono,
|
||||
Activo = usuario.Activo
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// POST: Usuario/Edit/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(long id, UsuarioViewModel model)
|
||||
{
|
||||
if (id != model.Id) return NotFound();
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var usuario = await _context.Usuarios
|
||||
.Include(u => u.Persona)
|
||||
.FirstOrDefaultAsync(u => u.Id == id);
|
||||
|
||||
if (usuario == null) return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
// Update Persona
|
||||
usuario.Persona.Nombres = model.Nombres;
|
||||
usuario.Persona.Apellidos = model.Apellidos;
|
||||
usuario.Persona.Telefono = model.Telefono;
|
||||
usuario.Persona.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
// Update Usuario
|
||||
usuario.NombreUsuario = model.NombreUsuario;
|
||||
usuario.Email = model.Email;
|
||||
usuario.Activo = model.Activo;
|
||||
usuario.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
// Update password if provided
|
||||
if (!string.IsNullOrEmpty(model.Contrasena))
|
||||
{
|
||||
usuario.HashContrasena = BCrypt.Net.BCrypt.HashPassword(model.Contrasena);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
if (!UsuarioExists(model.Id.Value)) return NotFound();
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// POST: Usuario/Desactivar/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Desactivar(long id)
|
||||
{
|
||||
var usuario = await _context.Usuarios.FindAsync(id);
|
||||
if (usuario != null)
|
||||
{
|
||||
usuario.Activo = false;
|
||||
usuario.ActualizadoEn = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
private bool UsuarioExists(long id)
|
||||
{
|
||||
return _context.Usuarios.Any(e => e.Id == id);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ public class ApplicationDbContext : DbContext
|
||||
public DbSet<Asistencia> Asistencias { get; set; }
|
||||
public DbSet<ConfiguracionSistema> Configuraciones { get; set; }
|
||||
public DbSet<EncargadoNino> EncargadosNino { get; set; }
|
||||
public DbSet<AntecedenteNino> AntecedentesNino { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -42,5 +43,19 @@ public class ApplicationDbContext : DbContext
|
||||
.HasOne(u => u.Persona)
|
||||
.WithMany()
|
||||
.HasForeignKey(u => u.PersonaId);
|
||||
|
||||
// Global configuration: Convert all dates to UTC when saving
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var properties = entityType.GetProperties()
|
||||
.Where(p => p.ClrType == typeof(DateTime) || p.ClrType == typeof(DateTime?));
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
property.SetValueConverter(new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<DateTime, DateTime>(
|
||||
v => v.Kind == DateTimeKind.Utc ? v : DateTime.SpecifyKind(v, DateTimeKind.Utc),
|
||||
v => v));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
foundation_system/Data/PostgresQueryExecutor.cs
Normal file
39
foundation_system/Data/PostgresQueryExecutor.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Npgsql;
|
||||
|
||||
namespace foundation_system.Data;
|
||||
|
||||
public static class PostgresQueryExecutor
|
||||
{
|
||||
public static async Task<List<T>> ExecuteQueryAsync<T>(
|
||||
DbContext context,
|
||||
string sql,
|
||||
Func<NpgsqlDataReader, T> map,
|
||||
Action<NpgsqlParameterCollection>? parameters = null
|
||||
)
|
||||
{
|
||||
var conn = (NpgsqlConnection)context.Database.GetDbConnection();
|
||||
|
||||
if (conn.State != ConnectionState.Open)
|
||||
await conn.OpenAsync();
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
|
||||
var currentTx = context.Database.CurrentTransaction;
|
||||
if (currentTx != null)
|
||||
cmd.Transaction = (NpgsqlTransaction)currentTx.GetDbTransaction();
|
||||
|
||||
parameters?.Invoke(cmd.Parameters);
|
||||
|
||||
var results = new List<T>();
|
||||
await using var reader = await cmd.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
results.Add(map(reader));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
27
foundation_system/Dockerfile
Normal file
27
foundation_system/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
# Runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
# Build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
|
||||
# Copiar TODO el proyecto
|
||||
COPY . .
|
||||
|
||||
# Restaurar y compilar
|
||||
RUN dotnet restore "foundation_system.csproj"
|
||||
RUN dotnet build "foundation_system.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# Publish
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "foundation_system.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Final
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "foundation_system.dll"]
|
||||
51
foundation_system/Models/AntecedenteNino.cs
Normal file
51
foundation_system/Models/AntecedenteNino.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace foundation_system.Models;
|
||||
|
||||
[Table("antecedentes_nino")]
|
||||
public class AntecedenteNino
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("nino_id")]
|
||||
public long NinoId { get; set; }
|
||||
|
||||
[ForeignKey("NinoId")]
|
||||
public virtual Nino Nino { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
[Column("fecha_incidente")]
|
||||
public DateTime FechaIncidente { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Required]
|
||||
[Column("tipo_antecedente")]
|
||||
[StringLength(50)]
|
||||
public string TipoAntecedente { get; set; } = string.Empty; // LlamadaAtencion, Suspension, Castigo
|
||||
|
||||
[Required]
|
||||
[Column("descripcion")]
|
||||
public string Descripcion { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[Column("gravedad")]
|
||||
[StringLength(20)]
|
||||
public string Gravedad { get; set; } = string.Empty; // Alta, Media, Baja
|
||||
|
||||
[Required]
|
||||
[Column("usuario_registra")]
|
||||
[StringLength(100)]
|
||||
public string UsuarioRegistra { get; set; } = string.Empty;
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
39
foundation_system/Models/Asistencia.cs
Normal file
39
foundation_system/Models/Asistencia.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace foundation_system.Models;
|
||||
|
||||
[Table("asistencia")]
|
||||
public class Asistencia
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("nino_id")]
|
||||
public long NinoId { get; set; }
|
||||
|
||||
[Column("fecha")]
|
||||
public DateOnly Fecha { get; set; } = DateOnly.FromDateTime(DateTime.Now);
|
||||
|
||||
[Column("estado")]
|
||||
[Required]
|
||||
[StringLength(20)]
|
||||
public string Estado { get; set; } = "PRESENTE";
|
||||
|
||||
[Column("hora_entrada")]
|
||||
public TimeOnly? HoraEntrada { get; set; }
|
||||
|
||||
[Column("hora_salida")]
|
||||
public TimeOnly? HoraSalida { get; set; }
|
||||
|
||||
[Column("notas")]
|
||||
public string? Notas { get; set; }
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation properties
|
||||
[ForeignKey("NinoId")]
|
||||
public Nino Nino { get; set; } = null!;
|
||||
}
|
||||
57
foundation_system/Models/ConfiguracionSistema.cs
Normal file
57
foundation_system/Models/ConfiguracionSistema.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace foundation_system.Models;
|
||||
|
||||
[Table("configuracion_sistema")]
|
||||
public class ConfiguracionSistema
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("clave")]
|
||||
[StringLength(100)]
|
||||
public string Clave { get; set; } = string.Empty;
|
||||
|
||||
[Column("valor")]
|
||||
public string? Valor { get; set; }
|
||||
|
||||
[Column("tipo_dato")]
|
||||
[StringLength(20)]
|
||||
public string TipoDato { get; set; } = "TEXTO";
|
||||
|
||||
[Column("categoria")]
|
||||
[StringLength(50)]
|
||||
public string Categoria { get; set; } = "GENERAL";
|
||||
|
||||
[Column("grupo")]
|
||||
[StringLength(50)]
|
||||
public string Grupo { get; set; } = "SISTEMA";
|
||||
|
||||
[Column("descripcion")]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("es_editable")]
|
||||
public bool EsEditable { get; set; } = true;
|
||||
|
||||
[Column("es_publico")]
|
||||
public bool EsPublico { get; set; } = false;
|
||||
|
||||
[Column("orden")]
|
||||
public int Orden { get; set; } = 0;
|
||||
|
||||
[Column("opciones", TypeName = "jsonb")]
|
||||
public string? Opciones { get; set; }
|
||||
|
||||
[Column("validacion_regex")]
|
||||
[StringLength(200)]
|
||||
public string? ValidacionRegex { get; set; }
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
39
foundation_system/Models/EncargadoNino.cs
Normal file
39
foundation_system/Models/EncargadoNino.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace foundation_system.Models;
|
||||
|
||||
[Table("encargados_nino")]
|
||||
public class EncargadoNino
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("nino_id")]
|
||||
public long NinoId { get; set; }
|
||||
|
||||
[Column("persona_id")]
|
||||
public long PersonaId { get; set; }
|
||||
|
||||
[Column("parentesco")]
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
public string Parentesco { get; set; } = "PADRE";
|
||||
|
||||
[Column("es_principal")]
|
||||
public bool EsPrincipal { get; set; } = false;
|
||||
|
||||
[Column("puede_recoger")]
|
||||
public bool PuedeRecoger { get; set; } = true;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation properties
|
||||
[ForeignKey("NinoId")]
|
||||
public Nino Nino { get; set; } = null!;
|
||||
|
||||
[ForeignKey("PersonaId")]
|
||||
public Persona Persona { get; set; } = null!;
|
||||
}
|
||||
55
foundation_system/Models/Nino.cs
Normal file
55
foundation_system/Models/Nino.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace foundation_system.Models;
|
||||
|
||||
[Table("ninos")]
|
||||
public class Nino
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("persona_id")]
|
||||
public long PersonaId { get; set; }
|
||||
|
||||
[Column("fecha_inscripcion")]
|
||||
public DateOnly FechaInscripcion { get; set; } = DateOnly.FromDateTime(DateTime.Now);
|
||||
|
||||
[Column("codigo_inscripcion")]
|
||||
[Required]
|
||||
[StringLength(20)]
|
||||
public string CodigoInscripcion { get; set; } = string.Empty;
|
||||
|
||||
[Column("estado")]
|
||||
[StringLength(20)]
|
||||
public string Estado { get; set; } = "ACTIVO";
|
||||
|
||||
[Column("nivel_grado")]
|
||||
[StringLength(20)]
|
||||
public string? NivelGrado { get; set; }
|
||||
|
||||
[Column("alergias")]
|
||||
public string? Alergias { get; set; }
|
||||
|
||||
[Column("contacto_emergencia_nombre")]
|
||||
[StringLength(100)]
|
||||
public string? ContactoEmergenciaNombre { get; set; }
|
||||
|
||||
[Column("contacto_emergencia_telefono")]
|
||||
[StringLength(20)]
|
||||
public string? ContactoEmergenciaTelefono { get; set; }
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation properties
|
||||
[ForeignKey("PersonaId")]
|
||||
public Persona Persona { get; set; } = null!;
|
||||
}
|
||||
64
foundation_system/Models/Persona.cs
Normal file
64
foundation_system/Models/Persona.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace foundation_system.Models;
|
||||
|
||||
[Table("personas")]
|
||||
public class Persona
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("nombres")]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Nombres { get; set; } = string.Empty;
|
||||
|
||||
[Column("apellidos")]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Apellidos { get; set; } = string.Empty;
|
||||
|
||||
[Column("dui")]
|
||||
[StringLength(12)]
|
||||
public string? Dui { get; set; }
|
||||
|
||||
[Column("nit")]
|
||||
[StringLength(17)]
|
||||
public string? Nit { get; set; }
|
||||
|
||||
[Column("fecha_nacimiento")]
|
||||
public DateOnly? FechaNacimiento { get; set; }
|
||||
|
||||
[Column("genero")]
|
||||
[StringLength(1)]
|
||||
public string? Genero { get; set; }
|
||||
|
||||
[Column("email")]
|
||||
[StringLength(255)]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Column("telefono")]
|
||||
[StringLength(20)]
|
||||
public string? Telefono { get; set; }
|
||||
|
||||
[Column("direccion")]
|
||||
public string? Direccion { get; set; }
|
||||
|
||||
[Column("foto_url")]
|
||||
public string? FotoUrl { get; set; }
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Nombre completo
|
||||
[NotMapped]
|
||||
public string NombreCompleto => $"{Nombres} {Apellidos}";
|
||||
}
|
||||
30
foundation_system/Models/RolSistema.cs
Normal file
30
foundation_system/Models/RolSistema.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace foundation_system.Models;
|
||||
|
||||
[Table("roles_sistema")]
|
||||
public class RolSistema
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("codigo")]
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
public string Codigo { get; set; } = string.Empty;
|
||||
|
||||
[Column("nombre")]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public ICollection<RolUsuario> RolesUsuario { get; set; } = new List<RolUsuario>();
|
||||
}
|
||||
24
foundation_system/Models/RolUsuario.cs
Normal file
24
foundation_system/Models/RolUsuario.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace foundation_system.Models;
|
||||
|
||||
[Table("roles_usuario")]
|
||||
public class RolUsuario
|
||||
{
|
||||
[Column("usuario_id")]
|
||||
public long UsuarioId { get; set; }
|
||||
|
||||
[Column("rol_id")]
|
||||
public int RolId { get; set; }
|
||||
|
||||
[Column("asignado_en")]
|
||||
public DateTime AsignadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation properties
|
||||
[ForeignKey("UsuarioId")]
|
||||
public Usuario Usuario { get; set; } = null!;
|
||||
|
||||
[ForeignKey("RolId")]
|
||||
public RolSistema Rol { get; set; } = null!;
|
||||
}
|
||||
47
foundation_system/Models/Usuario.cs
Normal file
47
foundation_system/Models/Usuario.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace foundation_system.Models;
|
||||
|
||||
[Table("usuarios")]
|
||||
public class Usuario
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("persona_id")]
|
||||
public long? PersonaId { get; set; }
|
||||
|
||||
[Column("nombre_usuario")]
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
public string NombreUsuario { get; set; } = string.Empty;
|
||||
|
||||
[Column("email")]
|
||||
[Required]
|
||||
[StringLength(255)]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Column("hash_contrasena")]
|
||||
[Required]
|
||||
public string HashContrasena { get; set; } = string.Empty;
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("ultimo_login")]
|
||||
public DateTime? UltimoLogin { get; set; }
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation properties
|
||||
[ForeignKey("PersonaId")]
|
||||
public Persona? Persona { get; set; }
|
||||
|
||||
public ICollection<RolUsuario> RolesUsuario { get; set; } = new List<RolUsuario>();
|
||||
}
|
||||
31
foundation_system/Models/ViewModels/AntecedentesViewModel.cs
Normal file
31
foundation_system/Models/ViewModels/AntecedentesViewModel.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using foundation_system.Models;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
|
||||
namespace foundation_system.Models.ViewModels;
|
||||
|
||||
public class AntecedentesViewModel
|
||||
{
|
||||
[ValidateNever]
|
||||
public Nino? Nino { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
public List<AntecedenteNino> Historial { get; set; } = new();
|
||||
|
||||
// Form fields for new record
|
||||
[Required(ErrorMessage = "La fecha es requerida")]
|
||||
[Display(Name = "Fecha del Incidente")]
|
||||
public DateTime FechaIncidente { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Required(ErrorMessage = "El tipo de antecedente es requerido")]
|
||||
[Display(Name = "Tipo de Antecedente")]
|
||||
public string TipoAntecedente { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "La descripción es requerida")]
|
||||
[Display(Name = "Descripción")]
|
||||
public string Descripcion { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "La gravedad es requerida")]
|
||||
[Display(Name = "Gravedad")]
|
||||
public string Gravedad { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using foundation_system.Models;
|
||||
|
||||
namespace foundation_system.Models.ViewModels;
|
||||
|
||||
public class AsistenciaGridViewModel
|
||||
{
|
||||
public int Año { get; set; }
|
||||
public int Mes { get; set; }
|
||||
public string NombreMes { get; set; } = string.Empty;
|
||||
public string DiasSemanaSeleccionados { get; set; } = string.Empty;
|
||||
public List<DateTime> DiasDelMes { get; set; } = new();
|
||||
public List<Nino> Expedientes { get; set; } = new();
|
||||
public Dictionary<string, string> Asistencias { get; set; } = new(); // Key: "ninoId_yyyy-MM-dd", Value: "P", "T", "F"
|
||||
}
|
||||
89
foundation_system/Models/ViewModels/ExpedienteViewModel.cs
Normal file
89
foundation_system/Models/ViewModels/ExpedienteViewModel.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace foundation_system.Models.ViewModels;
|
||||
|
||||
public class ExpedienteViewModel
|
||||
{
|
||||
public long? Id { get; set; } // Nino Id
|
||||
|
||||
// Persona Data
|
||||
[Required(ErrorMessage = "Los nombres son requeridos")]
|
||||
[Display(Name = "Nombres")]
|
||||
public string Nombres { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Los apellidos son requeridos")]
|
||||
[Display(Name = "Apellidos")]
|
||||
public string Apellidos { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "DUI")]
|
||||
[StringLength(12)]
|
||||
public string? Dui { get; set; }
|
||||
|
||||
[Display(Name = "NIT")]
|
||||
[StringLength(17)]
|
||||
public string? Nit { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "La fecha de nacimiento es requerida")]
|
||||
[Display(Name = "Fecha de Nacimiento")]
|
||||
[DataType(DataType.Date)]
|
||||
public DateOnly? FechaNacimiento { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El género es requerido")]
|
||||
[Display(Name = "Género")]
|
||||
public string? Genero { get; set; }
|
||||
|
||||
[EmailAddress(ErrorMessage = "Correo electrónico inválido")]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Display(Name = "Teléfono")]
|
||||
public string? Telefono { get; set; }
|
||||
|
||||
[Display(Name = "Foto")]
|
||||
public string? FotoUrl { get; set; }
|
||||
|
||||
[Display(Name = "Archivo de Foto")]
|
||||
public IFormFile? FotoFile { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "La dirección es requerida")]
|
||||
[Display(Name = "Dirección")]
|
||||
public string? Direccion { get; set; }
|
||||
|
||||
// Nino Data
|
||||
[Display(Name = "Código de Inscripción")]
|
||||
public string? CodigoInscripcion { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "La fecha de inscripción es requerida")]
|
||||
[Display(Name = "Fecha de Inscripción")]
|
||||
[DataType(DataType.Date)]
|
||||
public DateOnly FechaInscripcion { get; set; } = DateOnly.FromDateTime(DateTime.Now);
|
||||
|
||||
[Required(ErrorMessage = "El estado es requerido")]
|
||||
[Display(Name = "Estado")]
|
||||
public string Estado { get; set; } = "ACTIVO";
|
||||
|
||||
[Required(ErrorMessage = "El nivel/grado es requerido")]
|
||||
[Display(Name = "Nivel/Grado")]
|
||||
public string? NivelGrado { get; set; }
|
||||
|
||||
[Display(Name = "Alergias")]
|
||||
public string? Alergias { get; set; }
|
||||
|
||||
[Display(Name = "Contacto de Emergencia")]
|
||||
public string? ContactoEmergenciaNombre { get; set; }
|
||||
|
||||
[Display(Name = "Teléfono de Emergencia")]
|
||||
public string? ContactoEmergenciaTelefono { get; set; }
|
||||
|
||||
// Parents
|
||||
[Required(ErrorMessage = "El padre es requerido")]
|
||||
public long? PadreId { get; set; }
|
||||
public string? PadreNombre { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "La madre es requerida")]
|
||||
public long? MadreId { get; set; }
|
||||
public string? MadreNombre { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El encargado es requerido")]
|
||||
public long? EncargadoId { get; set; }
|
||||
public string? EncargadoNombre { get; set; }
|
||||
}
|
||||
18
foundation_system/Models/ViewModels/LoginViewModel.cs
Normal file
18
foundation_system/Models/ViewModels/LoginViewModel.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace foundation_system.Models.ViewModels;
|
||||
|
||||
public class LoginViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "El nombre de usuario es requerido")]
|
||||
[Display(Name = "Usuario")]
|
||||
public string NombreUsuario { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "La contraseña es requerida")]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Contraseña")]
|
||||
public string Contrasena { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "Recordarme")]
|
||||
public bool RecordarMe { get; set; }
|
||||
}
|
||||
39
foundation_system/Models/ViewModels/RegisterViewModel.cs
Normal file
39
foundation_system/Models/ViewModels/RegisterViewModel.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace foundation_system.Models.ViewModels;
|
||||
|
||||
public class RegisterViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Los nombres son requeridos")]
|
||||
[StringLength(100, ErrorMessage = "Máximo 100 caracteres")]
|
||||
[Display(Name = "Nombres")]
|
||||
public string Nombres { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Los apellidos son requeridos")]
|
||||
[StringLength(100, ErrorMessage = "Máximo 100 caracteres")]
|
||||
[Display(Name = "Apellidos")]
|
||||
public string Apellidos { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "El nombre de usuario es requerido")]
|
||||
[StringLength(50, MinimumLength = 3, ErrorMessage = "El usuario debe tener entre 3 y 50 caracteres")]
|
||||
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "Solo letras, números y guiones bajos")]
|
||||
[Display(Name = "Nombre de Usuario")]
|
||||
public string NombreUsuario { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "El correo electrónico es requerido")]
|
||||
[EmailAddress(ErrorMessage = "Correo electrónico inválido")]
|
||||
[Display(Name = "Correo Electrónico")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "La contraseña es requerida")]
|
||||
[StringLength(100, MinimumLength = 6, ErrorMessage = "La contraseña debe tener al menos 6 caracteres")]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Contraseña")]
|
||||
public string Contrasena { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Debe confirmar la contraseña")]
|
||||
[Compare("Contrasena", ErrorMessage = "Las contraseñas no coinciden")]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Confirmar Contraseña")]
|
||||
public string ConfirmarContrasena { get; set; } = string.Empty;
|
||||
}
|
||||
42
foundation_system/Models/ViewModels/UsuarioViewModel.cs
Normal file
42
foundation_system/Models/ViewModels/UsuarioViewModel.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace foundation_system.Models.ViewModels;
|
||||
|
||||
public class UsuarioViewModel
|
||||
{
|
||||
public long? Id { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Los nombres son requeridos")]
|
||||
[Display(Name = "Nombres")]
|
||||
public string Nombres { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Los apellidos son requeridos")]
|
||||
[Display(Name = "Apellidos")]
|
||||
public string Apellidos { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "El nombre de usuario es requerido")]
|
||||
[Display(Name = "Nombre de Usuario")]
|
||||
[StringLength(50)]
|
||||
public string NombreUsuario { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "El correo electrónico es requerido")]
|
||||
[EmailAddress(ErrorMessage = "Correo electrónico inválido")]
|
||||
[Display(Name = "Email")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "Contraseña")]
|
||||
[DataType(DataType.Password)]
|
||||
[StringLength(100, MinimumLength = 6, ErrorMessage = "La contraseña debe tener al menos 6 caracteres")]
|
||||
public string? Contrasena { get; set; }
|
||||
|
||||
[Display(Name = "Confirmar Contraseña")]
|
||||
[DataType(DataType.Password)]
|
||||
[Compare("Contrasena", ErrorMessage = "Las contraseñas no coinciden")]
|
||||
public string? ConfirmarContrasena { get; set; }
|
||||
|
||||
[Display(Name = "Estado")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Display(Name = "Teléfono")]
|
||||
public string? Telefono { get; set; }
|
||||
}
|
||||
@@ -14,8 +14,9 @@ builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
|
||||
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
||||
|
||||
// Register authentication service
|
||||
// Register services
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<IAntecedentesService, AntecedentesService>();
|
||||
|
||||
// Configure cookie authentication
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
|
||||
37
foundation_system/Services/AntecedentesService.cs
Normal file
37
foundation_system/Services/AntecedentesService.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using foundation_system.Data;
|
||||
using foundation_system.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace foundation_system.Services;
|
||||
|
||||
public class AntecedentesService : IAntecedentesService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public AntecedentesService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<AntecedenteNino>> GetHistorialByNinoIdAsync(long ninoId)
|
||||
{
|
||||
return await _context.AntecedentesNino
|
||||
.Where(a => a.NinoId == ninoId && a.Activo)
|
||||
.OrderByDescending(a => a.FechaIncidente)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> AddAntecedenteAsync(AntecedenteNino antecedente)
|
||||
{
|
||||
try
|
||||
{
|
||||
_context.AntecedentesNino.Add(antecedente);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
148
foundation_system/Services/AuthService.cs
Normal file
148
foundation_system/Services/AuthService.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using foundation_system.Data;
|
||||
using foundation_system.Models;
|
||||
using foundation_system.Models.ViewModels;
|
||||
using BC = BCrypt.Net.BCrypt;
|
||||
|
||||
namespace foundation_system.Services;
|
||||
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
|
||||
public AuthService(ApplicationDbContext context, ILogger<AuthService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Usuario?> ValidateUserAsync(string username, string password)
|
||||
{
|
||||
var usuario = await _context.Usuarios
|
||||
.Include(u => u.Persona)
|
||||
.Include(u => u.RolesUsuario)
|
||||
.ThenInclude(ru => ru.Rol)
|
||||
.FirstOrDefaultAsync(u => u.NombreUsuario == username && u.Activo);
|
||||
|
||||
if (usuario == null)
|
||||
{
|
||||
_logger.LogWarning("Login attempt for non-existent user: {Username}", username);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify password using BCrypt
|
||||
try
|
||||
{
|
||||
if (!BC.Verify(password, usuario.HashContrasena))
|
||||
{
|
||||
_logger.LogWarning("Invalid password for user: {Username}", username);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error verifying password for user: {Username}", username);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogInformation("User {Username} logged in successfully", username);
|
||||
return usuario;
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Message, Usuario? User)> RegisterUserAsync(RegisterViewModel model)
|
||||
{
|
||||
// Check if username already exists
|
||||
if (await _context.Usuarios.AnyAsync(u => u.NombreUsuario == model.NombreUsuario))
|
||||
{
|
||||
return (false, "El nombre de usuario ya existe", null);
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
if (await _context.Usuarios.AnyAsync(u => u.Email == model.Email))
|
||||
{
|
||||
return (false, "El correo electrónico ya está registrado", null);
|
||||
}
|
||||
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Create persona first
|
||||
var persona = new Persona
|
||||
{
|
||||
Nombres = model.Nombres,
|
||||
Apellidos = model.Apellidos,
|
||||
Email = model.Email,
|
||||
Activo = true,
|
||||
CreadoEn = DateTime.UtcNow,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.Personas.Add(persona);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Create user with hashed password
|
||||
var usuario = new Usuario
|
||||
{
|
||||
PersonaId = persona.Id,
|
||||
NombreUsuario = model.NombreUsuario,
|
||||
Email = model.Email,
|
||||
HashContrasena = BC.HashPassword(model.Contrasena),
|
||||
Activo = true,
|
||||
CreadoEn = DateTime.UtcNow,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.Usuarios.Add(usuario);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Assign default role (LECTOR - reader)
|
||||
var defaultRole = await _context.RolesSistema
|
||||
.FirstOrDefaultAsync(r => r.Codigo == "LECTOR");
|
||||
|
||||
if (defaultRole != null)
|
||||
{
|
||||
var rolUsuario = new RolUsuario
|
||||
{
|
||||
UsuarioId = usuario.Id,
|
||||
RolId = defaultRole.Id,
|
||||
AsignadoEn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.RolesUsuario.Add(rolUsuario);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogInformation("New user registered: {Username}", model.NombreUsuario);
|
||||
return (true, "Usuario registrado exitosamente", usuario);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(ex, "Error registering user: {Username}", model.NombreUsuario);
|
||||
return (false, "Error al registrar el usuario", null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetUserRolesAsync(long userId)
|
||||
{
|
||||
return await _context.RolesUsuario
|
||||
.Where(ru => ru.UsuarioId == userId)
|
||||
.Include(ru => ru.Rol)
|
||||
.Select(ru => ru.Rol.Codigo)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateLastLoginAsync(long userId)
|
||||
{
|
||||
var usuario = await _context.Usuarios.FindAsync(userId);
|
||||
if (usuario != null)
|
||||
{
|
||||
usuario.UltimoLogin = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
foundation_system/Services/IAntecedentesService.cs
Normal file
9
foundation_system/Services/IAntecedentesService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using foundation_system.Models;
|
||||
|
||||
namespace foundation_system.Services;
|
||||
|
||||
public interface IAntecedentesService
|
||||
{
|
||||
Task<List<AntecedenteNino>> GetHistorialByNinoIdAsync(long ninoId);
|
||||
Task<bool> AddAntecedenteAsync(AntecedenteNino antecedente);
|
||||
}
|
||||
27
foundation_system/Services/IAuthService.cs
Normal file
27
foundation_system/Services/IAuthService.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using foundation_system.Models;
|
||||
using foundation_system.Models.ViewModels;
|
||||
|
||||
namespace foundation_system.Services;
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates user credentials and returns the user if valid
|
||||
/// </summary>
|
||||
Task<Usuario?> ValidateUserAsync(string username, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new user
|
||||
/// </summary>
|
||||
Task<(bool Success, string Message, Usuario? User)> RegisterUserAsync(RegisterViewModel model);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the roles for a user
|
||||
/// </summary>
|
||||
Task<List<string>> GetUserRolesAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the last login timestamp
|
||||
/// </summary>
|
||||
Task UpdateLastLoginAsync(long userId);
|
||||
}
|
||||
18
foundation_system/Views/Account/AccessDenied.cshtml
Normal file
18
foundation_system/Views/Account/AccessDenied.cshtml
Normal file
@@ -0,0 +1,18 @@
|
||||
@{
|
||||
ViewData["Title"] = "Acceso Denegado";
|
||||
}
|
||||
|
||||
<div class="container text-center py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-1 fw-bold text-danger">403</h1>
|
||||
<h2 class="mb-4">Acceso Denegado</h2>
|
||||
<p class="text-muted mb-4">
|
||||
No tienes permisos para acceder a esta página.
|
||||
</p>
|
||||
<a asp-controller="Home" asp-action="Index" class="btn btn-primary">
|
||||
Volver al Inicio
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
108
foundation_system/Views/Account/Login.cshtml
Normal file
108
foundation_system/Views/Account/Login.cshtml
Normal file
@@ -0,0 +1,108 @@
|
||||
@model foundation_system.Models.ViewModels.LoginViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Iniciar Sesión";
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>@ViewData["Title"] - MIES</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="~/css/auth.css" asp-append-version="true"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-background">
|
||||
<div class="auth-gradient"></div>
|
||||
<div class="auth-pattern"></div>
|
||||
</div>
|
||||
|
||||
<div class="auth-card animate-fade-in">
|
||||
<div class="auth-header">
|
||||
<div class="auth-logo">
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="45" stroke="currentColor" stroke-width="4"/>
|
||||
<path d="M30 55 L50 35 L70 55" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M40 65 L50 55 L60 65" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="auth-title">MIES</h1>
|
||||
<p class="auth-subtitle">Misión Esperanza</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
@TempData["SuccessMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<form asp-action="Login" method="post" class="auth-form">
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="NombreUsuario" class="form-control" id="username" placeholder="Usuario" autofocus autocomplete="username"/>
|
||||
<label for="username">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
Usuario
|
||||
</label>
|
||||
<span asp-validation-for="NombreUsuario" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Contrasena" class="form-control" id="password" placeholder="Contraseña" autocomplete="current-password"/>
|
||||
<label for="password">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
Contraseña
|
||||
</label>
|
||||
<span asp-validation-for="Contrasena" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-4">
|
||||
<input asp-for="RecordarMe" class="form-check-input" id="rememberMe"/>
|
||||
<label class="form-check-label" for="rememberMe">
|
||||
Recordarme
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-auth">
|
||||
<span>Iniciar Sesión</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>¿No tienes una cuenta?</p>
|
||||
<a asp-action="Register" class="auth-link">Crear cuenta</a>
|
||||
</div>
|
||||
|
||||
<div class="auth-branding">
|
||||
<span>Sistema de Gestión</span>
|
||||
<span class="separator">•</span>
|
||||
<span>Fundación MIES</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
140
foundation_system/Views/Account/Register.cshtml
Normal file
140
foundation_system/Views/Account/Register.cshtml
Normal file
@@ -0,0 +1,140 @@
|
||||
@model foundation_system.Models.ViewModels.RegisterViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Crear Cuenta";
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>@ViewData["Title"] - MIES</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="~/css/auth.css" asp-append-version="true"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-background">
|
||||
<div class="auth-gradient"></div>
|
||||
<div class="auth-pattern"></div>
|
||||
</div>
|
||||
|
||||
<div class="auth-card auth-card-register animate-fade-in">
|
||||
<div class="auth-header">
|
||||
<div class="auth-logo">
|
||||
<div class="logo-icon">
|
||||
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="45" stroke="currentColor" stroke-width="4"/>
|
||||
<path d="M30 55 L50 35 L70 55" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M40 65 L50 55 L60 65" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="auth-title">Crear Cuenta</h1>
|
||||
<p class="auth-subtitle">Únete a MIES - Misión Esperanza</p>
|
||||
</div>
|
||||
|
||||
<form asp-action="Register" method="post" class="auth-form">
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Nombres" class="form-control" id="nombres" placeholder="Nombres" autofocus/>
|
||||
<label for="nombres">Nombres</label>
|
||||
<span asp-validation-for="Nombres" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Apellidos" class="form-control" id="apellidos" placeholder="Apellidos"/>
|
||||
<label for="apellidos">Apellidos</label>
|
||||
<span asp-validation-for="Apellidos" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="NombreUsuario" class="form-control" id="username" placeholder="Nombre de Usuario" autocomplete="username"/>
|
||||
<label for="username">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
Nombre de Usuario
|
||||
</label>
|
||||
<span asp-validation-for="NombreUsuario" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Email" class="form-control" id="email" placeholder="Correo Electrónico" type="email" autocomplete="email"/>
|
||||
<label for="email">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||
<polyline points="22,6 12,13 2,6"/>
|
||||
</svg>
|
||||
Correo Electrónico
|
||||
</label>
|
||||
<span asp-validation-for="Email" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="Contrasena" class="form-control" id="password" placeholder="Contraseña" autocomplete="new-password"/>
|
||||
<label for="password">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
Contraseña
|
||||
</label>
|
||||
<span asp-validation-for="Contrasena" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating mb-3">
|
||||
<input asp-for="ConfirmarContrasena" class="form-control" id="confirmPassword" placeholder="Confirmar Contraseña" autocomplete="new-password"/>
|
||||
<label for="confirmPassword">
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
Confirmar
|
||||
</label>
|
||||
<span asp-validation-for="ConfirmarContrasena" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-auth">
|
||||
<span>Crear Cuenta</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="8.5" cy="7" r="4"/>
|
||||
<line x1="20" y1="8" x2="20" y2="14"/>
|
||||
<line x1="23" y1="11" x2="17" y2="11"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>¿Ya tienes una cuenta?</p>
|
||||
<a asp-action="Login" class="auth-link">Iniciar Sesión</a>
|
||||
</div>
|
||||
|
||||
<div class="auth-branding">
|
||||
<span>Sistema de Gestión</span>
|
||||
<span class="separator">•</span>
|
||||
<span>Fundación MIES</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
89
foundation_system/Views/Antecedentes/Index.cshtml
Normal file
89
foundation_system/Views/Antecedentes/Index.cshtml
Normal file
@@ -0,0 +1,89 @@
|
||||
@model IEnumerable<foundation_system.Models.Nino>
|
||||
@{
|
||||
ViewData["Title"] = "Gestión de Antecedentes";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Antecedentes</h2>
|
||||
<p class="text-muted small mb-0">Seleccione un niño para gestionar su historial de antecedentes y conducta.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-custom mb-4">
|
||||
<form asp-action="Index" method="get" class="row g-3 align-items-center">
|
||||
<div class="col-md-10">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0">
|
||||
<i class="bi bi-search text-muted"></i>
|
||||
</span>
|
||||
<input type="text" name="search" class="form-control border-start-0 ps-0"
|
||||
placeholder="Buscar por código o nombre..." value="@ViewBag.Search" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 d-grid">
|
||||
<button type="submit" class="btn btn-primary">Buscar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Nombre Completo</th>
|
||||
<th>Grado/Nivel</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-muted">No se encontraron niños.</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td class="fw-bold">@item.CodigoInscripcion</td>
|
||||
<td>@item.Persona.NombreCompleto</td>
|
||||
<td>@item.NivelGrado</td>
|
||||
<td>
|
||||
<span class="badge @(item.Estado == "ACTIVO" ? "bg-success" : "bg-secondary")">
|
||||
@item.Estado
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="Manage" asp-route-id="@item.Id" class="btn btn-sm btn-primary" title="Gestionar Antecedentes">
|
||||
<i class="bi bi-journal-text me-1"></i> Gestionar
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (ViewBag.TotalPages > 1)
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<div class="text-muted small">
|
||||
Mostrando página @ViewBag.CurrentPage de @ViewBag.TotalPages
|
||||
</div>
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
@for (int i = 1; i <= ViewBag.TotalPages; i++)
|
||||
{
|
||||
<li class="page-item @(i == ViewBag.CurrentPage ? "active" : "")">
|
||||
<a class="page-link" asp-action="Index" asp-route-page="@i" asp-route-search="@ViewBag.Search">@i</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
138
foundation_system/Views/Antecedentes/Manage.cshtml
Normal file
138
foundation_system/Views/Antecedentes/Manage.cshtml
Normal file
@@ -0,0 +1,138 @@
|
||||
@model foundation_system.Models.ViewModels.AntecedentesViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Historial de Antecedentes - " + Model.Nino.Persona.NombreCompleto;
|
||||
}
|
||||
|
||||
<div class="mb-4">
|
||||
<a asp-action="Index" class="btn btn-link p-0 text-decoration-none mb-2">
|
||||
<i class="bi bi-arrow-left"></i> Volver al listado
|
||||
</a>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2 class="mb-1">@Model.Nino.Persona.NombreCompleto</h2>
|
||||
<p class="text-muted small mb-0">Código: <strong>@Model.Nino.CodigoInscripcion</strong> | Grado: @Model.Nino.NivelGrado</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNuevoAntecedente">
|
||||
<i class="bi bi-plus-circle me-2"></i>Nuevo Registro
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card-custom">
|
||||
<h5 class="card-title mb-4">Historial de Antecedentes</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Tipo</th>
|
||||
<th>Gravedad</th>
|
||||
<th>Descripción</th>
|
||||
<th>Registrado por</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Historial.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-muted">No hay antecedentes registrados para este niño.</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var item in Model.Historial)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.FechaIncidente.ToString("dd/MM/yyyy HH:mm")</td>
|
||||
<td>
|
||||
<span class="badge @(item.TipoAntecedente switch {
|
||||
"LlamadaAtencion" => "bg-info",
|
||||
"Suspension" => "bg-warning",
|
||||
"Castigo" => "bg-danger",
|
||||
_ => "bg-secondary"
|
||||
})">
|
||||
@item.TipoAntecedente
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-@(item.Gravedad switch {
|
||||
"Alta" => "danger",
|
||||
"Media" => "warning",
|
||||
"Baja" => "success",
|
||||
_ => "muted"
|
||||
}) fw-bold">
|
||||
@item.Gravedad
|
||||
</span>
|
||||
</td>
|
||||
<td>@item.Descripcion</td>
|
||||
<td class="small">@item.UsuarioRegistra</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Nuevo Antecedente -->
|
||||
<div class="modal fade" id="modalNuevoAntecedente" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form asp-action="Create" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="ninoId" value="@Model.Nino.Id" />
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Registrar Nuevo Antecedente</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="FechaIncidente" class="form-label"></label>
|
||||
<input asp-for="FechaIncidente" class="form-control" type="datetime-local" />
|
||||
<span asp-validation-for="FechaIncidente" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="TipoAntecedente" class="form-label"></label>
|
||||
<select asp-for="TipoAntecedente" class="form-select">
|
||||
<option value="">Seleccione un tipo...</option>
|
||||
<option value="LlamadaAtencion">Llamada de Atención</option>
|
||||
<option value="Suspension">Suspensión</option>
|
||||
<option value="Castigo">Castigo</option>
|
||||
</select>
|
||||
<span asp-validation-for="TipoAntecedente" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Gravedad" class="form-label"></label>
|
||||
<select asp-for="Gravedad" class="form-select">
|
||||
<option value="">Seleccione gravedad...</option>
|
||||
<option value="Baja">Baja</option>
|
||||
<option value="Media">Media</option>
|
||||
<option value="Alta">Alta</option>
|
||||
</select>
|
||||
<span asp-validation-for="Gravedad" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Descripcion" class="form-label"></label>
|
||||
<textarea asp-for="Descripcion" class="form-control" rows="4" placeholder="Detalle lo sucedido..."></textarea>
|
||||
<span asp-validation-for="Descripcion" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Guardar Registro</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
803
foundation_system/Views/Asistencia/Index.cshtml
Normal file
803
foundation_system/Views/Asistencia/Index.cshtml
Normal file
@@ -0,0 +1,803 @@
|
||||
@model foundation_system.Models.ViewModels.AsistenciaGridViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Control de Asistencia";
|
||||
|
||||
var diasSeleccionadosList = new List<string>();
|
||||
if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados))
|
||||
{
|
||||
diasSeleccionadosList = Model.DiasSemanaSeleccionados
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(d => d.Trim())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var diasAMostrar = Model.DiasDelMes
|
||||
.Where(d => diasSeleccionadosList.Count == 0 ||
|
||||
diasSeleccionadosList.Contains(((int)d.DayOfWeek).ToString()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header text-white d-flex align-items-center" style="background: linear-gradient(180deg, #2c3e50 0%, #1a2530 100%);">
|
||||
<h5 class="mb-0 text-white"><i class="bi bi-funnel"></i> Filtros</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="get" id="filtroForm" class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Año</label>
|
||||
<select name="año" class="form-select" id="selectAnio">
|
||||
@foreach (var año in (List<SelectListItem>)ViewBag.Años)
|
||||
{
|
||||
<option value="@año.Value" selected="@año.Selected">
|
||||
@año.Text
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Mes</label>
|
||||
<select name="mes" class="form-select" id="selectMes">
|
||||
@foreach (var mes in (List<SelectListItem>)ViewBag.Meses)
|
||||
{
|
||||
<option value="@mes.Value" selected="@(mes.Value == Model.Mes.ToString())">
|
||||
@mes.Text
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Días de la semana</label>
|
||||
<div class="dias-semana-checkboxes">
|
||||
@foreach (var dia in (List<SelectListItem>)ViewBag.DiasSemana)
|
||||
{
|
||||
var isChecked = diasSeleccionadosList.Contains(dia.Value);
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input dia-checkbox"
|
||||
type="checkbox"
|
||||
value="@dia.Value"
|
||||
id="dia@(dia.Value)"
|
||||
@(isChecked ? "checked" : "")>
|
||||
<label class="form-check-label" for="dia@(dia.Value)">
|
||||
@dia.Text
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<input type="hidden" name="diasSemana" id="diasSemanaInput"
|
||||
value="@Model.DiasSemanaSeleccionados">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn w-100"
|
||||
style="background: linear-gradient(180deg, #2c3e50 0%, #1a2530 100%); color: white;">
|
||||
<i class="bi bi-search text-white"></i> Filtrar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-calendar-check"></i>
|
||||
Asistencia - @Model.NombreMes @Model.Año
|
||||
<span class="badge bg-light text-dark ms-2">@Model.Expedientes.Count niños</span>
|
||||
</h5>
|
||||
<div>
|
||||
<button class="btn btn-light btn-sm" id="btnExportar">
|
||||
<i class="bi bi-file-earmark-excel"></i> Exportar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 70vh; overflow-y: auto;">
|
||||
<table class="table table-bordered table-hover table-sm" id="tablaAsistencia">
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th class="sticky-left bg-dark text-white border-secondary" style="min-width: 200px;">
|
||||
<div class="text-white">Nombre</div>
|
||||
<div class="small text-light">Edad</div>
|
||||
</th>
|
||||
@foreach (var dia in Model.DiasDelMes)
|
||||
{
|
||||
var diaSemana = ((int)dia.DayOfWeek).ToString();
|
||||
var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true;
|
||||
|
||||
if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nombreDia = dia.ToString("ddd", new System.Globalization.CultureInfo("es-ES"));
|
||||
var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday;
|
||||
|
||||
<th class="text-center text-dark border-secondary @(esFinDeSemana ? "" : "")" style="min-width: 50px;">
|
||||
<div class="text-dark">@dia.Day</div>
|
||||
<div class="small text-muted">@nombreDia</div>
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var expediente in Model.Expedientes)
|
||||
{
|
||||
var nombreCompleto = $"{expediente.Persona.Nombres} {expediente.Persona.Apellidos}".Trim();
|
||||
var edad = 0;
|
||||
if (expediente.Persona.FechaNacimiento.HasValue)
|
||||
{
|
||||
var birthDate = expediente.Persona.FechaNacimiento.Value;
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
edad = today.Year - birthDate.Year;
|
||||
if (birthDate > today.AddYears(-edad))
|
||||
{
|
||||
edad--;
|
||||
}
|
||||
}
|
||||
|
||||
<tr data-expediente-id="@expediente.Id">
|
||||
<td class="sticky-left bg-white" style="min-width: 200px;">
|
||||
<div class="fw-bold">@nombreCompleto</div>
|
||||
<div class="small text-muted">Edad: @edad años</div>
|
||||
</td>
|
||||
|
||||
@foreach (var dia in Model.DiasDelMes)
|
||||
{
|
||||
var diaSemana = ((int)dia.DayOfWeek).ToString();
|
||||
var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true;
|
||||
|
||||
if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = $"{expediente.Id}_{dia:yyyy-MM-dd}";
|
||||
var estadoActual = Model.Asistencias.ContainsKey(key)
|
||||
? Model.Asistencias[key]
|
||||
: "";
|
||||
|
||||
var claseEstado = estadoActual switch
|
||||
{
|
||||
"P" => "celda-presente",
|
||||
"T" => "celda-tarde",
|
||||
"F" => "celda-falta",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday;
|
||||
|
||||
<td class="text-center p-0 celda-asistencia @claseEstado @(esFinDeSemana ? "bg-light" : "")"
|
||||
data-expediente="@expediente.Id"
|
||||
data-fecha="@dia.ToString("yyyy-MM-dd")"
|
||||
style="min-width: 50px;">
|
||||
|
||||
<select class="form-select form-select-sm estado-select border-0 h-100 w-100"
|
||||
data-initial="@estadoActual">
|
||||
<option value=""></option>
|
||||
<option value="P" selected="@(estadoActual == "P")" class="bg-success text-white">P</option>
|
||||
<option value="T" selected="@(estadoActual == "T")" class="bg-warning">T</option>
|
||||
<option value="F" selected="@(estadoActual == "F")" class="bg-danger text-white">F</option>
|
||||
<option value="J" selected="@(estadoActual == "J")" class="bg-info text-white">J</option>
|
||||
<option value="E" selected="@(estadoActual == "E")" class="bg-secondary text-white">E</option>
|
||||
</select>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot class="table-dark">
|
||||
<tr>
|
||||
<td class="sticky-left bg-dark text-white fw-bold">
|
||||
<div>TOTALES POR DÍA</div>
|
||||
<div class="small text-light">P/T/F</div>
|
||||
</td>
|
||||
|
||||
@foreach (var dia in Model.DiasDelMes)
|
||||
{
|
||||
var diaSemana = ((int)dia.DayOfWeek).ToString();
|
||||
var isChecked = Model.DiasSemanaSeleccionados?.Contains(diaSemana) ?? true;
|
||||
|
||||
if (!string.IsNullOrEmpty(Model.DiasSemanaSeleccionados) && !isChecked)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var esFinDeSemana = dia.DayOfWeek == DayOfWeek.Saturday || dia.DayOfWeek == DayOfWeek.Sunday;
|
||||
|
||||
var totalPresente = 0;
|
||||
var totalTarde = 0;
|
||||
var totalFalta = 0;
|
||||
var totalJustificado = 0;
|
||||
var totalEnfermo = 0;
|
||||
|
||||
foreach (var expediente in Model.Expedientes)
|
||||
{
|
||||
var key = $"{expediente.Id}_{dia:yyyy-MM-dd}";
|
||||
if (Model.Asistencias.ContainsKey(key))
|
||||
{
|
||||
var estado = Model.Asistencias[key];
|
||||
switch (estado)
|
||||
{
|
||||
case "P": totalPresente++; break;
|
||||
case "T": totalTarde++; break;
|
||||
case "F": totalFalta++; break;
|
||||
case "J": totalJustificado++; break;
|
||||
case "E": totalEnfermo++; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var totalDia = totalPresente + totalTarde + totalFalta + totalJustificado + totalEnfermo;
|
||||
|
||||
<td class="text-center p-1 @(esFinDeSemana ? "bg-secondary" : "bg-dark") text-white"
|
||||
style="min-width: 65px; font-size: 0.85rem;">
|
||||
<div class="fw-bold mb-1">@totalDia</div>
|
||||
<div class="d-flex justify-content-center gap-1 flex-wrap">
|
||||
<span class="badge bg-success" title="Presentes">@totalPresente</span>
|
||||
<span class="badge bg-warning text-dark" title="Tardes">@totalTarde</span>
|
||||
<span class="badge bg-danger" title="Faltas">@totalFalta</span>
|
||||
<span class="badge bg-info" title="Justificados">@totalJustificado</span>
|
||||
<span class="badge bg-secondary" title="Enfermos">@totalEnfermo</span>
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="leyenda">
|
||||
<span class="badge bg-success me-2">P = Presente</span>
|
||||
<span class="badge bg-warning me-2 text-dark">T = Tarde</span>
|
||||
<span class="badge bg-danger me-2">F = Falto</span>
|
||||
<span class="badge bg-info me-2">J = Justificado</span>
|
||||
<span class="badge bg-secondary me-2">E = Enfermo</span>
|
||||
<span class="text-muted ms-3">(Vacío = No registrado)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<small class="text-muted">
|
||||
Total: @Model.Expedientes.Count niños × @diasAMostrar.Count días =
|
||||
@(Model.Expedientes.Count * diasAMostrar.Count) registros posibles
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="modalCarga" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Cargando...</span>
|
||||
</div>
|
||||
<p class="mt-2">Guardando cambios...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
#estadisticasContainer {
|
||||
min-height: 90px;
|
||||
transition: min-height 0.3s ease;
|
||||
}
|
||||
|
||||
#estadisticasContainer .card {
|
||||
height: 100%;
|
||||
min-height: 90px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#estadisticasContainer .card-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#estadisticasContainer .spinner-border {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
#estadisticasContainer .alert {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 90px;
|
||||
}
|
||||
|
||||
tfoot.table-dark td {
|
||||
vertical-align: middle !important;
|
||||
border-color: #495057 !important;
|
||||
}
|
||||
|
||||
tfoot .badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.25em 0.5em;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
tfoot tr:first-child td {
|
||||
border-top: 2px solid #6c757d;
|
||||
}
|
||||
|
||||
tfoot tr.bg-success td {
|
||||
border-top: 2px solid #198754;
|
||||
}
|
||||
|
||||
tfoot td[colspan] {
|
||||
background: linear-gradient(135deg, #198754 0%, #146c43 100%) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
tfoot td[colspan] .badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4em 0.8em;
|
||||
}
|
||||
|
||||
tfoot td[colspan] .text-light {
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
}
|
||||
|
||||
tfoot.table-dark .sticky-left {
|
||||
background: #343a40 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
tfoot.table-dark td.bg-dark {
|
||||
background-color: #343a40 !important;
|
||||
}
|
||||
|
||||
tfoot.table-dark td.bg-secondary {
|
||||
background-color: #6c757d !important;
|
||||
}
|
||||
|
||||
tfoot.table-dark .text-white,
|
||||
tfoot.table-dark .text-light {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
tfoot.table-dark .text-muted {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.table th, .table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.celda-presente {
|
||||
background-color: #d1e7dd !important;
|
||||
}
|
||||
|
||||
.celda-tarde {
|
||||
background-color: #fff3cd !important;
|
||||
}
|
||||
|
||||
.celda-falta {
|
||||
background-color: #f8d7da !important;
|
||||
}
|
||||
|
||||
.celda-justificado {
|
||||
background-color: #cff4fc !important;
|
||||
}
|
||||
|
||||
.celda-enfermo {
|
||||
background-color: #e2e3e5 !important;
|
||||
}
|
||||
|
||||
.estado-select {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent !important;
|
||||
text-align: center;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.estado-select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.estado-select:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.estado-select.bg-success {
|
||||
background-color: #198754 !important;
|
||||
color: white !important;
|
||||
border-color: #198754 !important;
|
||||
}
|
||||
|
||||
.estado-select.bg-warning {
|
||||
background-color: #ffc107 !important;
|
||||
color: black !important;
|
||||
border-color: #ffc107 !important;
|
||||
}
|
||||
|
||||
.estado-select.bg-danger {
|
||||
background-color: #dc3545 !important;
|
||||
color: white !important;
|
||||
border-color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.estado-select.bg-info {
|
||||
background-color: #0dcaf0 !important;
|
||||
color: white !important;
|
||||
border-color: #0dcaf0 !important;
|
||||
}
|
||||
|
||||
.estado-select.bg-secondary {
|
||||
background-color: #6c757d !important;
|
||||
color: white !important;
|
||||
border-color: #6c757d !important;
|
||||
}
|
||||
|
||||
.estado-select:not([value]):not([value=""]) {
|
||||
background-color: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.celda-asistencia.changed {
|
||||
outline: 2px solid #0d6efd;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.sticky-left {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: white;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.dias-semana-checkboxes {
|
||||
background: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.bg-lunes { background-color: #e3f2fd !important; }
|
||||
.bg-martes { background-color: #f3e5f5 !important; }
|
||||
.bg-miercoles { background-color: #e8f5e8 !important; }
|
||||
.bg-jueves { background-color: #fff3e0 !important; }
|
||||
.bg-viernes { background-color: #fce4ec !important; }
|
||||
.bg-sabado { background-color: #f1f8e9 !important; }
|
||||
.bg-domingo { background-color: #fff8e1 !important; }
|
||||
|
||||
.table-dark.sticky-top {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card .display-6 {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.celda-asistencia:hover {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
.celda-presente:hover {
|
||||
background-color: #c3e6cb !important;
|
||||
}
|
||||
|
||||
.celda-tarde:hover {
|
||||
background-color: #ffeaa7 !important;
|
||||
}
|
||||
|
||||
.celda-falta:hover {
|
||||
background-color: #f5c6cb !important;
|
||||
}
|
||||
|
||||
.celda-justificado:hover {
|
||||
background-color: #b6effb !important;
|
||||
}
|
||||
|
||||
.celda-enfermo:hover {
|
||||
background-color: #d3d6d8 !important;
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
.table-responsive {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.estado-select {
|
||||
padding: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.dias-semana-checkboxes .form-check-inline {
|
||||
display: block;
|
||||
margin-right: 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.celda-asistencia, .estado-select {
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.bg-light {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
.leyenda .badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4em 0.8em;
|
||||
}
|
||||
|
||||
.celda-asistencia {
|
||||
min-width: 65px !important;
|
||||
}
|
||||
|
||||
.table th.text-center {
|
||||
min-width: 65px !important;
|
||||
}
|
||||
|
||||
.sticky-left {
|
||||
min-width: 240px !important;
|
||||
}
|
||||
|
||||
.estado-select {
|
||||
padding: 10px 4px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
cargarEstadisticas();
|
||||
|
||||
$('.dia-checkbox').change(function () {
|
||||
var diasSeleccionados = $('.dia-checkbox:checked').map(function () {
|
||||
return $(this).val();
|
||||
}).get().join(',');
|
||||
$('#diasSemanaInput').val(diasSeleccionados);
|
||||
});
|
||||
|
||||
inicializarColores();
|
||||
|
||||
$('#btnGuardarTodo').click(function () {
|
||||
var cambios = [];
|
||||
var celdasCambiadas = $('.celda-asistencia.changed');
|
||||
|
||||
if (celdasCambiadas.length === 0) {
|
||||
toastr.info('No hay cambios para guardar');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`¿Guardar ${celdasCambiadas.length} cambios?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('#modalCarga').modal('show');
|
||||
|
||||
celdasCambiadas.each(function () {
|
||||
var celda = $(this);
|
||||
var select = celda.find('.estado-select');
|
||||
var expedienteId = celda.data('expediente');
|
||||
var fecha = celda.data('fecha');
|
||||
var estado = select.val();
|
||||
|
||||
cambios.push({
|
||||
expedienteId: expedienteId,
|
||||
fecha: fecha,
|
||||
estado: estado
|
||||
});
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: '@Url.Action("GuardarAsistenciasMasivas", "Asistencia")',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(cambios),
|
||||
success: function (response) {
|
||||
$('#modalCarga').modal('hide');
|
||||
if (response.success) {
|
||||
$('.celda-asistencia').removeClass('changed');
|
||||
$('.estado-select').each(function () {
|
||||
$(this).data('initial', $(this).val());
|
||||
});
|
||||
toastr.success(response.message);
|
||||
cargarEstadisticas();
|
||||
} else {
|
||||
toastr.error(response.message);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
$('#modalCarga').modal('hide');
|
||||
toastr.error('Error al guardar los cambios');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#btnExportar').click(function () {
|
||||
var año = $('#selectAnio').val();
|
||||
var mes = $('#selectMes').val();
|
||||
var diasSemana = $('#diasSemanaInput').val();
|
||||
|
||||
var url = '@Url.Action("ExportarExcel", "Asistencia")' +
|
||||
'?año=' + año +
|
||||
'&mes=' + mes +
|
||||
'&diasSemana=' + diasSemana;
|
||||
|
||||
window.open(url, '_blank');
|
||||
});
|
||||
|
||||
$('#selectAnio, #selectMes').change(function () {
|
||||
$('#filtroForm').submit();
|
||||
});
|
||||
|
||||
aplicarColoresDias();
|
||||
|
||||
$('.estado-select').change(function () {
|
||||
var estado = $(this).val();
|
||||
var celda = $(this).closest('.celda-asistencia');
|
||||
var initial = $(this).data('initial');
|
||||
|
||||
aplicarColorSelect($(this), estado);
|
||||
aplicarColorCelda(celda, estado);
|
||||
|
||||
if (initial !== estado) {
|
||||
celda.addClass('changed');
|
||||
} else {
|
||||
celda.removeClass('changed');
|
||||
}
|
||||
|
||||
actualizarFooterTabla();
|
||||
|
||||
var expedienteId = celda.data('expediente');
|
||||
var fecha = celda.data('fecha');
|
||||
|
||||
guardarAsistencia(expedienteId, fecha, estado, celda);
|
||||
});
|
||||
});
|
||||
|
||||
function aplicarColorCelda(celda, estado) {
|
||||
celda.removeClass('celda-presente celda-tarde celda-falta celda-justificado celda-enfermo');
|
||||
switch (estado) {
|
||||
case 'P': celda.addClass('celda-presente'); break;
|
||||
case 'T': celda.addClass('celda-tarde'); break;
|
||||
case 'F': celda.addClass('celda-falta'); break;
|
||||
case 'J': celda.addClass('celda-justificado'); break;
|
||||
case 'E': celda.addClass('celda-enfermo'); break;
|
||||
}
|
||||
}
|
||||
|
||||
function aplicarColorSelect(select, estado) {
|
||||
select.removeClass('bg-success bg-warning bg-danger bg-info bg-secondary text-white');
|
||||
switch (estado) {
|
||||
case 'P': select.addClass('bg-success text-white'); break;
|
||||
case 'T': select.addClass('bg-warning'); break;
|
||||
case 'F': select.addClass('bg-danger text-white'); break;
|
||||
case 'J': select.addClass('bg-info text-white'); break;
|
||||
case 'E': select.addClass('bg-secondary text-white'); break;
|
||||
}
|
||||
}
|
||||
|
||||
function inicializarColores() {
|
||||
$('.estado-select').each(function () {
|
||||
var estado = $(this).val();
|
||||
var celda = $(this).closest('.celda-asistencia');
|
||||
aplicarColorSelect($(this), estado);
|
||||
aplicarColorCelda(celda, estado);
|
||||
});
|
||||
}
|
||||
|
||||
function guardarAsistencia(expedienteId, fecha, estado, celda) {
|
||||
$.ajax({
|
||||
url: '@Url.Action("GuardarAsistencia", "Asistencia")',
|
||||
type: 'POST',
|
||||
data: {
|
||||
expedienteId: expedienteId,
|
||||
fecha: fecha,
|
||||
estado: estado
|
||||
},
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
celda.removeClass('changed');
|
||||
celda.find('.estado-select').data('initial', estado);
|
||||
toastr.success('Guardado correctamente');
|
||||
actualizarFooterTabla();
|
||||
} else {
|
||||
toastr.error(response.message);
|
||||
var select = celda.find('.estado-select');
|
||||
var initial = select.data('initial');
|
||||
aplicarColorSelect(select, initial);
|
||||
aplicarColorCelda(celda, initial);
|
||||
select.val(initial);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
toastr.error('Error de conexión');
|
||||
var select = celda.find('.estado-select');
|
||||
var initial = select.data('initial');
|
||||
aplicarColorSelect(select, initial);
|
||||
aplicarColorCelda(celda, initial);
|
||||
select.val(initial);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cargarEstadisticas() {
|
||||
// Statistics cards removed from UI
|
||||
actualizarFooterTabla();
|
||||
}
|
||||
|
||||
function actualizarFooterTabla() {
|
||||
$('tfoot tr td').each(function (index) {
|
||||
if (index === 0) return;
|
||||
var td = $(this);
|
||||
var colIndex = td.index();
|
||||
if (td.hasClass('text-center')) {
|
||||
var totalPresente = 0, totalTarde = 0, totalFalta = 0, totalJustificado = 0, totalEnfermo = 0;
|
||||
$('tbody tr').each(function () {
|
||||
var celda = $(this).find('td').eq(colIndex);
|
||||
if (celda.length) {
|
||||
var estado = celda.find('.estado-select').val();
|
||||
if (estado === 'P') totalPresente++;
|
||||
else if (estado === 'T') totalTarde++;
|
||||
else if (estado === 'F') totalFalta++;
|
||||
else if (estado === 'J') totalJustificado++;
|
||||
else if (estado === 'E') totalEnfermo++;
|
||||
}
|
||||
});
|
||||
var totalDia = totalPresente + totalTarde + totalFalta + totalJustificado + totalEnfermo;
|
||||
td.html(`
|
||||
<div class="fw-bold mb-1">${totalDia}</div>
|
||||
<div class="d-flex justify-content-center gap-1 flex-wrap">
|
||||
<span class="badge bg-success" title="Presentes">${totalPresente}</span>
|
||||
<span class="badge bg-warning text-dark" title="Tardes">${totalTarde}</span>
|
||||
<span class="badge bg-danger" title="Faltas">${totalFalta}</span>
|
||||
<span class="badge bg-info" title="Justificados">${totalJustificado}</span>
|
||||
<span class="badge bg-secondary" title="Enfermos">${totalEnfermo}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function aplicarColoresDias() {
|
||||
$('th.text-center').each(function () {
|
||||
var textoDia = $(this).find('.small').text().trim();
|
||||
var claseColor = '';
|
||||
switch (textoDia) {
|
||||
case 'lun': claseColor = 'bg-lunes'; break;
|
||||
case 'mar': claseColor = 'bg-martes'; break;
|
||||
case 'mié': claseColor = 'bg-miercoles'; break;
|
||||
case 'jue': claseColor = 'bg-jueves'; break;
|
||||
case 'vie': claseColor = 'bg-viernes'; break;
|
||||
case 'sáb': claseColor = 'bg-sabado'; break;
|
||||
case 'dom': claseColor = 'bg-domingo'; break;
|
||||
}
|
||||
if (claseColor) $(this).addClass(claseColor);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
73
foundation_system/Views/Configuracion/Edit.cshtml
Normal file
73
foundation_system/Views/Configuracion/Edit.cshtml
Normal file
@@ -0,0 +1,73 @@
|
||||
@model foundation_system.Models.ConfiguracionSistema
|
||||
@{
|
||||
ViewData["Title"] = "Editar Configuración";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Editar Parámetro</h2>
|
||||
<p class="text-muted small mb-0">Modificando la clave: <code>@Model.Clave</code></p>
|
||||
</div>
|
||||
<a asp-action="Index" asp-route-categoria="@Model.Categoria" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card-custom">
|
||||
<form asp-action="Edit" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" asp-for="Id" />
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Descripción</label>
|
||||
<p class="text-muted small">@Model.Descripcion</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label asp-for="Valor" class="form-label fw-bold">Valor</label>
|
||||
|
||||
@if (Model.TipoDato == "BOOLEANO")
|
||||
{
|
||||
<select asp-for="Valor" class="form-select">
|
||||
<option value="true">Activado (True)</option>
|
||||
<option value="false">Desactivado (False)</option>
|
||||
</select>
|
||||
}
|
||||
else if (Model.TipoDato == "HTML" || Model.TipoDato == "JSON")
|
||||
{
|
||||
<textarea asp-for="Valor" class="form-control" rows="8" style="font-family: monospace;"></textarea>
|
||||
}
|
||||
else if (Model.TipoDato == "NUMERO")
|
||||
{
|
||||
<input asp-for="Valor" class="form-control" type="number" />
|
||||
}
|
||||
else if (Model.TipoDato == "FECHA")
|
||||
{
|
||||
<input asp-for="Valor" class="form-control" type="date" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<input asp-for="Valor" class="form-control" />
|
||||
}
|
||||
<span asp-validation-for="Valor" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-5">
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Tipo: <span class="badge bg-light text-dark border">@Model.TipoDato</span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary-custom">
|
||||
<i class="bi bi-save me-2"></i>Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
93
foundation_system/Views/Configuracion/Index.cshtml
Normal file
93
foundation_system/Views/Configuracion/Index.cshtml
Normal file
@@ -0,0 +1,93 @@
|
||||
@model IEnumerable<foundation_system.Models.ConfiguracionSistema>
|
||||
@{
|
||||
ViewData["Title"] = "Configuración del Sistema";
|
||||
var categorias = (List<string>)ViewBag.Categorias;
|
||||
var selectedCategoria = (string)ViewBag.SelectedCategoria;
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Configuración</h2>
|
||||
<p class="text-muted small mb-0">Gestione los parámetros dinámicos y ajustes globales del sistema.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card-custom py-2 px-3">
|
||||
<div class="d-flex gap-2 overflow-auto">
|
||||
<a asp-action="Index" class="btn btn-sm @(string.IsNullOrEmpty(selectedCategoria) ? "btn-primary-custom" : "btn-outline-secondary")">
|
||||
Todas
|
||||
</a>
|
||||
@foreach (var cat in categorias)
|
||||
{
|
||||
<a asp-action="Index" asp-route-categoria="@cat"
|
||||
class="btn btn-sm @(selectedCategoria == cat ? "btn-primary-custom" : "btn-outline-secondary")">
|
||||
@cat
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Clave</th>
|
||||
<th>Valor</th>
|
||||
<th>Categoría / Grupo</th>
|
||||
<th>Descripción</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-muted">No hay configuraciones registradas.</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td class="fw-bold">
|
||||
<code>@item.Clave</code>
|
||||
</td>
|
||||
<td>
|
||||
@if (item.TipoDato == "BOOLEANO")
|
||||
{
|
||||
<span class="badge @(item.Valor?.ToLower() == "true" ? "bg-success" : "bg-danger")">
|
||||
@(item.Valor?.ToLower() == "true" ? "Activado" : "Desactivado")
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-truncate d-inline-block" style="max-width: 200px;">@item.Valor</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark border">@item.Categoria</span>
|
||||
<span class="badge bg-light text-muted border">@item.Grupo</span>
|
||||
</td>
|
||||
<td class="small text-muted">@item.Descripcion</td>
|
||||
<td>
|
||||
@if (item.EsEditable)
|
||||
{
|
||||
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary" title="Editar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bi bi-lock-fill text-muted" title="Solo lectura"></i>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
465
foundation_system/Views/Expediente/Create.cshtml
Normal file
465
foundation_system/Views/Expediente/Create.cshtml
Normal file
@@ -0,0 +1,465 @@
|
||||
@model foundation_system.Models.ViewModels.ExpedienteViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Nuevo Expediente";
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
||||
<style>
|
||||
.gender-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.gender-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.gender-option input {
|
||||
cursor: pointer;
|
||||
}
|
||||
.photo-upload-container {
|
||||
position: relative;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 0 auto 20px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid #dee2e6;
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.photo-upload-container:hover {
|
||||
border-color: #0d6efd;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.photo-upload-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.photo-upload-container .upload-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.photo-upload-container:hover .upload-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
.photo-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 60px;
|
||||
color: #adb5bd;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Nuevo Expediente</h2>
|
||||
<p class="text-muted small mb-0">Complete la información para registrar un nuevo niño en la fundación.</p>
|
||||
</div>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Volver al Listado
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form asp-action="Create" method="post" id="expedienteForm" enctype="multipart/form-data">
|
||||
@Html.AntiForgeryToken()
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Datos Personales -->
|
||||
<div class="col-md-8">
|
||||
<div class="card-custom mb-4">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-person-vcard me-2 text-primary"></i>Datos Personales</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<div class="text-center mb-4">
|
||||
<input type="hidden" asp-for="FotoUrl" id="FotoUrl" />
|
||||
<input asp-for="FotoFile" type="file" id="photoInput" style="display: none;" accept="image/*" onchange="previewPhoto(this)" />
|
||||
<div class="photo-upload-container" onclick="document.getElementById('photoInput').click()">
|
||||
<div id="photoPreview">
|
||||
<img src="/Assets/default_avatar.png" alt="Foto de perfil" id="previewImg" />
|
||||
</div>
|
||||
<div class="upload-overlay">
|
||||
<i class="bi bi-camera"></i> Seleccionar Foto
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">Haga clic para seleccionar una foto</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Nombres" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Nombres" class="form-control" placeholder="Ej. Juan Alberto" required />
|
||||
<span asp-validation-for="Nombres" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Apellidos" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Apellidos" class="form-control" placeholder="Ej. Pérez García" required />
|
||||
<span asp-validation-for="Apellidos" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="FechaNacimiento" class="form-label fw-semibold"></label>
|
||||
<input asp-for="FechaNacimiento" class="form-control" type="date" required />
|
||||
<span asp-validation-for="FechaNacimiento" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Genero" class="form-label fw-semibold"></label>
|
||||
<div class="gender-group">
|
||||
<label class="gender-option">
|
||||
<input type="radio" asp-for="Genero" value="M" required />
|
||||
<span>Masculino</span>
|
||||
</label>
|
||||
<label class="gender-option">
|
||||
<input type="radio" asp-for="Genero" value="F" required />
|
||||
<span>Femenino</span>
|
||||
</label>
|
||||
</div>
|
||||
<span asp-validation-for="Genero" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="Direccion" class="form-label fw-semibold"></label>
|
||||
<textarea asp-for="Direccion" class="form-control" rows="2" placeholder="Dirección completa de residencia" required></textarea>
|
||||
<span asp-validation-for="Direccion" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Padres / Encargados -->
|
||||
<div class="card-custom mb-4">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-people me-2 text-info"></i>Padres / Encargados</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Padre <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" asp-for="PadreId" id="PadreId" required />
|
||||
<input type="text" id="PadreNombreDisplay" class="form-control" placeholder="Seleccionar..." readonly />
|
||||
<button type="button" class="btn btn-outline-primary" onclick="openParentModal('PADRE')">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span asp-validation-for="PadreId" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Madre <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" asp-for="MadreId" id="MadreId" required />
|
||||
<input type="text" id="MadreNombreDisplay" class="form-control" placeholder="Seleccionar..." readonly />
|
||||
<button type="button" class="btn btn-outline-primary" onclick="openParentModal('MADRE')">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span asp-validation-for="MadreId" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Encargado <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" asp-for="EncargadoId" id="EncargadoId" required />
|
||||
<input type="text" id="EncargadoNombreDisplay" class="form-control" placeholder="Seleccionar..." readonly />
|
||||
<button type="button" class="btn btn-outline-primary" onclick="openParentModal('ENCARGADO')">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span asp-validation-for="EncargadoId" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-telephone-outbound me-2 text-danger"></i>Contacto de Emergencia (Opcional)</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-7">
|
||||
<label asp-for="ContactoEmergenciaNombre" class="form-label fw-semibold"></label>
|
||||
<input asp-for="ContactoEmergenciaNombre" class="form-control" placeholder="Nombre del responsable" />
|
||||
<span asp-validation-for="ContactoEmergenciaNombre" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label asp-for="ContactoEmergenciaTelefono" class="form-label fw-semibold"></label>
|
||||
<input asp-for="ContactoEmergenciaTelefono" class="form-control" placeholder="Teléfono de contacto" />
|
||||
<span asp-validation-for="ContactoEmergenciaTelefono" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datos de Inscripción -->
|
||||
<div class="col-md-4">
|
||||
<div class="card-custom mb-4">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-clipboard-check me-2 text-success"></i>Inscripción</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label asp-for="CodigoInscripcion" class="form-label fw-semibold"></label>
|
||||
<input asp-for="CodigoInscripcion" class="form-control fw-bold text-primary" placeholder="Generado automáticamente" disabled />
|
||||
<span class="text-muted extra-small">El código se generará al guardar.</span>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="FechaInscripcion" class="form-label fw-semibold"></label>
|
||||
<input asp-for="FechaInscripcion" class="form-control" type="date" required />
|
||||
<span asp-validation-for="FechaInscripcion" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="NivelGrado" class="form-label fw-semibold"></label>
|
||||
<select asp-for="NivelGrado" class="form-select" required>
|
||||
<option value="">Seleccione grado...</option>
|
||||
<option value="NoEstudia">No estudia</option>
|
||||
<option value="Parvularia">Parvularia</option>
|
||||
<option value="Primer Ciclo">Primer Ciclo</option>
|
||||
<option value="Segundo Ciclo">Segundo Ciclo</option>
|
||||
<option value="Tercer Ciclo">Tercer Ciclo</option>
|
||||
</select>
|
||||
<span asp-validation-for="NivelGrado" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="Estado" class="form-label fw-semibold"></label>
|
||||
<select asp-for="Estado" class="form-select" required>
|
||||
<option value="ACTIVO">Activo</option>
|
||||
<option value="INACTIVO">Inactivo</option>
|
||||
<option value="GRADUADO">Graduado</option>
|
||||
</select>
|
||||
<span asp-validation-for="Estado" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-heart-pulse me-2 text-warning"></i>Salud (Opcional)</h5>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="Alergias" class="form-label fw-semibold"></label>
|
||||
<textarea asp-for="Alergias" class="form-control" rows="3" placeholder="Detalle alergias o condiciones médicas..."></textarea>
|
||||
<span asp-validation-for="Alergias" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-grid">
|
||||
<button type="submit" class="btn btn-primary-custom btn-lg">
|
||||
<i class="bi bi-save me-2"></i>Guardar Expediente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Modal para Seleccionar/Crear Padre/Madre -->
|
||||
<div class="modal fade" id="parentModal" tabindex="-1" aria-labelledby="parentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" id="parentModalLabel">Seleccionar Persona</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="nav nav-tabs mb-3" id="parentTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="search-tab" data-bs-toggle="tab" data-bs-target="#search-pane" type="button" role="tab">Buscar Existente</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="create-tab" data-bs-toggle="tab" data-bs-target="#create-pane" type="button" role="tab">Crear Nuevo</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="parentTabsContent">
|
||||
<!-- Tab Buscar -->
|
||||
<div class="tab-pane fade show active" id="search-pane" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<input type="text" id="searchPersonaInput" class="form-control" placeholder="Buscar por nombre o DUI..." />
|
||||
<button class="btn btn-primary" type="button" onclick="searchPersonas()">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="searchResults" class="list-group">
|
||||
<!-- Resultados de búsqueda aquí -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab Crear -->
|
||||
<div class="tab-pane fade" id="create-pane" role="tabpanel">
|
||||
<form id="createPersonaForm">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Nombres</label>
|
||||
<input type="text" name="Nombres" class="form-control" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Apellidos</label>
|
||||
<input type="text" name="Apellidos" class="form-control" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">DUI</label>
|
||||
<input type="text" name="Dui" class="form-control" placeholder="00000000-0" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Teléfono</label>
|
||||
<input type="text" name="Telefono" class="form-control" placeholder="0000-0000" />
|
||||
</div>
|
||||
<div class="col-md-12 text-end">
|
||||
<button type="button" class="btn btn-success" onclick="createPersona()">
|
||||
<i class="bi bi-plus-circle me-2"></i>Crear y Seleccionar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
||||
|
||||
<script>
|
||||
let currentParentType = '';
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
Toastify({
|
||||
text: message,
|
||||
duration: 3000,
|
||||
close: true,
|
||||
gravity: "top",
|
||||
position: "right",
|
||||
stopOnFocus: true,
|
||||
style: {
|
||||
background: type === 'success' ? "#28a745" : "#dc3545",
|
||||
}
|
||||
}).showToast();
|
||||
}
|
||||
|
||||
function openParentModal(type) {
|
||||
currentParentType = type;
|
||||
let label = 'Seleccionar ';
|
||||
if (type === 'PADRE') label += 'Padre';
|
||||
else if (type === 'MADRE') label += 'Madre';
|
||||
else label += 'Encargado';
|
||||
|
||||
document.getElementById('parentModalLabel').innerText = label;
|
||||
const modal = new bootstrap.Modal(document.getElementById('parentModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async function searchPersonas() {
|
||||
const term = document.getElementById('searchPersonaInput').value;
|
||||
if (term.length < 3) {
|
||||
showToast('Por favor ingrese al menos 3 caracteres', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/Expediente/SearchPersonas?term=${encodeURIComponent(term)}&type=ENCARGADO`);
|
||||
const data = await response.json();
|
||||
|
||||
const resultsDiv = document.getElementById('searchResults');
|
||||
resultsDiv.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
resultsDiv.innerHTML = '<div class="list-group-item text-muted">No se encontraron resultados</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(p => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'list-group-item list-group-item-action';
|
||||
btn.innerText = p.text;
|
||||
btn.onclick = () => selectPersona(p.id, p.text);
|
||||
resultsDiv.appendChild(btn);
|
||||
});
|
||||
} catch (error) {
|
||||
showToast('Error al buscar personas', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function selectPersona(id, name) {
|
||||
if (currentParentType === 'PADRE') {
|
||||
document.getElementById('PadreId').value = id;
|
||||
document.getElementById('PadreNombreDisplay').value = name;
|
||||
} else if (currentParentType === 'MADRE') {
|
||||
document.getElementById('MadreId').value = id;
|
||||
document.getElementById('MadreNombreDisplay').value = name;
|
||||
} else {
|
||||
document.getElementById('EncargadoId').value = id;
|
||||
document.getElementById('EncargadoNombreDisplay').value = name;
|
||||
}
|
||||
|
||||
const modalElement = document.getElementById('parentModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalElement);
|
||||
modal.hide();
|
||||
showToast('Persona seleccionada correctamente');
|
||||
}
|
||||
|
||||
async function createPersona() {
|
||||
const form = document.getElementById('createPersonaForm');
|
||||
const formData = new FormData(form);
|
||||
const persona = {};
|
||||
formData.forEach((value, key) => persona[key] = value);
|
||||
|
||||
if (!persona.Nombres || !persona.Apellidos) {
|
||||
showToast('Nombres y Apellidos son requeridos', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/Expediente/CreatePersona', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
||||
},
|
||||
body: JSON.stringify(persona)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
selectPersona(result.id, result.text);
|
||||
form.reset();
|
||||
showToast('Persona creada y seleccionada');
|
||||
} else {
|
||||
showToast(result.message || 'Error al crear la persona', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Error de conexión al crear persona', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function previewPhoto(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('previewImg').src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
showToast('Foto seleccionada');
|
||||
}
|
||||
}
|
||||
|
||||
// Form validation with Toastify
|
||||
document.getElementById('expedienteForm').addEventListener('submit', function(e) {
|
||||
const form = this;
|
||||
if (!form.checkValidity()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showToast('Por favor complete todos los campos requeridos', 'error');
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
188
foundation_system/Views/Expediente/Details.cshtml
Normal file
188
foundation_system/Views/Expediente/Details.cshtml
Normal file
@@ -0,0 +1,188 @@
|
||||
@model foundation_system.Models.ViewModels.ExpedienteViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Detalles del Expediente";
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
.gender-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.photo-container {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 0 auto 20px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid #dee2e6;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.photo-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.info-value {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.375rem;
|
||||
min-height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Detalles del Expediente</h2>
|
||||
<p class="text-muted small mb-0">Información completa de: <strong>@Model.Nombres @Model.Apellidos</strong></p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-primary">
|
||||
<i class="bi bi-pencil me-2"></i>Editar
|
||||
</a>
|
||||
<a asp-action="Print" asp-route-id="@Model.Id" target="_blank" class="btn btn-primary">
|
||||
<i class="bi bi-printer me-2"></i>Imprimir
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Datos Personales -->
|
||||
<div class="col-md-8">
|
||||
<div class="card-custom mb-4">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-person-vcard me-2 text-primary"></i>Datos Personales</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<div class="text-center mb-4">
|
||||
<div class="photo-container">
|
||||
@if (!string.IsNullOrEmpty(Model.FotoUrl))
|
||||
{
|
||||
<img src="@Model.FotoUrl" alt="Foto de perfil" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/Assets/default_avatar.png" alt="Foto de perfil" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-label">Nombres</div>
|
||||
<div class="info-value">@Model.Nombres</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-label">Apellidos</div>
|
||||
<div class="info-value">@Model.Apellidos</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-label">Fecha de Nacimiento</div>
|
||||
<div class="info-value">@Model.FechaNacimiento?.ToString("dd/MM/yyyy")</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-label">Género</div>
|
||||
<div class="info-value">@(Model.Genero == "M" ? "Masculino" : "Femenino")</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div class="info-label">Dirección</div>
|
||||
<div class="info-value">@Model.Direccion</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Padres / Encargados -->
|
||||
<div class="card-custom mb-4">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-people me-2 text-info"></i>Padres / Encargados</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="info-label">Padre</div>
|
||||
<div class="info-value">@(Model.PadreNombre ?? "No registrado")</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="info-label">Madre</div>
|
||||
<div class="info-value">@(Model.MadreNombre ?? "No registrada")</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="info-label">Encargado Principal</div>
|
||||
<div class="info-value">@(Model.EncargadoNombre ?? "No registrado")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-telephone-outbound me-2 text-danger"></i>Contacto de Emergencia</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-7">
|
||||
<div class="info-label">Nombre del Responsable</div>
|
||||
<div class="info-value">@(Model.ContactoEmergenciaNombre ?? "No registrado")</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="info-label">Teléfono de Contacto</div>
|
||||
<div class="info-value">@(Model.ContactoEmergenciaTelefono ?? "No registrado")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datos de Inscripción -->
|
||||
<div class="col-md-4">
|
||||
<div class="card-custom mb-4">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-clipboard-check me-2 text-success"></i>Inscripción</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<div class="info-label">Código de Inscripción</div>
|
||||
<div class="info-value fw-bold text-primary">@Model.CodigoInscripcion</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div class="info-label">Fecha de Inscripción</div>
|
||||
<div class="info-value">@Model.FechaInscripcion.ToString("dd/MM/yyyy")</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div class="info-label">Nivel / Grado</div>
|
||||
<div class="info-value">@Model.NivelGrado</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<div class="info-label">Estado</div>
|
||||
<div class="info-value">
|
||||
@if (Model.Estado == "ACTIVO")
|
||||
{
|
||||
<span class="badge bg-success">Activo</span>
|
||||
}
|
||||
else if (Model.Estado == "GRADUADO")
|
||||
{
|
||||
<span class="badge bg-primary">Graduado</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-danger">Inactivo</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-heart-pulse me-2 text-warning"></i>Salud</h5>
|
||||
<div class="col-md-12">
|
||||
<div class="info-label">Alergias / Condiciones</div>
|
||||
<div class="info-value" style="min-height: 100px; align-items: flex-start;">@(Model.Alergias ?? "Ninguna registrada")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
473
foundation_system/Views/Expediente/Edit.cshtml
Normal file
473
foundation_system/Views/Expediente/Edit.cshtml
Normal file
@@ -0,0 +1,473 @@
|
||||
@model foundation_system.Models.ViewModels.ExpedienteViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Editar Expediente";
|
||||
}
|
||||
|
||||
@section Styles {
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
||||
<style>
|
||||
.gender-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
.gender-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.gender-option input {
|
||||
cursor: pointer;
|
||||
}
|
||||
.photo-upload-container {
|
||||
position: relative;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 0 auto 20px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 3px solid #dee2e6;
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.photo-upload-container:hover {
|
||||
border-color: #0d6efd;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.photo-upload-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.photo-upload-container .upload-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.photo-upload-container:hover .upload-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
.photo-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 60px;
|
||||
color: #adb5bd;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Editar Expediente</h2>
|
||||
<p class="text-muted small mb-0">Actualice la información del niño(a): <strong>@Model.Nombres @Model.Apellidos</strong></p>
|
||||
</div>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Volver al Listado
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form asp-action="Edit" method="post" id="expedienteForm" enctype="multipart/form-data">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Datos Personales -->
|
||||
<div class="col-md-8">
|
||||
<div class="card-custom mb-4">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-person-vcard me-2 text-primary"></i>Datos Personales</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<div class="text-center mb-4">
|
||||
<input type="hidden" asp-for="FotoUrl" id="FotoUrl" />
|
||||
<input asp-for="FotoFile" type="file" id="photoInput" style="display: none;" accept="image/*" onchange="previewPhoto(this)" />
|
||||
<div class="photo-upload-container" onclick="document.getElementById('photoInput').click()">
|
||||
<div id="photoPreview">
|
||||
@if (!string.IsNullOrEmpty(Model.FotoUrl))
|
||||
{
|
||||
<img src="@Model.FotoUrl" alt="Foto de perfil" id="previewImg" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/Assets/default_avatar.png" alt="Foto de perfil" id="previewImg" />
|
||||
}
|
||||
</div>
|
||||
<div class="upload-overlay">
|
||||
<i class="bi bi-camera"></i> Cambiar Foto
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">Haga clic para cambiar la foto</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Nombres" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Nombres" class="form-control" placeholder="Ej. Juan Alberto" required />
|
||||
<span asp-validation-for="Nombres" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Apellidos" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Apellidos" class="form-control" placeholder="Ej. Pérez García" required />
|
||||
<span asp-validation-for="Apellidos" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="FechaNacimiento" class="form-label fw-semibold"></label>
|
||||
<input asp-for="FechaNacimiento" class="form-control" type="date" required />
|
||||
<span asp-validation-for="FechaNacimiento" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Genero" class="form-label fw-semibold"></label>
|
||||
<div class="gender-group">
|
||||
<label class="gender-option">
|
||||
<input type="radio" asp-for="Genero" value="M" required />
|
||||
<span>Masculino</span>
|
||||
</label>
|
||||
<label class="gender-option">
|
||||
<input type="radio" asp-for="Genero" value="F" required />
|
||||
<span>Femenino</span>
|
||||
</label>
|
||||
</div>
|
||||
<span asp-validation-for="Genero" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="Direccion" class="form-label fw-semibold"></label>
|
||||
<textarea asp-for="Direccion" class="form-control" rows="2" placeholder="Dirección completa de residencia" required></textarea>
|
||||
<span asp-validation-for="Direccion" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Padres / Encargados -->
|
||||
<div class="card-custom mb-4">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-people me-2 text-info"></i>Padres / Encargados</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Padre <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" asp-for="PadreId" id="PadreId" required />
|
||||
<input type="text" id="PadreNombreDisplay" class="form-control" value="@Model.PadreNombre" placeholder="Seleccionar..." readonly />
|
||||
<button type="button" class="btn btn-outline-primary" onclick="openParentModal('PADRE')">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span asp-validation-for="PadreId" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Madre <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" asp-for="MadreId" id="MadreId" required />
|
||||
<input type="text" id="MadreNombreDisplay" class="form-control" value="@Model.MadreNombre" placeholder="Seleccionar..." readonly />
|
||||
<button type="button" class="btn btn-outline-primary" onclick="openParentModal('MADRE')">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span asp-validation-for="MadreId" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-semibold">Encargado <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" asp-for="EncargadoId" id="EncargadoId" required />
|
||||
<input type="text" id="EncargadoNombreDisplay" class="form-control" value="@Model.EncargadoNombre" placeholder="Seleccionar..." readonly />
|
||||
<button type="button" class="btn btn-outline-primary" onclick="openParentModal('ENCARGADO')">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span asp-validation-for="EncargadoId" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-telephone-outbound me-2 text-danger"></i>Contacto de Emergencia (Opcional)</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-7">
|
||||
<label asp-for="ContactoEmergenciaNombre" class="form-label fw-semibold"></label>
|
||||
<input asp-for="ContactoEmergenciaNombre" class="form-control" placeholder="Nombre del responsable" />
|
||||
<span asp-validation-for="ContactoEmergenciaNombre" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label asp-for="ContactoEmergenciaTelefono" class="form-label fw-semibold"></label>
|
||||
<input asp-for="ContactoEmergenciaTelefono" class="form-control" placeholder="Teléfono de contacto" />
|
||||
<span asp-validation-for="ContactoEmergenciaTelefono" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datos de Inscripción -->
|
||||
<div class="col-md-4">
|
||||
<div class="card-custom mb-4">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-clipboard-check me-2 text-success"></i>Inscripción</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label asp-for="CodigoInscripcion" class="form-label fw-semibold"></label>
|
||||
<input asp-for="CodigoInscripcion" class="form-control fw-bold text-muted" readonly />
|
||||
<span class="text-muted extra-small">El código de expediente no es editable.</span>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="FechaInscripcion" class="form-label fw-semibold"></label>
|
||||
<input asp-for="FechaInscripcion" class="form-control" type="date" required />
|
||||
<span asp-validation-for="FechaInscripcion" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="NivelGrado" class="form-label fw-semibold"></label>
|
||||
<select asp-for="NivelGrado" class="form-select" required>
|
||||
<option value="">Seleccione grado...</option>
|
||||
<option value="NoEstudia">No estudia</option>
|
||||
<option value="Parvularia">Parvularia</option>
|
||||
<option value="Primer Ciclo">Primer Ciclo</option>
|
||||
<option value="Segundo Ciclo">Segundo Ciclo</option>
|
||||
<option value="Tercer Ciclo">Tercer Ciclo</option>
|
||||
</select>
|
||||
<span asp-validation-for="NivelGrado" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="Estado" class="form-label fw-semibold"></label>
|
||||
<select asp-for="Estado" class="form-select" required>
|
||||
<option value="ACTIVO">Activo</option>
|
||||
<option value="INACTIVO">Inactivo</option>
|
||||
<option value="GRADUADO">Graduado</option>
|
||||
</select>
|
||||
<span asp-validation-for="Estado" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<h5 class="card-title mb-4"><i class="bi bi-heart-pulse me-2 text-warning"></i>Salud (Opcional)</h5>
|
||||
<div class="col-md-12">
|
||||
<label asp-for="Alergias" class="form-label fw-semibold"></label>
|
||||
<textarea asp-for="Alergias" class="form-control" rows="3" placeholder="Detalle alergias o condiciones médicas..."></textarea>
|
||||
<span asp-validation-for="Alergias" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-grid">
|
||||
<button type="submit" class="btn btn-primary-custom btn-lg">
|
||||
<i class="bi bi-check2-circle me-2"></i>Actualizar Expediente
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Modal para Seleccionar/Crear Padre/Madre -->
|
||||
<div class="modal fade" id="parentModal" tabindex="-1" aria-labelledby="parentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title" id="parentModalLabel">Seleccionar Persona</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul class="nav nav-tabs mb-3" id="parentTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="search-tab" data-bs-toggle="tab" data-bs-target="#search-pane" type="button" role="tab">Buscar Existente</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="create-tab" data-bs-toggle="tab" data-bs-target="#create-pane" type="button" role="tab">Crear Nuevo</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="parentTabsContent">
|
||||
<!-- Tab Buscar -->
|
||||
<div class="tab-pane fade show active" id="search-pane" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<div class="input-group">
|
||||
<input type="text" id="searchPersonaInput" class="form-control" placeholder="Buscar por nombre o DUI..." />
|
||||
<button class="btn btn-primary" type="button" onclick="searchPersonas()">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="searchResults" class="list-group">
|
||||
<!-- Resultados de búsqueda aquí -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab Crear -->
|
||||
<div class="tab-pane fade" id="create-pane" role="tabpanel">
|
||||
<form id="createPersonaForm">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Nombres</label>
|
||||
<input type="text" name="Nombres" class="form-control" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Apellidos</label>
|
||||
<input type="text" name="Apellidos" class="form-control" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">DUI</label>
|
||||
<input type="text" name="Dui" class="form-control" placeholder="00000000-0" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Teléfono</label>
|
||||
<input type="text" name="Telefono" class="form-control" placeholder="0000-0000" />
|
||||
</div>
|
||||
<div class="col-md-12 text-end">
|
||||
<button type="button" class="btn btn-success" onclick="createPersona()">
|
||||
<i class="bi bi-plus-circle me-2"></i>Crear y Seleccionar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
||||
|
||||
<script>
|
||||
let currentParentType = '';
|
||||
|
||||
function showToast(message, type = 'success') {
|
||||
Toastify({
|
||||
text: message,
|
||||
duration: 3000,
|
||||
close: true,
|
||||
gravity: "top",
|
||||
position: "right",
|
||||
stopOnFocus: true,
|
||||
style: {
|
||||
background: type === 'success' ? "#28a745" : "#dc3545",
|
||||
}
|
||||
}).showToast();
|
||||
}
|
||||
|
||||
function openParentModal(type) {
|
||||
currentParentType = type;
|
||||
let label = 'Seleccionar ';
|
||||
if (type === 'PADRE') label += 'Padre';
|
||||
else if (type === 'MADRE') label += 'Madre';
|
||||
else label += 'Encargado';
|
||||
|
||||
document.getElementById('parentModalLabel').innerText = label;
|
||||
const modal = new bootstrap.Modal(document.getElementById('parentModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async function searchPersonas() {
|
||||
const term = document.getElementById('searchPersonaInput').value;
|
||||
if (term.length < 3) {
|
||||
showToast('Por favor ingrese al menos 3 caracteres', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/Expediente/SearchPersonas?term=${encodeURIComponent(term)}&type=ENCARGADO`);
|
||||
const data = await response.json();
|
||||
|
||||
const resultsDiv = document.getElementById('searchResults');
|
||||
resultsDiv.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
resultsDiv.innerHTML = '<div class="list-group-item text-muted">No se encontraron resultados</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(p => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'list-group-item list-group-item-action';
|
||||
btn.innerText = p.text;
|
||||
btn.onclick = () => selectPersona(p.id, p.text);
|
||||
resultsDiv.appendChild(btn);
|
||||
});
|
||||
} catch (error) {
|
||||
showToast('Error al buscar personas', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function selectPersona(id, name) {
|
||||
if (currentParentType === 'PADRE') {
|
||||
document.getElementById('PadreId').value = id;
|
||||
document.getElementById('PadreNombreDisplay').value = name;
|
||||
} else if (currentParentType === 'MADRE') {
|
||||
document.getElementById('MadreId').value = id;
|
||||
document.getElementById('MadreNombreDisplay').value = name;
|
||||
} else {
|
||||
document.getElementById('EncargadoId').value = id;
|
||||
document.getElementById('EncargadoNombreDisplay').value = name;
|
||||
}
|
||||
|
||||
const modalElement = document.getElementById('parentModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalElement);
|
||||
modal.hide();
|
||||
showToast('Persona seleccionada correctamente');
|
||||
}
|
||||
|
||||
async function createPersona() {
|
||||
const form = document.getElementById('createPersonaForm');
|
||||
const formData = new FormData(form);
|
||||
const persona = {};
|
||||
formData.forEach((value, key) => persona[key] = value);
|
||||
|
||||
if (!persona.Nombres || !persona.Apellidos) {
|
||||
showToast('Nombres y Apellidos son requeridos', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/Expediente/CreatePersona', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
||||
},
|
||||
body: JSON.stringify(persona)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
selectPersona(result.id, result.text);
|
||||
form.reset();
|
||||
showToast('Persona creada y seleccionada');
|
||||
} else {
|
||||
showToast(result.message || 'Error al crear la persona', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Error de conexión al crear persona', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function previewPhoto(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('previewImg').src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
showToast('Foto seleccionada');
|
||||
}
|
||||
}
|
||||
|
||||
// Form validation with Toastify
|
||||
document.getElementById('expedienteForm').addEventListener('submit', function(e) {
|
||||
const form = this;
|
||||
if (!form.checkValidity()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showToast('Por favor complete todos los campos requeridos', 'error');
|
||||
}
|
||||
form.classList.add('was-validated');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
172
foundation_system/Views/Expediente/Index.cshtml
Normal file
172
foundation_system/Views/Expediente/Index.cshtml
Normal file
@@ -0,0 +1,172 @@
|
||||
@model IEnumerable<foundation_system.Models.Nino>
|
||||
@{
|
||||
ViewData["Title"] = "Expedientes de Niños";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Expedientes</h2>
|
||||
<p class="text-muted small mb-0">Gestión de registros y expedientes de los niños de la fundación.</p>
|
||||
</div>
|
||||
<a asp-action="Create" class="btn btn-primary-custom">
|
||||
<i class="bi bi-person-plus-fill me-2"></i>Nuevo Expediente
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-custom mb-4">
|
||||
<form asp-action="Index" method="get" class="row g-3 align-items-center">
|
||||
<div class="col-md-10">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-end-0">
|
||||
<i class="bi bi-search text-muted"></i>
|
||||
</span>
|
||||
<input type="text" name="search" class="form-control border-start-0 ps-0"
|
||||
placeholder="Buscar por código, nombre o DUI..." value="@ViewBag.Search" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 d-grid">
|
||||
<button type="submit" class="btn btn-primary">Buscar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Nombre Completo</th>
|
||||
<th>Edad</th>
|
||||
<th>Grado/Nivel</th>
|
||||
<th>Fecha Inscripción</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-4 text-muted">No hay expedientes registrados.</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td class="fw-bold">@item.CodigoInscripcion</td>
|
||||
<td>@item.Persona.NombreCompleto</td>
|
||||
<td>
|
||||
@{
|
||||
if (item.Persona.FechaNacimiento.HasValue)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Now);
|
||||
var age = today.Year - item.Persona.FechaNacimiento.Value.Year;
|
||||
if (item.Persona.FechaNacimiento.Value > today.AddYears(-age)) age--;
|
||||
@age @(age == 1 ? "año" : "años")
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">N/A</span>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
<td>@item.NivelGrado</td>
|
||||
<td>@item.FechaInscripcion.ToShortDateString()</td>
|
||||
<td>
|
||||
<span class="badge @(item.Estado == "ACTIVO" ? "bg-success" : "bg-secondary")">
|
||||
@item.Estado
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-info" title="Ver Detalle">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary" title="Editar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" title="Desactivar"
|
||||
onclick="confirmarDesactivacion(@item.Id, '@item.Persona.NombreCompleto')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (ViewBag.TotalPages > 1)
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<div class="text-muted small">
|
||||
Mostrando página @ViewBag.CurrentPage de @ViewBag.TotalPages
|
||||
</div>
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item @(!ViewBag.HasPreviousPage ? "disabled" : "")">
|
||||
<a class="page-link" asp-action="Index"
|
||||
asp-route-page="@(ViewBag.CurrentPage - 1)"
|
||||
asp-route-search="@ViewBag.Search">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@for (int i = 1; i <= ViewBag.TotalPages; i++)
|
||||
{
|
||||
<li class="page-item @(i == ViewBag.CurrentPage ? "active" : "")">
|
||||
<a class="page-link" asp-action="Index"
|
||||
asp-route-page="@i"
|
||||
asp-route-search="@ViewBag.Search">@i</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
<li class="page-item @(!ViewBag.HasNextPage ? "disabled" : "")">
|
||||
<a class="page-link" asp-action="Index"
|
||||
asp-route-page="@(ViewBag.CurrentPage + 1)"
|
||||
asp-route-search="@ViewBag.Search">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Modal de Confirmación -->
|
||||
<div class="modal fade" id="modalDesactivar" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirmar Desactivación</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
¿Está seguro que desea desactivar el expediente de <strong id="nombreNino"></strong>?
|
||||
<p class="text-muted small mt-2">Esta acción cambiará el estado a INACTIVO y no aparecerá en el listado principal.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<form id="formDesactivar" asp-action="Desactivar" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" id="ninoIdDesactivar" />
|
||||
<button type="submit" class="btn btn-danger">Desactivar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function confirmarDesactivacion(id, nombre) {
|
||||
document.getElementById('nombreNino').innerText = nombre;
|
||||
document.getElementById('ninoIdDesactivar').value = id;
|
||||
new bootstrap.Modal(document.getElementById('modalDesactivar')).show();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
232
foundation_system/Views/Expediente/Print.cshtml
Normal file
232
foundation_system/Views/Expediente/Print.cshtml
Normal file
@@ -0,0 +1,232 @@
|
||||
@model foundation_system.Models.ViewModels.ExpedienteViewModel
|
||||
@{
|
||||
Layout = null;
|
||||
var antecedentes = ViewBag.Antecedentes as List<foundation_system.Models.AntecedenteNino>;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Imprimir Expediente - @Model.CodigoInscripcion</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
@@media print {
|
||||
.no-print { display: none !important; }
|
||||
body { padding: 0; margin: 0; }
|
||||
.container { width: 100% !important; max-width: none !important; }
|
||||
.card { border: none !important; }
|
||||
.badge { border: 1px solid #000; color: #000 !important; background: none !important; }
|
||||
}
|
||||
body {
|
||||
background-color: white;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
.print-header {
|
||||
border-bottom: 2px solid #0d6efd;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.photo-box {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.photo-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.section-title {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.5rem 1rem;
|
||||
border-left: 4px solid #0d6efd;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
.info-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
display: block;
|
||||
}
|
||||
.info-value {
|
||||
font-size: 1rem;
|
||||
color: #212529;
|
||||
}
|
||||
.table-antecedentes {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container my-4">
|
||||
<div class="no-print text-end mb-4">
|
||||
<button onclick="window.print()" class="btn btn-primary">
|
||||
<i class="bi bi-printer me-2"></i>Imprimir Ahora
|
||||
</button>
|
||||
<button onclick="window.close()" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-lg me-2"></i>Cerrar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="print-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h3 mb-0 text-primary">EXPEDIENTE</h1>
|
||||
<p class="text-muted mb-0">Fundación MIES - Sistema de Gestión</p>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="fw-bold">Código: @Model.CodigoInscripcion</div>
|
||||
<div class="small text-muted">Fecha Impresión: @DateTime.Now.ToString("dd/MM/yyyy HH:mm")</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="photo-box mx-auto">
|
||||
@if (!string.IsNullOrEmpty(Model.FotoUrl))
|
||||
{
|
||||
<img src="@Model.FotoUrl" alt="Foto" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/Assets/default_avatar.png" alt="Foto" />
|
||||
}
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<span class="badge @(Model.Estado == "ACTIVO" ? "bg-success" : "bg-secondary")">
|
||||
@Model.Estado
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="section-title">Información Personal</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Nombres</span>
|
||||
<span class="info-value">@Model.Nombres</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Apellidos</span>
|
||||
<span class="info-value">@Model.Apellidos</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Fecha de Nacimiento</span>
|
||||
<span class="info-value">@Model.FechaNacimiento?.ToString("dd/MM/yyyy")</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Género</span>
|
||||
<span class="info-value">@(Model.Genero == "M" ? "Masculino" : "Femenino")</span>
|
||||
</div>
|
||||
<div class="info-item" style="grid-column: span 2;">
|
||||
<span class="info-label">Dirección</span>
|
||||
<span class="info-value">@Model.Direccion</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="section-title">Padres y Encargados</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Padre</span>
|
||||
<span class="info-value">@(Model.PadreNombre ?? "N/A")</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Madre</span>
|
||||
<span class="info-value">@(Model.MadreNombre ?? "N/A")</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Encargado Principal</span>
|
||||
<span class="info-value">@(Model.EncargadoNombre ?? "N/A")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="section-title">Inscripción y Salud</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Fecha de Inscripción</span>
|
||||
<span class="info-value">@Model.FechaInscripcion.ToString("dd/MM/yyyy")</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Nivel / Grado</span>
|
||||
<span class="info-value">@Model.NivelGrado</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Alergias / Condiciones</span>
|
||||
<span class="info-value">@(Model.Alergias ?? "Ninguna")</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Historial de Antecedentes</div>
|
||||
@if (antecedentes != null && antecedentes.Any())
|
||||
{
|
||||
<table class="table table-bordered table-sm table-antecedentes">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Tipo</th>
|
||||
<th>Descripción</th>
|
||||
<th>Gravedad</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in antecedentes)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.FechaIncidente.ToString("dd/MM/yyyy")</td>
|
||||
<td>@item.TipoAntecedente</td>
|
||||
<td>@item.Descripcion</td>
|
||||
<td>
|
||||
<span class="badge @(item.Gravedad == "Alta" ? "bg-danger" : item.Gravedad == "Media" ? "bg-warning text-dark" : "bg-info text-dark")">
|
||||
@item.Gravedad
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted italic">No se registran antecedentes para este niño(a).</p>
|
||||
}
|
||||
|
||||
<div class="mt-5 pt-5">
|
||||
<div class="row text-center">
|
||||
<div class="col-4">
|
||||
<div style="border-top: 1px solid #000; width: 80%; margin: 0 auto;"></div>
|
||||
<div class="small">Firma del Encargado</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div style="border-top: 1px solid #000; width: 80%; margin: 0 auto;"></div>
|
||||
<div class="small">Firma Director(a)</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div style="border-top: 1px solid #000; width: 80%; margin: 0 auto;"></div>
|
||||
<div class="small">Sello de la Fundación</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,6 +6,8 @@
|
||||
<title>@ViewData["Title"] - MIES</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -35,6 +37,9 @@
|
||||
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Asistencia" ? "active" : "")" asp-controller="Asistencia" asp-action="Index">
|
||||
<i class="bi bi-calendar-check"></i> Asistencia
|
||||
</a>
|
||||
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Antecedentes" ? "active" : "")" asp-controller="Antecedentes" asp-action="Index">
|
||||
<i class="bi bi-journal-text"></i> Antecedentes
|
||||
</a>
|
||||
|
||||
<div class="nav-section-title">Administración</div>
|
||||
<a class="nav-link-custom @(ViewContext.RouteData.Values["controller"]?.ToString() == "Usuario" ? "active" : "")" asp-controller="Usuario" asp-action="Index">
|
||||
@@ -79,6 +84,7 @@
|
||||
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
|
||||
78
foundation_system/Views/Usuario/Create.cshtml
Normal file
78
foundation_system/Views/Usuario/Create.cshtml
Normal file
@@ -0,0 +1,78 @@
|
||||
@model foundation_system.Models.ViewModels.UsuarioViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Nuevo Usuario";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Nuevo Usuario</h2>
|
||||
<p class="text-muted small mb-0">Cree una nueva cuenta de acceso administrativo.</p>
|
||||
</div>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card-custom">
|
||||
<form asp-action="Create" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
|
||||
|
||||
<h5 class="mb-4 text-primary border-bottom pb-2">Información Personal</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Nombres" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Nombres" class="form-control" />
|
||||
<span asp-validation-for="Nombres" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Apellidos" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Apellidos" class="form-control" />
|
||||
<span asp-validation-for="Apellidos" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Email" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Telefono" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Telefono" class="form-control" />
|
||||
<span asp-validation-for="Telefono" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-4 text-primary border-bottom pb-2">Credenciales de Acceso</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-12">
|
||||
<label asp-for="NombreUsuario" class="form-label fw-semibold"></label>
|
||||
<input asp-for="NombreUsuario" class="form-control" />
|
||||
<span asp-validation-for="NombreUsuario" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Contrasena" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Contrasena" class="form-control" />
|
||||
<span asp-validation-for="Contrasena" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="ConfirmarContrasena" class="form-label fw-semibold"></label>
|
||||
<input asp-for="ConfirmarContrasena" class="form-control" />
|
||||
<span asp-validation-for="ConfirmarContrasena" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
|
||||
<button type="submit" class="btn btn-primary-custom px-5">
|
||||
<i class="bi bi-save me-2"></i>Crear Usuario
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
95
foundation_system/Views/Usuario/Edit.cshtml
Normal file
95
foundation_system/Views/Usuario/Edit.cshtml
Normal file
@@ -0,0 +1,95 @@
|
||||
@model foundation_system.Models.ViewModels.UsuarioViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Editar Usuario";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Editar Usuario</h2>
|
||||
<p class="text-muted small mb-0">Actualizando perfil de: <strong>@Model.NombreUsuario</strong></p>
|
||||
</div>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card-custom">
|
||||
<form asp-action="Edit" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger mb-3"></div>
|
||||
|
||||
<h5 class="mb-4 text-primary border-bottom pb-2">Información Personal</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Nombres" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Nombres" class="form-control" />
|
||||
<span asp-validation-for="Nombres" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Apellidos" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Apellidos" class="form-control" />
|
||||
<span asp-validation-for="Apellidos" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Email" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Email" class="form-control" />
|
||||
<span asp-validation-for="Email" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Telefono" class="form-label fw-semibold"></label>
|
||||
<input asp-for="Telefono" class="form-control" />
|
||||
<span asp-validation-for="Telefono" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-4 text-primary border-bottom pb-2">Configuración de Cuenta</h5>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="NombreUsuario" class="form-label fw-semibold"></label>
|
||||
<input asp-for="NombreUsuario" class="form-control" />
|
||||
<span asp-validation-for="NombreUsuario" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Activo" class="form-label fw-semibold"></label>
|
||||
<select asp-for="Activo" class="form-select">
|
||||
<option value="true">Activo</option>
|
||||
<option value="false">Inactivo</option>
|
||||
</select>
|
||||
<span asp-validation-for="Activo" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info small">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Deje los campos de contraseña en blanco si no desea cambiarla.
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="Contrasena" class="form-label fw-semibold">Nueva Contraseña</label>
|
||||
<input asp-for="Contrasena" class="form-control" placeholder="Opcional" />
|
||||
<span asp-validation-for="Contrasena" class="text-danger small"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label asp-for="ConfirmarContrasena" class="form-label fw-semibold"></label>
|
||||
<input asp-for="ConfirmarContrasena" class="form-control" placeholder="Opcional" />
|
||||
<span asp-validation-for="ConfirmarContrasena" class="text-danger small"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
|
||||
<button type="submit" class="btn btn-primary-custom px-5">
|
||||
<i class="bi bi-check2-circle me-2"></i>Actualizar Usuario
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
97
foundation_system/Views/Usuario/Index.cshtml
Normal file
97
foundation_system/Views/Usuario/Index.cshtml
Normal file
@@ -0,0 +1,97 @@
|
||||
@model IEnumerable<foundation_system.Models.Usuario>
|
||||
@{
|
||||
ViewData["Title"] = "Gestión de Usuarios";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="mb-1">Usuarios del Sistema</h2>
|
||||
<p class="text-muted small mb-0">Administre las cuentas de acceso y perfiles del personal administrativo.</p>
|
||||
</div>
|
||||
<a asp-action="Create" class="btn btn-primary-custom">
|
||||
<i class="bi bi-person-plus-fill me-2"></i>Nuevo Usuario
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card-custom">
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Usuario</th>
|
||||
<th>Nombre Completo</th>
|
||||
<th>Email</th>
|
||||
<th>Último Acceso</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td class="fw-bold">@item.NombreUsuario</td>
|
||||
<td>@item.Persona.Nombres @item.Persona.Apellidos</td>
|
||||
<td>@item.Email</td>
|
||||
<td class="small text-muted">
|
||||
@(item.UltimoLogin?.ToString("dd/MM/yyyy HH:mm") ?? "Nunca")
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge @(item.Activo ? "bg-success" : "bg-danger")">
|
||||
@(item.Activo ? "Activo" : "Inactivo")
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary" title="Editar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
@if (item.Activo && item.NombreUsuario != User.Identity?.Name)
|
||||
{
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" title="Desactivar"
|
||||
onclick="confirmarDesactivacion(@item.Id, '@item.NombreUsuario')">
|
||||
<i class="bi bi-person-x"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Confirmación -->
|
||||
<div class="modal fade" id="modalDesactivar" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Confirmar Desactivación</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
¿Está seguro que desea revocar el acceso al usuario <strong id="nombreUsuario"></strong>?
|
||||
<p class="text-muted small mt-2">El usuario ya no podrá iniciar sesión en el sistema.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<form id="formDesactivar" asp-action="Desactivar" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" id="usuarioIdDesactivar" />
|
||||
<button type="submit" class="btn btn-danger">Desactivar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function confirmarDesactivacion(id, nombre) {
|
||||
document.getElementById('nombreUsuario').innerText = nombre;
|
||||
document.getElementById('usuarioIdDesactivar').value = id;
|
||||
new bootstrap.Modal(document.getElementById('modalDesactivar')).show();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
954
foundation_system/init_sql.sql
Normal file
954
foundation_system/init_sql.sql
Normal file
@@ -0,0 +1,954 @@
|
||||
-- ============================================
|
||||
-- 0. EXTENSIONES
|
||||
-- ============================================
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
-- ============================================
|
||||
-- 2. TABLA PERSONAS (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE personas (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nombres VARCHAR(100) NOT NULL,
|
||||
apellidos VARCHAR(100) NOT NULL,
|
||||
dui VARCHAR(12) UNIQUE,
|
||||
nit VARCHAR(17) UNIQUE,
|
||||
fecha_nacimiento DATE,
|
||||
genero CHAR(1) CHECK (genero IN ('M', 'F', 'O')),
|
||||
email VARCHAR(255),
|
||||
telefono VARCHAR(20),
|
||||
direccion TEXT,
|
||||
foto_url VARCHAR(255),
|
||||
activo BOOLEAN DEFAULT true,
|
||||
creado_en TIMESTAMPTZ DEFAULT now(),
|
||||
actualizado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE personas IS 'Almacena información personal de todas las personas del sistema';
|
||||
|
||||
-- Índices básicos
|
||||
CREATE INDEX idx_personas_activas ON personas(activo) WHERE activo = true;
|
||||
CREATE INDEX idx_personas_nombre ON personas(nombres, apellidos);
|
||||
|
||||
-- ============================================
|
||||
-- 3. TABLA NIÑOS (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE ninos (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
persona_id BIGINT NOT NULL UNIQUE REFERENCES personas(id),
|
||||
fecha_inscripcion DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
codigo_inscripcion VARCHAR(20) UNIQUE NOT NULL,
|
||||
estado VARCHAR(20) DEFAULT 'ACTIVO'
|
||||
CHECK (estado IN ('ACTIVO', 'INACTIVO', 'GRADUADO')),
|
||||
nivel_grado VARCHAR(20),
|
||||
alergias TEXT,
|
||||
contacto_emergencia_nombre VARCHAR(100),
|
||||
contacto_emergencia_telefono VARCHAR(20),
|
||||
activo BOOLEAN DEFAULT true,
|
||||
creado_en TIMESTAMPTZ DEFAULT now(),
|
||||
actualizado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ninos IS 'Niños registrados en la fundación con información específica';
|
||||
|
||||
-- Índices básicos
|
||||
CREATE INDEX idx_ninos_estado ON ninos(estado);
|
||||
CREATE INDEX idx_ninos_activos ON ninos(activo) WHERE activo = true;
|
||||
|
||||
-- ============================================
|
||||
-- 4. TABLA ENCARGADOS (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE encargados_nino (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nino_id BIGINT NOT NULL REFERENCES ninos(id),
|
||||
persona_id BIGINT NOT NULL REFERENCES personas(id),
|
||||
parentesco VARCHAR(50) NOT NULL DEFAULT 'PADRE'
|
||||
CHECK (parentesco IN ('PADRE', 'MADRE', 'ENCARGADO', 'ABUELO')),
|
||||
es_principal BOOLEAN DEFAULT false,
|
||||
puede_recoger BOOLEAN DEFAULT true,
|
||||
creado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- Restricción para un solo encargado principal por niño
|
||||
CREATE UNIQUE INDEX uq_encargado_principal_nino
|
||||
ON encargados_nino(nino_id)
|
||||
WHERE es_principal = true;
|
||||
|
||||
-- Índices básicos
|
||||
CREATE INDEX idx_encargados_nino ON encargados_nino(nino_id);
|
||||
CREATE INDEX idx_encargados_persona ON encargados_nino(persona_id);
|
||||
|
||||
-- ============================================
|
||||
-- 5. TABLA ASISTENCIA (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE asistencia (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nino_id BIGINT NOT NULL REFERENCES ninos(id),
|
||||
fecha DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
estado VARCHAR(20) NOT NULL DEFAULT 'PRESENTE'
|
||||
CHECK (estado IN ('PRESENTE', 'AUSENTE', 'JUSTIFICADO', 'ENFERMO', 'TARDE')),
|
||||
hora_entrada TIME,
|
||||
hora_salida TIME,
|
||||
notas TEXT,
|
||||
creado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE asistencia IS 'Registro diario de asistencia de los niños';
|
||||
|
||||
-- Índices básicos
|
||||
CREATE INDEX idx_asistencia_fecha ON asistencia(fecha DESC);
|
||||
CREATE INDEX idx_asistencia_nino_fecha ON asistencia(nino_id, fecha DESC);
|
||||
CREATE UNIQUE INDEX uq_asistencia_nino_fecha ON asistencia(nino_id, fecha);
|
||||
-- ============================================
|
||||
-- 6. TABLA ANTECEDENTES (NUEVA)
|
||||
-- ============================================
|
||||
CREATE TABLE antecedentes_nino (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
nino_id BIGINT NOT NULL REFERENCES ninos(id),
|
||||
fecha_incidente TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
tipo_antecedente VARCHAR(50) NOT NULL
|
||||
CHECK (tipo_antecedente IN ('LlamadaAtencion', 'Suspension', 'Castigo')),
|
||||
descripcion TEXT NOT NULL,
|
||||
gravedad VARCHAR(20) NOT NULL
|
||||
CHECK (gravedad IN ('Alta', 'Media', 'Baja')),
|
||||
usuario_registra VARCHAR(100) NOT NULL,
|
||||
activo BOOLEAN DEFAULT true,
|
||||
creado_en TIMESTAMPTZ DEFAULT now(),
|
||||
actualizado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE antecedentes_nino IS 'Historial de antecedentes, incidentes y sanciones de los niños';
|
||||
|
||||
CREATE INDEX idx_antecedentes_nino ON antecedentes_nino(nino_id);
|
||||
CREATE INDEX idx_antecedentes_fecha ON antecedentes_nino(fecha_incidente DESC);
|
||||
|
||||
-- ============================================
|
||||
-- 6. TABLA INVENTARIO (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE categorias_inventario (
|
||||
id SERIAL PRIMARY KEY,
|
||||
nombre VARCHAR(100) NOT NULL,
|
||||
descripcion TEXT,
|
||||
activo BOOLEAN DEFAULT true,
|
||||
creado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE items_inventario (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
categoria_id INTEGER REFERENCES categorias_inventario(id),
|
||||
nombre VARCHAR(200) NOT NULL,
|
||||
descripcion TEXT,
|
||||
unidad VARCHAR(20) DEFAULT 'UNIDAD',
|
||||
stock_actual INTEGER DEFAULT 0,
|
||||
stock_minimo INTEGER DEFAULT 0,
|
||||
ubicacion VARCHAR(100),
|
||||
activo BOOLEAN DEFAULT true,
|
||||
creado_en TIMESTAMPTZ DEFAULT now(),
|
||||
actualizado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE items_inventario IS 'Items del inventario de la fundación';
|
||||
|
||||
-- Índices básicos
|
||||
CREATE INDEX idx_items_inventario_categoria ON items_inventario(categoria_id);
|
||||
CREATE INDEX idx_items_inventario_activos ON items_inventario(activo) WHERE activo = true;
|
||||
|
||||
-- ============================================
|
||||
-- 7. TABLA CAJA CHICA (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE categorias_gasto (
|
||||
id SERIAL PRIMARY KEY,
|
||||
nombre VARCHAR(100) NOT NULL,
|
||||
descripcion TEXT,
|
||||
activo BOOLEAN DEFAULT true,
|
||||
creado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE transacciones_caja (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
fecha_transaccion DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
tipo_transaccion VARCHAR(20) NOT NULL
|
||||
CHECK (tipo_transaccion IN ('INGRESO', 'EGRESO')),
|
||||
categoria_id INTEGER REFERENCES categorias_gasto(id),
|
||||
monto DECIMAL(12,2) NOT NULL CHECK (monto > 0),
|
||||
descripcion VARCHAR(200) NOT NULL,
|
||||
referencia VARCHAR(100),
|
||||
metodo_pago VARCHAR(20) DEFAULT 'EFECTIVO'
|
||||
CHECK (metodo_pago IN ('EFECTIVO', 'CHEQUE', 'TRANSFERENCIA')),
|
||||
creado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE transacciones_caja IS 'Movimientos financieros de la fundación';
|
||||
|
||||
-- Índices básicos
|
||||
CREATE INDEX idx_transacciones_fecha ON transacciones_caja(fecha_transaccion DESC);
|
||||
CREATE INDEX idx_transacciones_tipo ON transacciones_caja(tipo_transaccion);
|
||||
|
||||
-- ============================================
|
||||
-- 8. TABLA USUARIOS (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE usuarios (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
persona_id BIGINT UNIQUE REFERENCES personas(id),
|
||||
nombre_usuario VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
hash_contrasena TEXT NOT NULL,
|
||||
activo BOOLEAN DEFAULT true,
|
||||
ultimo_login TIMESTAMPTZ,
|
||||
creado_en TIMESTAMPTZ DEFAULT now(),
|
||||
actualizado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE usuarios IS 'Usuarios del sistema con credenciales de acceso';
|
||||
|
||||
-- Índices básicos
|
||||
CREATE INDEX idx_usuarios_activos ON usuarios(activo) WHERE activo = true;
|
||||
|
||||
-- ============================================
|
||||
-- 9. TABLA ROLES (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE roles_sistema (
|
||||
id SERIAL PRIMARY KEY,
|
||||
codigo VARCHAR(50) UNIQUE NOT NULL,
|
||||
nombre VARCHAR(100) NOT NULL,
|
||||
descripcion TEXT,
|
||||
creado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE roles_sistema IS 'Roles del sistema para control de acceso (RBAC)';
|
||||
|
||||
-- Roles básicos
|
||||
INSERT INTO roles_sistema (codigo, nombre, descripcion) VALUES
|
||||
('ADMIN', 'Administrador', 'Acceso completo al sistema'),
|
||||
('MAESTRO', 'Maestro', 'Personal docente'),
|
||||
('ENCARGADO', 'Encargado', 'Encargado de niños'),
|
||||
('LECTOR', 'Solo Lectura', 'Acceso de solo lectura');
|
||||
|
||||
-- ============================================
|
||||
-- 10. TABLA PERMISOS (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE permisos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
modulo VARCHAR(50) NOT NULL,
|
||||
codigo VARCHAR(100) UNIQUE NOT NULL,
|
||||
nombre VARCHAR(100) NOT NULL,
|
||||
descripcion TEXT,
|
||||
creado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE permisos IS 'Permisos específicos del sistema organizados por módulos';
|
||||
|
||||
-- Permisos básicos
|
||||
INSERT INTO permisos (modulo, codigo, nombre, descripcion) VALUES
|
||||
('PERSONAS', 'personas.ver', 'Ver Personas', 'Ver lista de personas'),
|
||||
('PERSONAS', 'personas.editar', 'Editar Personas', 'Editar información de personas'),
|
||||
('NIÑOS', 'ninos.ver', 'Ver Niños', 'Ver lista de niños'),
|
||||
('NIÑOS', 'ninos.editar', 'Editar Niños', 'Editar información de niños'),
|
||||
('ASISTENCIA', 'asistencia.ver', 'Ver Asistencia', 'Ver registros de asistencia'),
|
||||
('ASISTENCIA', 'asistencia.marcar', 'Marcar Asistencia', 'Registrar asistencia diaria'),
|
||||
('INVENTARIO', 'inventario.ver', 'Ver Inventario', 'Ver items del inventario'),
|
||||
('INVENTARIO', 'inventario.editar', 'Editar Inventario', 'Editar información de items'),
|
||||
('CAJA', 'caja.ver', 'Ver Caja', 'Ver movimientos de caja'),
|
||||
('CAJA', 'caja.editar', 'Editar Caja', 'Registrar movimientos de caja'),
|
||||
('USUARIOS', 'usuarios.ver', 'Ver Usuarios', 'Ver lista de usuarios'),
|
||||
('USUARIOS', 'usuarios.editar', 'Editar Usuarios', 'Editar información de usuarios');
|
||||
|
||||
-- ============================================
|
||||
-- 11. TABLA ROLES-USUARIO (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE roles_usuario (
|
||||
usuario_id BIGINT NOT NULL REFERENCES usuarios(id),
|
||||
rol_id INTEGER NOT NULL REFERENCES roles_sistema(id),
|
||||
asignado_en TIMESTAMPTZ DEFAULT now(),
|
||||
PRIMARY KEY (usuario_id, rol_id)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- 12. TABLA AUDITORÍA (SIMPLIFICADA)
|
||||
-- ============================================
|
||||
CREATE TABLE registros_auditoria (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
usuario_id BIGINT REFERENCES usuarios(id),
|
||||
accion VARCHAR(50) NOT NULL,
|
||||
entidad VARCHAR(50) NOT NULL,
|
||||
entidad_id BIGINT,
|
||||
descripcion TEXT,
|
||||
creado_en TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE registros_auditoria IS 'Registro de auditoría de todas las acciones importantes del sistema';
|
||||
|
||||
-- Índices básicos
|
||||
CREATE INDEX idx_auditoria_fecha ON registros_auditoria(creado_en DESC);
|
||||
CREATE INDEX idx_auditoria_usuario ON registros_auditoria(usuario_id);
|
||||
|
||||
-- ============================================
|
||||
-- 13. DATOS INICIALES
|
||||
-- ============================================
|
||||
-- Insertar categorías de gasto básicas
|
||||
INSERT INTO categorias_gasto (nombre, descripcion) VALUES
|
||||
('ALIMENTACION', 'Gastos de alimentación'),
|
||||
('EDUCACION', 'Material educativo'),
|
||||
('MANTENIMIENTO', 'Mantenimiento de instalaciones'),
|
||||
('SERVICIOS', 'Servicios públicos'),
|
||||
('SALARIOS', 'Pago de salarios'),
|
||||
('OTROS', 'Otros gastos');
|
||||
|
||||
-- Insertar categorías de inventario básicas
|
||||
INSERT INTO categorias_inventario (nombre, descripcion) VALUES
|
||||
('ALIMENTOS', 'Productos alimenticios'),
|
||||
('MATERIAL_ESCOLAR', 'Material escolar y educativo'),
|
||||
('LIMPIEZA', 'Productos de limpieza'),
|
||||
('OFICINA', 'Material de oficina'),
|
||||
('OTROS', 'Otras categorías');
|
||||
|
||||
-- ============================================
|
||||
-- 14. USUARIO ADMIN POR DEFECTO
|
||||
-- ============================================
|
||||
DO $$
|
||||
DECLARE
|
||||
persona_id BIGINT;
|
||||
usuario_id BIGINT;
|
||||
BEGIN
|
||||
-- Crear persona admin
|
||||
INSERT INTO personas (
|
||||
nombres,
|
||||
apellidos,
|
||||
email,
|
||||
telefono,
|
||||
activo
|
||||
) VALUES (
|
||||
'Administrador',
|
||||
'Sistema',
|
||||
'admin@fundacion.org',
|
||||
'0000-0000',
|
||||
true
|
||||
) RETURNING id INTO persona_id;
|
||||
|
||||
-- Crear usuario admin (contraseña: Admin123!)
|
||||
INSERT INTO usuarios (
|
||||
persona_id,
|
||||
nombre_usuario,
|
||||
email,
|
||||
hash_contrasena,
|
||||
activo
|
||||
) VALUES (
|
||||
persona_id,
|
||||
'admin',
|
||||
'admin@fundacion.org',
|
||||
-- bcrypt hash para 'Admin123!'
|
||||
'$2a$12$Y8vjQJfT6V5UeH5qQ8qB3uK9zLmNpQrS2T3U4V5W6X7Y8Z9A0B1C2D3E4F5G6H7I8',
|
||||
true
|
||||
) RETURNING id INTO usuario_id;
|
||||
|
||||
-- Asignar rol de admin
|
||||
INSERT INTO roles_usuario (usuario_id, rol_id)
|
||||
SELECT usuario_id, id
|
||||
FROM roles_sistema
|
||||
WHERE codigo = 'ADMIN';
|
||||
END
|
||||
$$;
|
||||
|
||||
-- ============================================
|
||||
-- 15. FUNCIÓN PARA ACTUALIZAR TIMESTAMPS
|
||||
-- ============================================
|
||||
CREATE OR REPLACE FUNCTION actualizar_actualizado_en()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.actualizado_en = now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Triggers para actualizar automáticamente
|
||||
CREATE TRIGGER trg_personas_actualizado
|
||||
BEFORE UPDATE ON personas
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION actualizar_actualizado_en();
|
||||
|
||||
CREATE TRIGGER trg_ninos_actualizado
|
||||
BEFORE UPDATE ON ninos
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION actualizar_actualizado_en();
|
||||
|
||||
CREATE TRIGGER trg_items_inventario_actualizado
|
||||
BEFORE UPDATE ON items_inventario
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION actualizar_actualizado_en();
|
||||
|
||||
CREATE TRIGGER trg_usuarios_actualizado
|
||||
BEFORE UPDATE ON usuarios
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION actualizar_actualizado_en();
|
||||
|
||||
-- ============================================
|
||||
-- 16. VISTAS ÚTILES
|
||||
-- ============================================
|
||||
-- Vista para información básica de niños
|
||||
CREATE VIEW vista_ninos_completa AS
|
||||
SELECT
|
||||
n.id,
|
||||
n.codigo_inscripcion,
|
||||
p.nombres,
|
||||
p.apellidos,
|
||||
p.fecha_nacimiento,
|
||||
EXTRACT(YEAR FROM AGE(p.fecha_nacimiento)) as edad,
|
||||
p.genero,
|
||||
n.estado,
|
||||
n.nivel_grado,
|
||||
n.fecha_inscripcion,
|
||||
n.contacto_emergencia_nombre,
|
||||
n.contacto_emergencia_telefono
|
||||
FROM ninos n
|
||||
JOIN personas p ON n.persona_id = p.id
|
||||
WHERE n.activo = true AND p.activo = true;
|
||||
|
||||
-- Vista para asistencia del mes actual
|
||||
CREATE VIEW vista_asistencia_mensual AS
|
||||
SELECT
|
||||
nino_id,
|
||||
DATE_TRUNC('month', fecha) as mes,
|
||||
COUNT(*) as total_dias,
|
||||
COUNT(*) FILTER (WHERE estado = 'PRESENTE') as dias_presente,
|
||||
COUNT(*) FILTER (WHERE estado = 'AUSENTE') as dias_ausente,
|
||||
ROUND(
|
||||
(COUNT(*) FILTER (WHERE estado = 'PRESENTE') * 100.0 /
|
||||
NULLIF(COUNT(*), 0)
|
||||
), 2) as porcentaje_asistencia
|
||||
FROM asistencia
|
||||
GROUP BY nino_id, DATE_TRUNC('month', fecha);
|
||||
|
||||
-- Vista para inventario bajo stock
|
||||
CREATE VIEW vista_inventario_bajo_stock AS
|
||||
SELECT
|
||||
i.id,
|
||||
i.nombre,
|
||||
c.nombre as categoria,
|
||||
i.unidad,
|
||||
i.stock_actual,
|
||||
i.stock_minimo,
|
||||
CASE
|
||||
WHEN i.stock_actual <= 0 THEN 'AGOTADO'
|
||||
WHEN i.stock_actual <= i.stock_minimo THEN 'BAJO'
|
||||
ELSE 'SUFICIENTE'
|
||||
END as estado_stock
|
||||
FROM items_inventario i
|
||||
JOIN categorias_inventario c ON i.categoria_id = c.id
|
||||
WHERE i.activo = true AND (i.stock_actual <= i.stock_minimo OR i.stock_actual <= 0)
|
||||
ORDER BY i.stock_actual ASC;
|
||||
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 17. TABLA CONFIGURACIÓN DEL SISTEMA (DINÁMICA)
|
||||
-- ============================================
|
||||
CREATE TABLE configuracion_sistema (
|
||||
id SERIAL PRIMARY KEY,
|
||||
clave VARCHAR(100) UNIQUE NOT NULL,
|
||||
valor TEXT,
|
||||
tipo_dato VARCHAR(20) DEFAULT 'TEXTO'
|
||||
CHECK (tipo_dato IN ('TEXTO', 'NUMERO', 'BOOLEANO', 'FECHA', 'JSON', 'HTML')),
|
||||
categoria VARCHAR(50) DEFAULT 'GENERAL',
|
||||
grupo VARCHAR(50) DEFAULT 'SISTEMA',
|
||||
descripcion TEXT,
|
||||
es_editable BOOLEAN DEFAULT true,
|
||||
es_publico BOOLEAN DEFAULT false,
|
||||
orden INTEGER DEFAULT 0,
|
||||
opciones JSONB, -- Para campos con opciones predefinidas
|
||||
validacion_regex VARCHAR(200), -- Expresión regular para validación
|
||||
creado_en TIMESTAMPTZ DEFAULT now(),
|
||||
actualizado_en TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE configuracion_sistema IS 'Configuración dinámica del sistema - clave-valor extensible';
|
||||
|
||||
-- Índices para búsqueda eficiente
|
||||
CREATE INDEX idx_configuracion_clave ON configuracion_sistema(clave);
|
||||
CREATE INDEX idx_configuracion_categoria ON configuracion_sistema(categoria);
|
||||
CREATE INDEX idx_configuracion_grupo ON configuracion_sistema(grupo);
|
||||
|
||||
-- ============================================
|
||||
-- 18. VISTAS PARA CONFIGURACIÓN
|
||||
-- ============================================
|
||||
-- Vista para obtener configuración organizada
|
||||
CREATE VIEW vista_configuracion_organizada AS
|
||||
SELECT
|
||||
id,
|
||||
clave,
|
||||
valor,
|
||||
tipo_dato,
|
||||
categoria,
|
||||
grupo,
|
||||
descripcion,
|
||||
es_editable,
|
||||
es_publico,
|
||||
orden,
|
||||
opciones,
|
||||
validacion_regex,
|
||||
creado_en,
|
||||
actualizado_en
|
||||
FROM configuracion_sistema
|
||||
ORDER BY categoria, grupo, orden, clave;
|
||||
|
||||
-- Vista para configuración pública (para APIs o frontend)
|
||||
CREATE VIEW vista_configuracion_publica AS
|
||||
SELECT
|
||||
clave,
|
||||
valor,
|
||||
tipo_dato,
|
||||
categoria,
|
||||
grupo,
|
||||
descripcion
|
||||
FROM configuracion_sistema
|
||||
WHERE es_publico = true
|
||||
ORDER BY categoria, grupo, orden, clave;
|
||||
|
||||
-- ============================================
|
||||
-- 19. FUNCIONES ÚTILES PARA CONFIGURACIÓN
|
||||
-- ============================================
|
||||
-- Función para obtener un valor de configuración con tipo correcto
|
||||
CREATE OR REPLACE FUNCTION obtener_configuracion(
|
||||
p_clave VARCHAR(100),
|
||||
p_valor_default TEXT DEFAULT NULL
|
||||
)
|
||||
RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
v_valor TEXT;
|
||||
BEGIN
|
||||
SELECT valor INTO v_valor
|
||||
FROM configuracion_sistema
|
||||
WHERE clave = p_clave;
|
||||
|
||||
RETURN COALESCE(v_valor, p_valor_default);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Función para establecer configuración (inserta o actualiza)
|
||||
CREATE OR REPLACE FUNCTION establecer_configuracion(
|
||||
p_clave VARCHAR(100),
|
||||
p_valor TEXT,
|
||||
p_tipo_dato VARCHAR(20) DEFAULT 'TEXTO',
|
||||
p_categoria VARCHAR(50) DEFAULT 'GENERAL',
|
||||
p_grupo VARCHAR(50) DEFAULT 'SISTEMA',
|
||||
p_descripcion TEXT DEFAULT NULL,
|
||||
p_es_editable BOOLEAN DEFAULT true,
|
||||
p_es_publico BOOLEAN DEFAULT false,
|
||||
p_orden INTEGER DEFAULT 0
|
||||
)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
INSERT INTO configuracion_sistema (
|
||||
clave, valor, tipo_dato, categoria, grupo,
|
||||
descripcion, es_editable, es_publico, orden
|
||||
) VALUES (
|
||||
p_clave, p_valor, p_tipo_dato, p_categoria, p_grupo,
|
||||
p_descripcion, p_es_editable, p_es_publico, p_orden
|
||||
)
|
||||
ON CONFLICT (clave) DO UPDATE SET
|
||||
valor = EXCLUDED.valor,
|
||||
tipo_dato = EXCLUDED.tipo_dato,
|
||||
categoria = EXCLUDED.categoria,
|
||||
grupo = EXCLUDED.grupo,
|
||||
descripcion = COALESCE(EXCLUDED.descripcion, configuracion_sistema.descripcion),
|
||||
es_editable = EXCLUDED.es_editable,
|
||||
es_publico = EXCLUDED.es_publico,
|
||||
orden = EXCLUDED.orden,
|
||||
actualizado_en = now();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Función para obtener configuración por categoría
|
||||
CREATE OR REPLACE FUNCTION obtener_configuracion_categoria(
|
||||
p_categoria VARCHAR(50)
|
||||
)
|
||||
RETURNS TABLE(
|
||||
clave VARCHAR(100),
|
||||
valor TEXT,
|
||||
tipo_dato VARCHAR(20),
|
||||
grupo VARCHAR(50),
|
||||
descripcion TEXT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
cs.clave,
|
||||
cs.valor,
|
||||
cs.tipo_dato,
|
||||
cs.grupo,
|
||||
cs.descripcion
|
||||
FROM configuracion_sistema cs
|
||||
WHERE cs.categoria = p_categoria
|
||||
ORDER BY cs.grupo, cs.orden, cs.clave;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ============================================
|
||||
-- 20. TRIGGER PARA ACTUALIZAR TIMESTAMP
|
||||
-- ============================================
|
||||
CREATE TRIGGER trg_configuracion_actualizado
|
||||
BEFORE UPDATE ON configuracion_sistema
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION actualizar_actualizado_en();
|
||||
|
||||
-- ============================================
|
||||
-- 21. FUNCIÓN PARA GENERAR CÓDIGO DE INSCRIPCIÓN
|
||||
-- ============================================
|
||||
CREATE OR REPLACE FUNCTION generar_codigo_inscripcion(
|
||||
p_apellidos TEXT,
|
||||
p_anio SMALLINT
|
||||
)
|
||||
RETURNS VARCHAR(20) AS $$
|
||||
DECLARE
|
||||
v_iniciales TEXT := '';
|
||||
v_anio TEXT := LPAD((p_anio % 100)::TEXT, 2, '0');
|
||||
v_correlativo INTEGER;
|
||||
v_ultimo_codigo TEXT;
|
||||
v_parte TEXT;
|
||||
BEGIN
|
||||
-- Validación básica
|
||||
IF p_anio < 2000 OR p_anio > 2100 THEN
|
||||
RAISE EXCEPTION 'Año inválido: %', p_anio;
|
||||
END IF;
|
||||
|
||||
-- Iniciales de apellidos
|
||||
FOR v_parte IN SELECT unnest(string_to_array(p_apellidos, ' ')) LOOP
|
||||
IF v_parte <> '' THEN
|
||||
v_iniciales := v_iniciales || UPPER(LEFT(v_parte, 1));
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- Último correlativo por inicial + año
|
||||
SELECT codigo_inscripcion
|
||||
INTO v_ultimo_codigo
|
||||
FROM ninos
|
||||
WHERE codigo_inscripcion LIKE v_iniciales || v_anio || '___'
|
||||
ORDER BY codigo_inscripcion DESC
|
||||
LIMIT 1;
|
||||
|
||||
IF v_ultimo_codigo IS NOT NULL THEN
|
||||
v_correlativo := RIGHT(v_ultimo_codigo, 3)::INTEGER + 1;
|
||||
ELSE
|
||||
v_correlativo := 1;
|
||||
END IF;
|
||||
|
||||
RETURN v_iniciales || v_anio || LPAD(v_correlativo::TEXT, 3, '0');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- ============================================================================
|
||||
-- MOTOR DE BÚSQUEDA AVANZADO PARA PERSONAS
|
||||
-- ============================================================================
|
||||
-- Características:
|
||||
-- ✅ Normalización (acentos, mayúsculas)
|
||||
-- ✅ Tokenización (búsqueda por palabras individuales)
|
||||
-- ✅ Fuzzy matching (errores ortográficos)
|
||||
-- ✅ Ranking ponderado (diferentes campos, diferentes pesos)
|
||||
-- ✅ Búsqueda en dos fases (índice + CPU)
|
||||
-- ============================================================================
|
||||
|
||||
-- 1️⃣ Asegurar que las extensiones estén activas
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;
|
||||
CREATE EXTENSION IF NOT EXISTS unaccent;
|
||||
|
||||
-- ============================================================================
|
||||
-- 2️⃣ FUNCIÓN AUXILIAR: Normalizar texto
|
||||
-- ============================================================================
|
||||
CREATE OR REPLACE FUNCTION normalizar_texto(texto TEXT)
|
||||
RETURNS TEXT AS $$
|
||||
BEGIN
|
||||
RETURN unaccent(lower(trim(texto)));
|
||||
END;
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
|
||||
-- ============================================================================
|
||||
-- 3️⃣ FUNCIÓN PRINCIPAL: Búsqueda avanzada
|
||||
-- ============================================================================
|
||||
CREATE OR REPLACE FUNCTION buscar_personas_v2(
|
||||
p_termino TEXT,
|
||||
p_tipo TEXT,
|
||||
p_limite INTEGER DEFAULT 10
|
||||
)
|
||||
RETURNS TABLE(
|
||||
id BIGINT,
|
||||
text TEXT,
|
||||
score DOUBLE PRECISION,
|
||||
match_type TEXT -- Para debugging: qué tipo de match fue
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_termino_norm TEXT;
|
||||
v_tokens TEXT[];
|
||||
v_sim_threshold DOUBLE PRECISION := 0.15; -- Más permisivo
|
||||
v_lev_threshold INTEGER := 3; -- Permite más errores
|
||||
|
||||
-- Pesos para ranking (ajusta según importancia)
|
||||
v_peso_nombre DOUBLE PRECISION := 1.0;
|
||||
v_peso_apellido DOUBLE PRECISION := 0.8;
|
||||
v_peso_codigo DOUBLE PRECISION := 0.6;
|
||||
v_peso_dui DOUBLE PRECISION := 0.5;
|
||||
BEGIN
|
||||
-- Normalizar el término de búsqueda
|
||||
v_termino_norm := normalizar_texto(p_termino);
|
||||
|
||||
-- Tokenizar (separar por palabras)
|
||||
v_tokens := string_to_array(v_termino_norm, ' ');
|
||||
|
||||
-- ========================================================================
|
||||
-- BÚSQUEDA PARA NIÑOS
|
||||
-- ========================================================================
|
||||
IF upper(p_tipo) = 'NINO' THEN
|
||||
RETURN QUERY
|
||||
WITH candidatos AS (
|
||||
-- FASE 1: Filtro rápido por índice (operador %)
|
||||
SELECT
|
||||
n.id,
|
||||
p.nombres,
|
||||
p.apellidos,
|
||||
n.codigo_inscripcion,
|
||||
normalizar_texto(p.nombres) AS nombres_norm,
|
||||
normalizar_texto(p.apellidos) AS apellidos_norm,
|
||||
normalizar_texto(n.codigo_inscripcion) AS codigo_norm
|
||||
FROM ninos n
|
||||
JOIN personas p ON p.id = n.persona_id
|
||||
WHERE n.activo = true
|
||||
AND p.activo = true
|
||||
AND (
|
||||
-- Búsqueda rápida con operador % (usa índice GIN)
|
||||
normalizar_texto(p.nombres || ' ' || p.apellidos) % v_termino_norm
|
||||
OR normalizar_texto(n.codigo_inscripcion) % v_termino_norm
|
||||
OR normalizar_texto(p.nombres) % ANY(v_tokens)
|
||||
OR normalizar_texto(p.apellidos) % ANY(v_tokens)
|
||||
)
|
||||
),
|
||||
scoring AS (
|
||||
-- FASE 2: Cálculo preciso de scores
|
||||
SELECT
|
||||
c.id,
|
||||
(c.nombres || ' ' || c.apellidos || ' | Código: ' || c.codigo_inscripcion)::TEXT AS display_text,
|
||||
|
||||
-- Score por similitud de nombre completo
|
||||
(similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) * v_peso_nombre) AS score_nombre_completo,
|
||||
|
||||
-- Score por similitud de nombres individuales
|
||||
(similarity(c.nombres_norm, v_termino_norm) * v_peso_nombre) AS score_nombres,
|
||||
|
||||
-- Score por similitud de apellidos
|
||||
(similarity(c.apellidos_norm, v_termino_norm) * v_peso_apellido) AS score_apellidos,
|
||||
|
||||
-- Score por código
|
||||
(similarity(c.codigo_norm, v_termino_norm) * v_peso_codigo) AS score_codigo,
|
||||
|
||||
-- Score por Levenshtein (nombres)
|
||||
(1.0 - (levenshtein(c.nombres_norm, v_termino_norm)::DOUBLE PRECISION
|
||||
/ GREATEST(length(c.nombres_norm), length(v_termino_norm), 1))) * v_peso_nombre AS score_lev_nombres,
|
||||
|
||||
-- Score por tokens (cada palabra individual)
|
||||
(
|
||||
SELECT COALESCE(MAX(
|
||||
GREATEST(
|
||||
similarity(c.nombres_norm, tok),
|
||||
similarity(c.apellidos_norm, tok)
|
||||
)
|
||||
), 0)
|
||||
FROM unnest(v_tokens) tok
|
||||
) * 0.9 AS score_tokens,
|
||||
|
||||
-- Determinar tipo de match
|
||||
CASE
|
||||
WHEN similarity(c.codigo_norm, v_termino_norm) >= v_sim_threshold THEN 'codigo'
|
||||
WHEN similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) >= v_sim_threshold THEN 'nombre_completo'
|
||||
WHEN levenshtein(c.nombres_norm, v_termino_norm) <= v_lev_threshold THEN 'fuzzy'
|
||||
ELSE 'token'
|
||||
END AS match_type
|
||||
|
||||
FROM candidatos c
|
||||
)
|
||||
SELECT
|
||||
s.id,
|
||||
s.display_text,
|
||||
-- Score final: tomar el máximo de todos los métodos
|
||||
GREATEST(
|
||||
s.score_nombre_completo,
|
||||
s.score_nombres,
|
||||
s.score_apellidos,
|
||||
s.score_codigo,
|
||||
s.score_lev_nombres,
|
||||
s.score_tokens
|
||||
) AS score,
|
||||
s.match_type
|
||||
FROM scoring s
|
||||
WHERE GREATEST(
|
||||
s.score_nombre_completo,
|
||||
s.score_nombres,
|
||||
s.score_apellidos,
|
||||
s.score_codigo,
|
||||
s.score_lev_nombres,
|
||||
s.score_tokens
|
||||
) > 0.1 -- Umbral mínimo para aparecer en resultados
|
||||
ORDER BY score DESC
|
||||
LIMIT p_limite;
|
||||
|
||||
-- ========================================================================
|
||||
-- BÚSQUEDA PARA ENCARGADOS
|
||||
-- ========================================================================
|
||||
ELSIF upper(p_tipo) = 'ENCARGADO' THEN
|
||||
RETURN QUERY
|
||||
WITH candidatos AS (
|
||||
SELECT
|
||||
p.id,
|
||||
p.nombres,
|
||||
p.apellidos,
|
||||
COALESCE(p.dui, '') AS dui,
|
||||
en.parentesco,
|
||||
normalizar_texto(p.nombres) AS nombres_norm,
|
||||
normalizar_texto(p.apellidos) AS apellidos_norm,
|
||||
normalizar_texto(COALESCE(p.dui, '')) AS dui_norm
|
||||
FROM encargados_nino en
|
||||
JOIN personas p ON p.id = en.persona_id
|
||||
WHERE p.activo = true
|
||||
AND (
|
||||
normalizar_texto(p.nombres || ' ' || p.apellidos) % v_termino_norm
|
||||
OR normalizar_texto(COALESCE(p.dui, '')) % v_termino_norm
|
||||
OR normalizar_texto(p.nombres) % ANY(v_tokens)
|
||||
OR normalizar_texto(p.apellidos) % ANY(v_tokens)
|
||||
)
|
||||
),
|
||||
scoring AS (
|
||||
SELECT
|
||||
c.id,
|
||||
(c.nombres || ' ' || c.apellidos || ' (' || c.parentesco || ')')::TEXT AS display_text,
|
||||
|
||||
(similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) * v_peso_nombre) AS score_nombre_completo,
|
||||
(similarity(c.nombres_norm, v_termino_norm) * v_peso_nombre) AS score_nombres,
|
||||
(similarity(c.apellidos_norm, v_termino_norm) * v_peso_apellido) AS score_apellidos,
|
||||
(similarity(c.dui_norm, v_termino_norm) * v_peso_dui) AS score_dui,
|
||||
|
||||
(1.0 - (levenshtein(c.nombres_norm, v_termino_norm)::DOUBLE PRECISION
|
||||
/ GREATEST(length(c.nombres_norm), length(v_termino_norm), 1))) * v_peso_nombre AS score_lev_nombres,
|
||||
|
||||
(
|
||||
SELECT COALESCE(MAX(
|
||||
GREATEST(
|
||||
similarity(c.nombres_norm, tok),
|
||||
similarity(c.apellidos_norm, tok)
|
||||
)
|
||||
), 0)
|
||||
FROM unnest(v_tokens) tok
|
||||
) * 0.9 AS score_tokens,
|
||||
|
||||
CASE
|
||||
WHEN similarity(c.dui_norm, v_termino_norm) >= v_sim_threshold THEN 'dui'
|
||||
WHEN similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) >= v_sim_threshold THEN 'nombre_completo'
|
||||
WHEN levenshtein(c.nombres_norm, v_termino_norm) <= v_lev_threshold THEN 'fuzzy'
|
||||
ELSE 'token'
|
||||
END AS match_type
|
||||
|
||||
FROM candidatos c
|
||||
)
|
||||
SELECT
|
||||
s.id,
|
||||
s.display_text,
|
||||
GREATEST(
|
||||
s.score_nombre_completo,
|
||||
s.score_nombres,
|
||||
s.score_apellidos,
|
||||
s.score_dui,
|
||||
s.score_lev_nombres,
|
||||
s.score_tokens
|
||||
) AS score,
|
||||
s.match_type
|
||||
FROM scoring s
|
||||
WHERE GREATEST(
|
||||
s.score_nombre_completo,
|
||||
s.score_nombres,
|
||||
s.score_apellidos,
|
||||
s.score_dui,
|
||||
s.score_lev_nombres,
|
||||
s.score_tokens
|
||||
) > 0.1
|
||||
ORDER BY score DESC
|
||||
LIMIT p_limite;
|
||||
|
||||
ELSE
|
||||
RAISE EXCEPTION 'Tipo no soportado: %. Use NINO o ENCARGADO', p_tipo;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- ============================================================================
|
||||
-- 4️⃣ ÍNDICES RECOMENDADOS (CRÍTICO PARA PERFORMANCE)
|
||||
-- ============================================================================
|
||||
|
||||
-- Para tabla personas
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_nombres_trgm
|
||||
ON personas USING gin(normalizar_texto(nombres) gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_apellidos_trgm
|
||||
ON personas USING gin(normalizar_texto(apellidos) gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_nombre_completo_trgm
|
||||
ON personas USING gin(normalizar_texto(nombres || ' ' || apellidos) gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_dui_trgm
|
||||
ON personas USING gin(normalizar_texto(dui) gin_trgm_ops)
|
||||
WHERE dui IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_personas_activo
|
||||
ON personas(activo) WHERE activo = true;
|
||||
|
||||
-- Para tabla ninos
|
||||
CREATE INDEX IF NOT EXISTS idx_ninos_codigo_trgm
|
||||
ON ninos USING gin(normalizar_texto(codigo_inscripcion) gin_trgm_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ninos_activo
|
||||
ON ninos(activo) WHERE activo = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ninos_persona_id
|
||||
ON ninos(persona_id);
|
||||
|
||||
-- Para tabla encargados_nino
|
||||
CREATE INDEX IF NOT EXISTS idx_encargados_persona_id
|
||||
ON encargados_nino(persona_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- 5️⃣ EJEMPLOS DE USO
|
||||
-- ============================================================================
|
||||
|
||||
/*
|
||||
-- Búsqueda de niños con errores ortográficos
|
||||
SELECT * FROM buscar_personas_v2('juan albero', 'NINO'); -- encuentra "Juan Alberto"
|
||||
SELECT * FROM buscar_personas_v2('mria', 'NINO'); -- encuentra "María"
|
||||
SELECT * FROM buscar_personas_v2('jose', 'NINO'); -- encuentra "José"
|
||||
|
||||
-- Búsqueda por código parcial
|
||||
SELECT * FROM buscar_personas_v2('2024', 'NINO');
|
||||
|
||||
-- Búsqueda de encargados
|
||||
SELECT * FROM buscar_personas_v2('ana garcia', 'ENCARGADO');
|
||||
SELECT * FROM buscar_personas_v2('12345678', 'ENCARGADO'); -- por DUI
|
||||
|
||||
-- Con límite personalizado
|
||||
SELECT * FROM buscar_personas_v2('juan', 'NINO', 20);
|
||||
*/
|
||||
|
||||
-- ============================================================================
|
||||
-- 6️⃣ ESTADÍSTICAS Y DEBUGGING
|
||||
-- ============================================================================
|
||||
|
||||
-- Ver distribución de tipos de match
|
||||
CREATE OR REPLACE FUNCTION analizar_busqueda(p_termino TEXT, p_tipo TEXT)
|
||||
RETURNS TABLE(
|
||||
match_type TEXT,
|
||||
cantidad BIGINT,
|
||||
score_promedio DOUBLE PRECISION
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
b.match_type,
|
||||
COUNT(*) AS cantidad,
|
||||
AVG(b.score) AS score_promedio
|
||||
FROM buscar_personas_v2(p_termino, p_tipo, 100) b
|
||||
GROUP BY b.match_type
|
||||
ORDER BY cantidad DESC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Ejemplo: SELECT * FROM analizar_busqueda('juan', 'NINO');
|
||||
BIN
foundation_system/wwwroot/Assets/default_avatar.png
Normal file
BIN
foundation_system/wwwroot/Assets/default_avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
472
foundation_system/wwwroot/css/auth.css
Normal file
472
foundation_system/wwwroot/css/auth.css
Normal file
@@ -0,0 +1,472 @@
|
||||
/* ===============================================
|
||||
MIES - Authentication Styles
|
||||
Premium glassmorphism design with animations
|
||||
=============================================== */
|
||||
|
||||
/* CSS Variables */
|
||||
:root {
|
||||
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
--accent-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
|
||||
--primary-color: #667eea;
|
||||
--primary-dark: #5a67d8;
|
||||
--secondary-color: #764ba2;
|
||||
--accent-color: #4facfe;
|
||||
|
||||
--glass-bg: rgba(255, 255, 255, 0.15);
|
||||
--glass-border: rgba(255, 255, 255, 0.25);
|
||||
--glass-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
|
||||
|
||||
--text-primary: #1a1a2e;
|
||||
--text-secondary: #4a4a68;
|
||||
--text-light: #8888a4;
|
||||
|
||||
--font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
sans-serif;
|
||||
|
||||
--border-radius: 16px;
|
||||
--border-radius-sm: 12px;
|
||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Auth Container */
|
||||
.auth-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Background Effects */
|
||||
.auth-background {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-gradient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--primary-gradient);
|
||||
}
|
||||
|
||||
.auth-gradient::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
animation: pulse 15s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.auth-pattern {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: radial-gradient(
|
||||
circle at 20% 80%,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 20%,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 40% 40%,
|
||||
rgba(255, 255, 255, 0.05) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
}
|
||||
|
||||
/* Floating orbs */
|
||||
.auth-background::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 50%;
|
||||
top: -100px;
|
||||
right: -100px;
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Auth Card - Glassmorphism */
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-card-register {
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.auth-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: var(--primary-gradient);
|
||||
}
|
||||
|
||||
/* Auth Header */
|
||||
.auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0 auto;
|
||||
padding: 12px;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
box-shadow: 0 10px 30px -5px rgba(102, 126, 234, 0.5);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.logo-icon:hover {
|
||||
transform: scale(1.05) rotate(5deg);
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 4px;
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.auth-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.auth-form {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-floating {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-floating > .form-control {
|
||||
height: 56px;
|
||||
padding: 16px;
|
||||
padding-left: 16px;
|
||||
border: 2px solid #e8e8f0;
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
background: rgba(248, 248, 252, 0.8);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.form-floating > .form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-floating > .form-control::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.form-floating > label {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
pointer-events: none;
|
||||
border: 2px solid transparent;
|
||||
transform-origin: 0 0;
|
||||
transition: var(--transition);
|
||||
color: var(--text-light);
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.form-floating > .form-control:focus ~ label,
|
||||
.form-floating > .form-control:not(:placeholder-shown) ~ label {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.85) translateY(-0.75rem) translateX(0.15rem);
|
||||
background: white;
|
||||
padding: 0 8px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Check input */
|
||||
.form-check-input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.form-check-input:focus {
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Auth Button */
|
||||
.btn-auth {
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
background: var(--primary-gradient);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-auth:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-auth:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-auth svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-auth:hover svg {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* Auth Footer */
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.auth-footer p {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
color: var(--secondary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Auth Branding */
|
||||
.auth-branding {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.auth-branding .separator {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Alert Styles */
|
||||
.alert {
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-size: 0.875rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(72, 187, 120, 0.1) 0%,
|
||||
rgba(56, 178, 172, 0.1) 100%
|
||||
);
|
||||
color: #276749;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(245, 101, 101, 0.1) 0%,
|
||||
rgba(237, 100, 166, 0.1) 100%
|
||||
);
|
||||
color: #c53030;
|
||||
}
|
||||
|
||||
/* Validation Styles */
|
||||
.text-danger {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-validation-error {
|
||||
border-color: #e53e3e !important;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-30px, 30px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media (max-width: 576px) {
|
||||
.auth-card {
|
||||
padding: 28px 24px;
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.auth-card-register .row > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Row helpers for registration */
|
||||
.auth-form .row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
|
||||
.auth-form .row > .col-md-6 {
|
||||
padding: 0 8px;
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.auth-form .row > .col-md-6 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
foundation_system/wwwroot/uploads/fotos/SA25001.jpg
Normal file
BIN
foundation_system/wwwroot/uploads/fotos/SA25001.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Reference in New Issue
Block a user