cinu
This commit is contained in:
15
RS_system/.idea/.idea.RS_system/.idea/.gitignore
generated
vendored
Normal file
15
RS_system/.idea/.idea.RS_system/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/contentModel.xml
|
||||
/projectSettingsUpdater.xml
|
||||
/.idea.RS_system.iml
|
||||
/modules.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
4
RS_system/.idea/.idea.RS_system/.idea/encodings.xml
generated
Normal file
4
RS_system/.idea/.idea.RS_system/.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
8
RS_system/.idea/.idea.RS_system/.idea/indexLayout.xml
generated
Normal file
8
RS_system/.idea/.idea.RS_system/.idea/indexLayout.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
6
RS_system/.idea/.idea.RS_system/.idea/vcs.xml
generated
Normal file
6
RS_system/.idea/.idea.RS_system/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
177
RS_system/Controllers/DiezmoCatalogoController.cs
Normal file
177
RS_system/Controllers/DiezmoCatalogoController.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Filters;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Models.ViewModels.Catalogos;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[Permission("Diezmo.Index")] // Requiere permisos base del módulo
|
||||
public class DiezmoCatalogoController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public DiezmoCatalogoController(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private string UsuarioActual() => User.FindFirst(ClaimTypes.Name)?.Value ?? User.Identity?.Name ?? "Sistema";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Tipos de Salida
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
public async Task<IActionResult> TiposSalida()
|
||||
{
|
||||
var lista = await _context.DiezmoTiposSalida
|
||||
.Where(x => !x.Eliminado)
|
||||
.OrderBy(x => x.Nombre)
|
||||
.ToListAsync();
|
||||
return View(lista);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> GuardarTipoSalida(TipoSalidaViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
TempData["ErrorMessage"] = "Datos inválidos: " + string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage));
|
||||
return RedirectToAction(nameof(TiposSalida));
|
||||
}
|
||||
|
||||
if (vm.Id == 0) // Crear
|
||||
{
|
||||
var nuevo = new DiezmoTipoSalida
|
||||
{
|
||||
Nombre = vm.Nombre,
|
||||
Descripcion = vm.Descripcion,
|
||||
EsEntregaPastor = vm.EsEntregaPastor,
|
||||
CreadoPor = UsuarioActual(),
|
||||
CreadoEn = DateTime.UtcNow
|
||||
};
|
||||
_context.DiezmoTiposSalida.Add(nuevo);
|
||||
TempData["SuccessMessage"] = "Tipo de salida creado.";
|
||||
}
|
||||
else // Editar
|
||||
{
|
||||
var dbItem = await _context.DiezmoTiposSalida.FindAsync(vm.Id);
|
||||
if (dbItem == null || dbItem.Eliminado) return NotFound();
|
||||
|
||||
dbItem.Nombre = vm.Nombre;
|
||||
dbItem.Descripcion = vm.Descripcion;
|
||||
dbItem.EsEntregaPastor = vm.EsEntregaPastor;
|
||||
dbItem.ActualizadoEn = DateTime.UtcNow;
|
||||
_context.Update(dbItem);
|
||||
|
||||
TempData["SuccessMessage"] = "Tipo de salida actualizado.";
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return RedirectToAction(nameof(TiposSalida));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EliminarTipoSalida(long id)
|
||||
{
|
||||
var dbItem = await _context.DiezmoTiposSalida.FindAsync(id);
|
||||
if (dbItem == null) return NotFound();
|
||||
|
||||
// Validación simple (si ya hay salidas con este tipo no borrar duro)
|
||||
var enUso = await _context.DiezmoSalidas.AnyAsync(s => s.TipoSalidaId == id && !s.Eliminado);
|
||||
if (enUso)
|
||||
{
|
||||
dbItem.Activo = false; // Desactivar en lugar de borrar
|
||||
dbItem.Eliminado = true;
|
||||
TempData["SuccessMessage"] = "Tipo de salida desactivado (Estaba en uso).";
|
||||
}
|
||||
else
|
||||
{
|
||||
dbItem.Eliminado = true;
|
||||
TempData["SuccessMessage"] = "Tipo de salida eliminado.";
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return RedirectToAction(nameof(TiposSalida));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Beneficiarios
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
public async Task<IActionResult> Beneficiarios()
|
||||
{
|
||||
var lista = await _context.DiezmoBeneficiarios
|
||||
.Where(x => !x.Eliminado)
|
||||
.OrderBy(x => x.Nombre)
|
||||
.ToListAsync();
|
||||
return View(lista);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> GuardarBeneficiario(BeneficiarioViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
TempData["ErrorMessage"] = "Datos inválidos: " + string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage));
|
||||
return RedirectToAction(nameof(Beneficiarios));
|
||||
}
|
||||
|
||||
if (vm.Id == 0)
|
||||
{
|
||||
var nuevo = new DiezmoBeneficiario
|
||||
{
|
||||
Nombre = vm.Nombre,
|
||||
Descripcion = vm.Descripcion,
|
||||
CreadoPor = UsuarioActual()
|
||||
};
|
||||
_context.DiezmoBeneficiarios.Add(nuevo);
|
||||
TempData["SuccessMessage"] = "Beneficiario creado.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var dbItem = await _context.DiezmoBeneficiarios.FindAsync(vm.Id);
|
||||
if (dbItem == null || dbItem.Eliminado) return NotFound();
|
||||
|
||||
dbItem.Nombre = vm.Nombre;
|
||||
dbItem.Descripcion = vm.Descripcion;
|
||||
dbItem.ActualizadoPor = UsuarioActual();
|
||||
dbItem.ActualizadoEn = DateTime.UtcNow;
|
||||
_context.Update(dbItem);
|
||||
|
||||
TempData["SuccessMessage"] = "Beneficiario actualizado.";
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return RedirectToAction(nameof(Beneficiarios));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EliminarBeneficiario(long id)
|
||||
{
|
||||
var dbItem = await _context.DiezmoBeneficiarios.FindAsync(id);
|
||||
if (dbItem == null) return NotFound();
|
||||
|
||||
var enUso = await _context.DiezmoSalidas.AnyAsync(s => s.BeneficiarioId == id && !s.Eliminado);
|
||||
if (enUso)
|
||||
{
|
||||
dbItem.Activo = false;
|
||||
dbItem.Eliminado = true;
|
||||
TempData["SuccessMessage"] = "Beneficiario desactivado (estaba en uso).";
|
||||
}
|
||||
else
|
||||
{
|
||||
dbItem.Eliminado = true;
|
||||
TempData["SuccessMessage"] = "Beneficiario eliminado.";
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return RedirectToAction(nameof(Beneficiarios));
|
||||
}
|
||||
}
|
||||
356
RS_system/Controllers/DiezmoController.cs
Normal file
356
RS_system/Controllers/DiezmoController.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Rs_system.Filters;
|
||||
using Rs_system.Models.ViewModels;
|
||||
using Rs_system.Services;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class DiezmoController : Controller
|
||||
{
|
||||
private readonly IDiezmoCierreService _cierreService;
|
||||
private readonly IDiezmoReciboService _reciboService;
|
||||
private readonly IMiembroService _miembroService;
|
||||
|
||||
public DiezmoController(
|
||||
IDiezmoCierreService cierreService,
|
||||
IDiezmoReciboService reciboService,
|
||||
IMiembroService miembroService)
|
||||
{
|
||||
_cierreService = cierreService;
|
||||
_reciboService = reciboService;
|
||||
_miembroService = miembroService;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET: /Diezmo — Listado de cierres
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[Permission("Diezmo.Index")]
|
||||
public async Task<IActionResult> Index(int? anio)
|
||||
{
|
||||
anio ??= DateTime.Today.Year;
|
||||
var cierres = await _cierreService.GetCierresAsync(anio);
|
||||
|
||||
var vm = cierres.Select(c => new DiezmoCierreListViewModel
|
||||
{
|
||||
Id = c.Id,
|
||||
Fecha = c.Fecha,
|
||||
Cerrado = c.Cerrado,
|
||||
TotalRecibido = c.TotalRecibido,
|
||||
TotalNeto = c.TotalNeto,
|
||||
TotalSalidas = c.TotalSalidas,
|
||||
SaldoFinal = c.SaldoFinal,
|
||||
NumeroDetalles = c.Detalles?.Count ?? 0,
|
||||
NumeroSalidas = c.Salidas?.Count ?? 0
|
||||
}).ToList();
|
||||
|
||||
ViewBag.AnioActual = anio;
|
||||
ViewBag.Anios = GetAniosSelectList();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET: /Diezmo/Create
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[Permission("Diezmo.Create")]
|
||||
public IActionResult Create()
|
||||
=> View(new DiezmoCierreCreateViewModel());
|
||||
|
||||
// POST: /Diezmo/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.Create")]
|
||||
public async Task<IActionResult> Create(DiezmoCierreCreateViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
try
|
||||
{
|
||||
var cierre = await _cierreService.CrearCierreAsync(
|
||||
vm.Fecha, vm.Observaciones, UsuarioActual());
|
||||
|
||||
TempData["SuccessMessage"] = $"Cierre del {cierre.Fecha:dd/MM/yyyy} creado exitosamente.";
|
||||
return RedirectToAction(nameof(Detail), new { id = cierre.Id });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
ModelState.AddModelError("Fecha", ex.Message);
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET: /Diezmo/Detail/{id} — Pantalla operativa
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[Permission("Diezmo.Index")]
|
||||
public async Task<IActionResult> Detail(long id)
|
||||
{
|
||||
var cierre = await _cierreService.GetCierreByIdAsync(id);
|
||||
if (cierre == null || cierre.Eliminado) return NotFound();
|
||||
|
||||
var tiposSalida = await _cierreService.GetTiposSalidaActivosAsync();
|
||||
var beneficiarios = await _cierreService.GetBeneficiariosActivosAsync();
|
||||
var todosMiembros = await _miembroService.GetAllAsync();
|
||||
var miembrosSelect = todosMiembros
|
||||
.Where(m => m.Activo)
|
||||
.OrderBy(m => m.NombreCompleto)
|
||||
.Select(m => new SelectListItem(m.NombreCompleto, m.Id.ToString()))
|
||||
.ToList();
|
||||
|
||||
var vm = new DiezmoCierreDetalleViewModel
|
||||
{
|
||||
Id = cierre.Id,
|
||||
Fecha = cierre.Fecha,
|
||||
Cerrado = cierre.Cerrado,
|
||||
Observaciones = cierre.Observaciones,
|
||||
CerradoPor = cierre.CerradoPor,
|
||||
FechaCierre = cierre.FechaCierre,
|
||||
TotalRecibido = cierre.TotalRecibido,
|
||||
TotalCambio = cierre.TotalCambio,
|
||||
TotalNeto = cierre.TotalNeto,
|
||||
TotalSalidas = cierre.TotalSalidas,
|
||||
SaldoFinal = cierre.SaldoFinal,
|
||||
|
||||
Detalles = cierre.Detalles.Select(d => new DiezmoDetalleRowViewModel
|
||||
{
|
||||
Id = d.Id,
|
||||
MiembroId = d.MiembroId,
|
||||
NombreMiembro = d.Miembro?.Persona?.NombreCompleto ?? "—",
|
||||
MontoEntregado = d.MontoEntregado,
|
||||
CambioEntregado = d.CambioEntregado,
|
||||
MontoNeto = d.MontoNeto,
|
||||
Observaciones = d.Observaciones,
|
||||
Fecha = d.Fecha
|
||||
}).ToList(),
|
||||
|
||||
Salidas = cierre.Salidas.Select(s => new DiezmoSalidaRowViewModel
|
||||
{
|
||||
Id = s.Id,
|
||||
TipoSalidaNombre = s.TipoSalida?.Nombre ?? "—",
|
||||
BeneficiarioNombre = s.Beneficiario?.Nombre,
|
||||
Monto = s.Monto,
|
||||
Concepto = s.Concepto,
|
||||
NumeroRecibo = s.NumeroRecibo,
|
||||
Fecha = s.Fecha
|
||||
}).ToList(),
|
||||
|
||||
MiembrosSelect = miembrosSelect,
|
||||
TiposSalidaSelect = tiposSalida.Select(t =>
|
||||
new SelectListItem(t.Nombre, t.Id.ToString())).ToList(),
|
||||
BeneficiariosSelect = beneficiarios.Select(b =>
|
||||
new SelectListItem(b.Nombre, b.Id.ToString())).ToList()
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST: /Diezmo/AddDetalle
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.AddDetalle")]
|
||||
public async Task<IActionResult> AddDetalle(long cierreId, DiezmoDetalleFormViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest("Datos inválidos.");
|
||||
|
||||
TempData["ErrorMessage"] = "Datos inválidos. Verifique el formulario.";
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _cierreService.AgregarDetalleAsync(cierreId, vm, UsuarioActual());
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return await GetTotalesJsonAsync(cierreId);
|
||||
|
||||
TempData["SuccessMessage"] = "Diezmo registrado correctamente.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest(ex.Message);
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
// POST: /Diezmo/DeleteDetalle
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.AddDetalle")]
|
||||
public async Task<IActionResult> DeleteDetalle(long detalleId, long cierreId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cierreService.EliminarDetalleAsync(detalleId, UsuarioActual());
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return await GetTotalesJsonAsync(cierreId);
|
||||
|
||||
TempData["SuccessMessage"] = "Detalle eliminado.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest(ex.Message);
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST: /Diezmo/AddSalida
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.AddSalida")]
|
||||
public async Task<IActionResult> AddSalida(long cierreId, DiezmoSalidaFormViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest("Datos inválidos.");
|
||||
|
||||
TempData["ErrorMessage"] = "Datos inválidos. Verifique el formulario.";
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _cierreService.AgregarSalidaAsync(cierreId, vm, UsuarioActual());
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return await GetTotalesJsonAsync(cierreId);
|
||||
|
||||
TempData["SuccessMessage"] = "Salida registrada correctamente.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest(ex.Message);
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
// POST: /Diezmo/DeleteSalida
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.AddSalida")]
|
||||
public async Task<IActionResult> DeleteSalida(long salidaId, long cierreId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cierreService.EliminarSalidaAsync(salidaId, UsuarioActual());
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return await GetTotalesJsonAsync(cierreId);
|
||||
|
||||
TempData["SuccessMessage"] = "Salida eliminada.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest(ex.Message);
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST: /Diezmo/Close/{id}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.Close")]
|
||||
public async Task<IActionResult> Close(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cierreService.CerrarCierreAsync(id, UsuarioActual());
|
||||
TempData["SuccessMessage"] = "Cierre sellado exitosamente.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST: /Diezmo/Reopen/{id} — Solo Administrador
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.Reopen")]
|
||||
public async Task<IActionResult> Reopen(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cierreService.ReabrirCierreAsync(id, UsuarioActual());
|
||||
TempData["SuccessMessage"] = "Cierre reabierto.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET: /Diezmo/Recibo/{salidaId}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[Permission("Diezmo.Index")]
|
||||
public async Task<IActionResult> Recibo(long salidaId)
|
||||
{
|
||||
var numero = await _reciboService.GenerarNumeroReciboAsync(salidaId);
|
||||
var salida = await _reciboService.GetSalidaParaReciboAsync(salidaId);
|
||||
|
||||
if (salida == null) return NotFound();
|
||||
|
||||
ViewBag.NumeroRecibo = numero;
|
||||
ViewBag.Emisor = UsuarioActual();
|
||||
return View(salida);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Helpers privados
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
private string UsuarioActual()
|
||||
=> User.FindFirst(ClaimTypes.Name)?.Value
|
||||
?? User.Identity?.Name
|
||||
?? "Sistema";
|
||||
|
||||
private static List<SelectListItem> GetAniosSelectList()
|
||||
{
|
||||
var anioActual = DateTime.Today.Year;
|
||||
return Enumerable.Range(anioActual - 3, 5)
|
||||
.Select(a => new SelectListItem(a.ToString(), a.ToString()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<IActionResult> GetTotalesJsonAsync(long id)
|
||||
{
|
||||
var cierre = await _cierreService.GetCierreByIdAsync(id);
|
||||
if (cierre == null) return NotFound();
|
||||
|
||||
return Json(new {
|
||||
totalRecibido = cierre.TotalRecibido,
|
||||
totalCambio = cierre.TotalCambio,
|
||||
totalNeto = cierre.TotalNeto,
|
||||
totalSalidas = cierre.TotalSalidas,
|
||||
saldoFinal = cierre.SaldoFinal
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,13 @@ public class ApplicationDbContext : DbContext
|
||||
public DbSet<Colaboracion> Colaboraciones { get; set; }
|
||||
public DbSet<DetalleColaboracion> DetalleColaboraciones { get; set; }
|
||||
|
||||
// Diezmos module
|
||||
public DbSet<DiezmoCierre> DiezmoCierres { get; set; }
|
||||
public DbSet<DiezmoDetalle> DiezmoDetalles { get; set; }
|
||||
public DbSet<DiezmoSalida> DiezmoSalidas { get; set; }
|
||||
public DbSet<DiezmoBeneficiario> DiezmoBeneficiarios { get; set; }
|
||||
public DbSet<DiezmoTipoSalida> DiezmoTiposSalida { get; set; }
|
||||
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
@@ -217,6 +224,81 @@ public class ApplicationDbContext : DbContext
|
||||
.IsUnique();
|
||||
});
|
||||
|
||||
// ── Diezmos module configuration ──────────────────────────────────
|
||||
modelBuilder.Entity<DiezmoCierre>(entity =>
|
||||
{
|
||||
entity.ToTable("diezmo_cierres", "public");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.TotalRecibido).HasColumnType("numeric(12,2)");
|
||||
entity.Property(e => e.TotalCambio).HasColumnType("numeric(12,2)");
|
||||
entity.Property(e => e.TotalNeto).HasColumnType("numeric(12,2)");
|
||||
entity.Property(e => e.TotalSalidas).HasColumnType("numeric(12,2)");
|
||||
entity.Property(e => e.SaldoFinal).HasColumnType("numeric(12,2)");
|
||||
|
||||
// One closure per date
|
||||
entity.HasIndex(e => e.Fecha).IsUnique();
|
||||
|
||||
entity.HasMany(e => e.Detalles)
|
||||
.WithOne(d => d.DiezmoCierre)
|
||||
.HasForeignKey(d => d.DiezmoCierreId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasMany(e => e.Salidas)
|
||||
.WithOne(s => s.DiezmoCierre)
|
||||
.HasForeignKey(s => s.DiezmoCierreId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<DiezmoDetalle>(entity =>
|
||||
{
|
||||
entity.ToTable("diezmo_detalles", "public");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.MontoEntregado).HasColumnType("numeric(12,2)");
|
||||
entity.Property(e => e.CambioEntregado).HasColumnType("numeric(12,2)");
|
||||
entity.Property(e => e.MontoNeto).HasColumnType("numeric(12,2)");
|
||||
|
||||
entity.HasIndex(e => e.DiezmoCierreId);
|
||||
entity.HasIndex(e => e.MiembroId);
|
||||
|
||||
entity.HasOne(e => e.Miembro)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.MiembroId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<DiezmoSalida>(entity =>
|
||||
{
|
||||
entity.ToTable("diezmo_salidas", "public");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Monto).HasColumnType("numeric(12,2)");
|
||||
|
||||
entity.HasIndex(e => e.DiezmoCierreId);
|
||||
|
||||
entity.HasOne(e => e.TipoSalida)
|
||||
.WithMany(t => t.Salidas)
|
||||
.HasForeignKey(e => e.TipoSalidaId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
entity.HasOne(e => e.Beneficiario)
|
||||
.WithMany(b => b.Salidas)
|
||||
.HasForeignKey(e => e.BeneficiarioId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<DiezmoTipoSalida>(entity =>
|
||||
{
|
||||
entity.ToTable("diezmo_tipos_salida", "public");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Nombre).HasMaxLength(100).IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<DiezmoBeneficiario>(entity =>
|
||||
{
|
||||
entity.ToTable("diezmo_beneficiarios", "public");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Nombre).HasMaxLength(150).IsRequired();
|
||||
});
|
||||
|
||||
|
||||
// Global configuration: Convert all dates to UTC when saving
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
|
||||
221
RS_system/Migrations/sql_diezmos.sql
Normal file
221
RS_system/Migrations/sql_diezmos.sql
Normal file
@@ -0,0 +1,221 @@
|
||||
-- =============================================
|
||||
-- Módulo de Diezmos — Script SQL de migración
|
||||
-- Ejecutar contra la BD de desarrollo/producción
|
||||
-- Idempotente: usa IF NOT EXISTS / WHERE NOT EXISTS
|
||||
-- =============================================
|
||||
|
||||
-- ─────────────────────────────────────────────
|
||||
-- Tabla: diezmo_tipos_salida
|
||||
-- Catálogo de tipos de salida (Entrega al Pastor, Gastos, etc.)
|
||||
-- ─────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS public.diezmo_tipos_salida
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 CACHE 1),
|
||||
nombre varchar(100) NOT NULL,
|
||||
descripcion varchar(300),
|
||||
es_entrega_pastor boolean NOT NULL DEFAULT false,
|
||||
activo boolean NOT NULL DEFAULT true,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
creado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
creado_por varchar(100),
|
||||
actualizado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT diezmo_tipos_salida_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.diezmo_tipos_salida IS 'Catálogo de tipos de salida del módulo de diezmos';
|
||||
|
||||
INSERT INTO public.diezmo_tipos_salida (nombre, descripcion, es_entrega_pastor, activo)
|
||||
SELECT 'Entrega al Pastor', 'Entrega oficial de diezmos al pastor', true, true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.diezmo_tipos_salida WHERE es_entrega_pastor = true);
|
||||
|
||||
INSERT INTO public.diezmo_tipos_salida (nombre, descripcion, es_entrega_pastor, activo)
|
||||
SELECT 'Gastos Administrativos', 'Gastos operativos de la iglesia', false, true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.diezmo_tipos_salida WHERE nombre = 'Gastos Administrativos');
|
||||
|
||||
-- ─────────────────────────────────────────────
|
||||
-- Tabla: diezmo_beneficiarios
|
||||
-- Personas o entidades que reciben salidas
|
||||
-- ─────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS public.diezmo_beneficiarios
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 CACHE 1),
|
||||
nombre varchar(150) NOT NULL,
|
||||
descripcion varchar(300),
|
||||
activo boolean NOT NULL DEFAULT true,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
creado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
creado_por varchar(100),
|
||||
actualizado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
actualizado_por varchar(100),
|
||||
CONSTRAINT diezmo_beneficiarios_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.diezmo_beneficiarios IS 'Personas o entidades beneficiarias de salidas de diezmos';
|
||||
|
||||
-- ─────────────────────────────────────────────
|
||||
-- Tabla: diezmo_cierres
|
||||
-- Agregado raíz — un cierre por fecha (UNIQUE)
|
||||
-- ─────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS public.diezmo_cierres
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 CACHE 1),
|
||||
fecha date NOT NULL,
|
||||
cerrado boolean NOT NULL DEFAULT false,
|
||||
fecha_cierre timestamptz,
|
||||
cerrado_por varchar(100),
|
||||
observaciones varchar(500),
|
||||
total_recibido numeric(12,2) NOT NULL DEFAULT 0,
|
||||
total_cambio numeric(12,2) NOT NULL DEFAULT 0,
|
||||
total_neto numeric(12,2) NOT NULL DEFAULT 0,
|
||||
total_salidas numeric(12,2) NOT NULL DEFAULT 0,
|
||||
saldo_final numeric(12,2) NOT NULL DEFAULT 0,
|
||||
creado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
creado_por varchar(100),
|
||||
actualizado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
actualizado_por varchar(100),
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT diezmo_cierres_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT diezmo_cierres_fecha_uq UNIQUE (fecha)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.diezmo_cierres IS 'Cierres periódicos del módulo de diezmos (un registro por fecha)';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_diezmo_cierres_fecha ON public.diezmo_cierres (fecha);
|
||||
CREATE INDEX IF NOT EXISTS idx_diezmo_cierres_cerrado ON public.diezmo_cierres (cerrado);
|
||||
CREATE INDEX IF NOT EXISTS idx_diezmo_cierres_eliminado ON public.diezmo_cierres (eliminado);
|
||||
|
||||
-- ─────────────────────────────────────────────
|
||||
-- Tabla: diezmo_detalles
|
||||
-- Diezmo individual por miembro dentro de un cierre
|
||||
-- ─────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS public.diezmo_detalles
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 CACHE 1),
|
||||
diezmo_cierre_id bigint NOT NULL,
|
||||
miembro_id bigint NOT NULL,
|
||||
monto_entregado numeric(12,2) NOT NULL CHECK (monto_entregado >= 0),
|
||||
cambio_entregado numeric(12,2) NOT NULL DEFAULT 0 CHECK (cambio_entregado >= 0),
|
||||
monto_neto numeric(12,2) NOT NULL DEFAULT 0,
|
||||
observaciones varchar(300),
|
||||
fecha timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
creado_por varchar(100),
|
||||
creado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
actualizado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
actualizado_por varchar(100),
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT diezmo_detalles_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT fk_diezmo_detalles_cierre
|
||||
FOREIGN KEY (diezmo_cierre_id) REFERENCES public.diezmo_cierres (id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_diezmo_detalles_miembro
|
||||
FOREIGN KEY (miembro_id) REFERENCES public.miembros (id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.diezmo_detalles IS 'Diezmos individuales aportados por miembro en cada cierre';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_diezmo_detalles_cierre_id ON public.diezmo_detalles (diezmo_cierre_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_diezmo_detalles_miembro_id ON public.diezmo_detalles (miembro_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_diezmo_detalles_eliminado ON public.diezmo_detalles (eliminado);
|
||||
|
||||
-- ─────────────────────────────────────────────
|
||||
-- Tabla: diezmo_salidas
|
||||
-- Salida de fondos registrada contra un cierre
|
||||
-- ─────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS public.diezmo_salidas
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 CACHE 1),
|
||||
diezmo_cierre_id bigint NOT NULL,
|
||||
tipo_salida_id bigint NOT NULL,
|
||||
beneficiario_id bigint,
|
||||
monto numeric(12,2) NOT NULL CHECK (monto > 0),
|
||||
concepto varchar(300) NOT NULL,
|
||||
numero_recibo varchar(30),
|
||||
fecha timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
creado_por varchar(100),
|
||||
creado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
actualizado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT diezmo_salidas_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT fk_diezmo_salidas_cierre
|
||||
FOREIGN KEY (diezmo_cierre_id) REFERENCES public.diezmo_cierres (id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT fk_diezmo_salidas_tipo
|
||||
FOREIGN KEY (tipo_salida_id) REFERENCES public.diezmo_tipos_salida (id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_diezmo_salidas_beneficiario
|
||||
FOREIGN KEY (beneficiario_id) REFERENCES public.diezmo_beneficiarios (id)
|
||||
ON UPDATE CASCADE ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.diezmo_salidas IS 'Salidas de fondos (entregas, gastos) registradas en un cierre de diezmos';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_diezmo_salidas_cierre_id ON public.diezmo_salidas (diezmo_cierre_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_diezmo_salidas_eliminado ON public.diezmo_salidas (eliminado);
|
||||
|
||||
-- =============================================
|
||||
-- Bitácora de operaciones críticas del módulo
|
||||
-- =============================================
|
||||
CREATE TABLE IF NOT EXISTS public.diezmo_bitacora
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY (INCREMENT 1 START 1 MINVALUE 1 CACHE 1),
|
||||
diezmo_cierre_id bigint,
|
||||
accion varchar(50) NOT NULL, -- 'CIERRE', 'REAPERTURA', 'ELIMINAR_DETALLE', 'ELIMINAR_SALIDA'
|
||||
detalle varchar(500),
|
||||
realizado_por varchar(100) NOT NULL,
|
||||
realizado_en timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT diezmo_bitacora_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_diezmo_bitacora_cierre_id ON public.diezmo_bitacora (diezmo_cierre_id);
|
||||
|
||||
-- =============================================
|
||||
-- Permisos para el módulo
|
||||
-- =============================================
|
||||
|
||||
-- Crear módulo si no existe
|
||||
INSERT INTO public.modulos (nombre, descripcion, icono, orden, activo)
|
||||
SELECT 'Diezmos', 'Módulo de gestión de diezmos', 'bi bi-cash-coin', 50, true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.modulos WHERE nombre = 'Diezmos');
|
||||
|
||||
-- Permiso: ver listado de cierres
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Diezmo.Index', 'Ver Diezmos', 'Permite ver el listado de cierres de diezmos',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Diezmos'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Diezmo.Index');
|
||||
|
||||
-- Permiso: crear cierre
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Diezmo.Create', 'Crear Cierre', 'Permite crear un nuevo cierre de diezmos',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Diezmos'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Diezmo.Create');
|
||||
|
||||
-- Permiso: registrar detalle (diezmo por miembro)
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Diezmo.AddDetalle', 'Registrar Detalle', 'Permite agregar/eliminar diezmos de miembros en un cierre',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Diezmos'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Diezmo.AddDetalle');
|
||||
|
||||
-- Permiso: registrar salida
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Diezmo.AddSalida', 'Registrar Salida', 'Permite registrar salidas/entregas en un cierre',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Diezmos'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Diezmo.AddSalida');
|
||||
|
||||
-- Permiso: cerrar cierre
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Diezmo.Close', 'Cerrar Cierre', 'Permite cerrar un cierre de diezmos (bloquea edición)',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Diezmos'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Diezmo.Close');
|
||||
|
||||
-- Permiso: reabrir cierre (solo Administrador)
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Diezmo.Reopen', 'Reabrir Cierre', 'Permite reabrir un cierre cerrado (solo Administrador)',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Diezmos'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Diezmo.Reopen');
|
||||
|
||||
-- Permiso: reportes y consultas
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Diezmo.Reportes', 'Reportes Diezmos', 'Permite ver reportes y estado de cuenta del módulo de diezmos',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Diezmos'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Diezmo.Reportes');
|
||||
48
RS_system/Models/DiezmoBeneficiario.cs
Normal file
48
RS_system/Models/DiezmoBeneficiario.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Personas o entidades que pueden recibir salidas de diezmos
|
||||
/// (pastor, tesorero, organismos externos, etc.)
|
||||
/// </summary>
|
||||
[Table("diezmo_beneficiarios")]
|
||||
public class DiezmoBeneficiario
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("nombre")]
|
||||
[Required]
|
||||
[StringLength(150)]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(300)]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("eliminado")]
|
||||
public bool Eliminado { get; set; } = false;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("creado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CreadoPor { get; set; }
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_por")]
|
||||
[StringLength(100)]
|
||||
public string? ActualizadoPor { get; set; }
|
||||
|
||||
// Navegación
|
||||
public virtual ICollection<DiezmoSalida> Salidas { get; set; } = new List<DiezmoSalida>();
|
||||
}
|
||||
74
RS_system/Models/DiezmoCierre.cs
Normal file
74
RS_system/Models/DiezmoCierre.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Agregado raíz del módulo de diezmos.
|
||||
/// Representa un período/corte de diezmos (la fecha la elige el operador libremente).
|
||||
/// Un cierre por fecha (UNIQUE en fecha).
|
||||
/// </summary>
|
||||
[Table("diezmo_cierres")]
|
||||
public class DiezmoCierre
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>Fecha del cierre. UNIQUE — no pueden existir dos cierres para el mismo día.</summary>
|
||||
[Column("fecha")]
|
||||
[Required]
|
||||
public DateOnly Fecha { get; set; }
|
||||
|
||||
[Column("cerrado")]
|
||||
public bool Cerrado { get; set; } = false;
|
||||
|
||||
[Column("fecha_cierre")]
|
||||
public DateTime? FechaCierre { get; set; }
|
||||
|
||||
[Column("cerrado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CerradoPor { get; set; }
|
||||
|
||||
[Column("observaciones")]
|
||||
[StringLength(500)]
|
||||
public string? Observaciones { get; set; }
|
||||
|
||||
// ── Totales calculados (persistidos para consulta rápida en el listado) ──
|
||||
[Column("total_recibido", TypeName = "numeric(12,2)")]
|
||||
public decimal TotalRecibido { get; set; } = 0;
|
||||
|
||||
[Column("total_cambio", TypeName = "numeric(12,2)")]
|
||||
public decimal TotalCambio { get; set; } = 0;
|
||||
|
||||
[Column("total_neto", TypeName = "numeric(12,2)")]
|
||||
public decimal TotalNeto { get; set; } = 0;
|
||||
|
||||
[Column("total_salidas", TypeName = "numeric(12,2)")]
|
||||
public decimal TotalSalidas { get; set; } = 0;
|
||||
|
||||
[Column("saldo_final", TypeName = "numeric(12,2)")]
|
||||
public decimal SaldoFinal { get; set; } = 0;
|
||||
|
||||
// ── Auditoría ──
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("creado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CreadoPor { get; set; }
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_por")]
|
||||
[StringLength(100)]
|
||||
public string? ActualizadoPor { get; set; }
|
||||
|
||||
[Column("eliminado")]
|
||||
public bool Eliminado { get; set; } = false;
|
||||
|
||||
// ── Navegación ──
|
||||
public virtual ICollection<DiezmoDetalle> Detalles { get; set; } = new List<DiezmoDetalle>();
|
||||
public virtual ICollection<DiezmoSalida> Salidas { get; set; } = new List<DiezmoSalida>();
|
||||
}
|
||||
67
RS_system/Models/DiezmoDetalle.cs
Normal file
67
RS_system/Models/DiezmoDetalle.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Diezmo individual aportado por un miembro dentro de un cierre.
|
||||
/// MontoNeto = MontoEntregado - CambioEntregado (calculado por el sistema).
|
||||
/// </summary>
|
||||
[Table("diezmo_detalles")]
|
||||
public class DiezmoDetalle
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("diezmo_cierre_id")]
|
||||
public long DiezmoCierreId { get; set; }
|
||||
|
||||
[Column("miembro_id")]
|
||||
public long MiembroId { get; set; }
|
||||
|
||||
/// <summary>Monto físico que el miembro entregó (puede incluir cambio).</summary>
|
||||
[Column("monto_entregado", TypeName = "numeric(12,2)")]
|
||||
[Required]
|
||||
public decimal MontoEntregado { get; set; }
|
||||
|
||||
/// <summary>Cambio devuelto al miembro.</summary>
|
||||
[Column("cambio_entregado", TypeName = "numeric(12,2)")]
|
||||
public decimal CambioEntregado { get; set; } = 0;
|
||||
|
||||
/// <summary>Diezmo neto real = MontoEntregado - CambioEntregado. Calculado por el sistema.</summary>
|
||||
[Column("monto_neto", TypeName = "numeric(12,2)")]
|
||||
public decimal MontoNeto { get; set; }
|
||||
|
||||
[Column("observaciones")]
|
||||
[StringLength(300)]
|
||||
public string? Observaciones { get; set; }
|
||||
|
||||
[Column("fecha")]
|
||||
public DateTime Fecha { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// ── Auditoría ──
|
||||
[Column("creado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CreadoPor { get; set; }
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_por")]
|
||||
[StringLength(100)]
|
||||
public string? ActualizadoPor { get; set; }
|
||||
|
||||
[Column("eliminado")]
|
||||
public bool Eliminado { get; set; } = false;
|
||||
|
||||
// ── Navegación ──
|
||||
[ForeignKey("DiezmoCierreId")]
|
||||
public virtual DiezmoCierre DiezmoCierre { get; set; } = null!;
|
||||
|
||||
[ForeignKey("MiembroId")]
|
||||
public virtual Miembro Miembro { get; set; } = null!;
|
||||
}
|
||||
66
RS_system/Models/DiezmoSalida.cs
Normal file
66
RS_system/Models/DiezmoSalida.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Salida de fondos registrada contra un cierre de diezmos.
|
||||
/// Incluye entregas al pastor, gastos administrativos, misiones, etc.
|
||||
/// </summary>
|
||||
[Table("diezmo_salidas")]
|
||||
public class DiezmoSalida
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("diezmo_cierre_id")]
|
||||
public long DiezmoCierreId { get; set; }
|
||||
|
||||
[Column("tipo_salida_id")]
|
||||
public long TipoSalidaId { get; set; }
|
||||
|
||||
[Column("beneficiario_id")]
|
||||
public long? BeneficiarioId { get; set; }
|
||||
|
||||
[Column("monto", TypeName = "numeric(12,2)")]
|
||||
[Required]
|
||||
public decimal Monto { get; set; }
|
||||
|
||||
[Column("concepto")]
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Concepto { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Correlativo de recibo asignado al momento de generar el comprobante.</summary>
|
||||
[Column("numero_recibo")]
|
||||
[StringLength(30)]
|
||||
public string? NumeroRecibo { get; set; }
|
||||
|
||||
[Column("fecha")]
|
||||
public DateTime Fecha { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// ── Auditoría ──
|
||||
[Column("creado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CreadoPor { get; set; }
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("eliminado")]
|
||||
public bool Eliminado { get; set; } = false;
|
||||
|
||||
// ── Navegación ──
|
||||
[ForeignKey("DiezmoCierreId")]
|
||||
public virtual DiezmoCierre DiezmoCierre { get; set; } = null!;
|
||||
|
||||
[ForeignKey("TipoSalidaId")]
|
||||
public virtual DiezmoTipoSalida TipoSalida { get; set; } = null!;
|
||||
|
||||
[ForeignKey("BeneficiarioId")]
|
||||
public virtual DiezmoBeneficiario? Beneficiario { get; set; }
|
||||
}
|
||||
51
RS_system/Models/DiezmoTipoSalida.cs
Normal file
51
RS_system/Models/DiezmoTipoSalida.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Catálogo de tipos de salida del módulo de diezmos
|
||||
/// (Entrega al Pastor, Gastos Administrativos, Misiones, etc.)
|
||||
/// </summary>
|
||||
[Table("diezmo_tipos_salida")]
|
||||
public class DiezmoTipoSalida
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("nombre")]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(300)]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Marca este tipo como la entrega oficial al pastor.
|
||||
/// Permite sugerirlo/forzarlo automáticamente al cerrar con saldo pendiente.
|
||||
/// </summary>
|
||||
[Column("es_entrega_pastor")]
|
||||
public bool EsEntregaPastor { get; set; } = false;
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("eliminado")]
|
||||
public bool Eliminado { get; set; } = false;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("creado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CreadoPor { get; set; }
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navegación
|
||||
public virtual ICollection<DiezmoSalida> Salidas { get; set; } = new List<DiezmoSalida>();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Rs_system.Models.ViewModels.Catalogos;
|
||||
|
||||
public class TipoSalidaViewModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El nombre es obligatorio")]
|
||||
[StringLength(100)]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(300)]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Display(Name = "Es Entrega Directa a Pastor")]
|
||||
public bool EsEntregaPastor { get; set; }
|
||||
}
|
||||
|
||||
public class BeneficiarioViewModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El nombre es obligatorio")]
|
||||
[StringLength(150)]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(300)]
|
||||
public string? Descripcion { get; set; }
|
||||
}
|
||||
84
RS_system/Models/ViewModels/DiezmoCierreDetalleViewModel.cs
Normal file
84
RS_system/Models/ViewModels/DiezmoCierreDetalleViewModel.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Formulario — Nuevo cierre
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
public class DiezmoCierreCreateViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "La fecha es obligatoria.")]
|
||||
[Display(Name = "Fecha del cierre")]
|
||||
public DateOnly Fecha { get; set; } = DateOnly.FromDateTime(DateTime.Today);
|
||||
|
||||
[Display(Name = "Observaciones")]
|
||||
[StringLength(500)]
|
||||
public string? Observaciones { get; set; }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Pantalla operativa de detalle del cierre
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
public class DiezmoCierreDetalleViewModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public DateOnly Fecha { get; set; }
|
||||
public bool Cerrado { get; set; }
|
||||
public string? Observaciones { get; set; }
|
||||
public string? CerradoPor { get; set; }
|
||||
public DateTime? FechaCierre { get; set; }
|
||||
|
||||
// Totales
|
||||
public decimal TotalRecibido { get; set; }
|
||||
public decimal TotalCambio { get; set; }
|
||||
public decimal TotalNeto { get; set; }
|
||||
public decimal TotalSalidas { get; set; }
|
||||
public decimal SaldoFinal { get; set; }
|
||||
|
||||
// Datos de detalles
|
||||
public List<DiezmoDetalleRowViewModel> Detalles { get; set; } = new();
|
||||
|
||||
// Datos de salidas
|
||||
public List<DiezmoSalidaRowViewModel> Salidas { get; set; } = new();
|
||||
|
||||
// Formularios embebidos para modales
|
||||
public DiezmoDetalleFormViewModel FormDetalle { get; set; } = new();
|
||||
public DiezmoSalidaFormViewModel FormSalida { get; set; } = new();
|
||||
|
||||
// Datos de selectores para los modales
|
||||
public List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem> MiembrosSelect { get; set; } = new();
|
||||
public List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem> TiposSalidaSelect { get; set; } = new();
|
||||
public List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem> BeneficiariosSelect { get; set; } = new();
|
||||
|
||||
public string EstadoBadge => Cerrado ? "badge bg-secondary" : "badge bg-success";
|
||||
public string EstadoTexto => Cerrado ? "Cerrado" : "Abierto";
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Fila de un detalle en la tabla
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
public class DiezmoDetalleRowViewModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long MiembroId { get; set; }
|
||||
public string NombreMiembro { get; set; } = string.Empty;
|
||||
public decimal MontoEntregado { get; set; }
|
||||
public decimal CambioEntregado { get; set; }
|
||||
public decimal MontoNeto { get; set; }
|
||||
public string? Observaciones { get; set; }
|
||||
public DateTime Fecha { get; set; }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Fila de una salida en la tabla
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
public class DiezmoSalidaRowViewModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string TipoSalidaNombre { get; set; } = string.Empty;
|
||||
public string? BeneficiarioNombre { get; set; }
|
||||
public decimal Monto { get; set; }
|
||||
public string Concepto { get; set; } = string.Empty;
|
||||
public string? NumeroRecibo { get; set; }
|
||||
public DateTime Fecha { get; set; }
|
||||
}
|
||||
19
RS_system/Models/ViewModels/DiezmoCierreListViewModel.cs
Normal file
19
RS_system/Models/ViewModels/DiezmoCierreListViewModel.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
/// <summary>Fila del listado de cierres de diezmos.</summary>
|
||||
public class DiezmoCierreListViewModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public DateOnly Fecha { get; set; }
|
||||
public bool Cerrado { get; set; }
|
||||
public decimal TotalRecibido { get; set; }
|
||||
public decimal TotalNeto { get; set; }
|
||||
public decimal TotalSalidas { get; set; }
|
||||
public decimal SaldoFinal { get; set; }
|
||||
public int NumeroDetalles { get; set; }
|
||||
public int NumeroSalidas { get; set; }
|
||||
public string EstadoBadge => Cerrado ? "badge bg-secondary" : "badge bg-success";
|
||||
public string EstadoTexto => Cerrado ? "Cerrado" : "Abierto";
|
||||
}
|
||||
28
RS_system/Models/ViewModels/DiezmoDetalleFormViewModel.cs
Normal file
28
RS_system/Models/ViewModels/DiezmoDetalleFormViewModel.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
/// <summary>Formulario modal para agregar un diezmo de un miembro.</summary>
|
||||
public class DiezmoDetalleFormViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Seleccione un miembro.")]
|
||||
[Display(Name = "Miembro")]
|
||||
public long MiembroId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El monto entregado es obligatorio.")]
|
||||
[Range(0.01, 999999.99, ErrorMessage = "El monto debe ser mayor a 0.")]
|
||||
[Display(Name = "Monto entregado")]
|
||||
public decimal MontoEntregado { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El monto del diezmo (neto) es obligatorio.")]
|
||||
[Range(0.01, 999999.99, ErrorMessage = "El diezmo debe ser mayor a 0.")]
|
||||
[Display(Name = "Diezmo (Neto)")]
|
||||
public decimal MontoNeto { get; set; }
|
||||
|
||||
// Este campo ahora vendrá como solo-lectura desde el formulario
|
||||
public decimal CambioEntregado { get; set; } = 0;
|
||||
|
||||
[Display(Name = "Observaciones")]
|
||||
[StringLength(300)]
|
||||
public string? Observaciones { get; set; }
|
||||
}
|
||||
24
RS_system/Models/ViewModels/DiezmoSalidaFormViewModel.cs
Normal file
24
RS_system/Models/ViewModels/DiezmoSalidaFormViewModel.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
/// <summary>Formulario modal para registrar una salida/entrega de fondos.</summary>
|
||||
public class DiezmoSalidaFormViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Seleccione el tipo de salida.")]
|
||||
[Display(Name = "Tipo de salida")]
|
||||
public long TipoSalidaId { get; set; }
|
||||
|
||||
[Display(Name = "Beneficiario")]
|
||||
public long? BeneficiarioId { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El monto es obligatorio.")]
|
||||
[Range(0.01, 999999.99, ErrorMessage = "El monto debe ser mayor a 0.")]
|
||||
[Display(Name = "Monto")]
|
||||
public decimal Monto { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El concepto es obligatorio.")]
|
||||
[StringLength(300)]
|
||||
[Display(Name = "Concepto")]
|
||||
public string Concepto { get; set; } = string.Empty;
|
||||
}
|
||||
14
RS_system/Models/ViewModels/PaginatedViewModel.cs
Normal file
14
RS_system/Models/ViewModels/PaginatedViewModel.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
public class PaginatedViewModel<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = new();
|
||||
public int CurrentPage { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalItems { get; set; }
|
||||
public int TotalPages => (int)Math.Ceiling((double)TotalItems / PageSize);
|
||||
public string? SearchQuery { get; set; }
|
||||
|
||||
public bool HasPreviousPage => CurrentPage > 1;
|
||||
public bool HasNextPage => CurrentPage < TotalPages;
|
||||
}
|
||||
@@ -50,6 +50,11 @@ builder.Services.AddScoped<IContabilidadGeneralService, ContabilidadGeneralServi
|
||||
builder.Services.AddScoped<IPrestamoService, PrestamoService>();
|
||||
builder.Services.AddScoped<IColaboracionService, ColaboracionService>();
|
||||
builder.Services.AddSingleton<IQueryCacheService, QueryCacheService>();
|
||||
|
||||
// Diezmos module services
|
||||
builder.Services.AddScoped<IDiezmoCalculoService, DiezmoCalculoService>();
|
||||
builder.Services.AddScoped<IDiezmoCierreService, DiezmoCierreService>();
|
||||
builder.Services.AddScoped<IDiezmoReciboService, DiezmoReciboService>();
|
||||
builder.Services.AddMemoryCache(options =>
|
||||
{
|
||||
options.SizeLimit = 1024; // 1024 cache entries max
|
||||
|
||||
25
RS_system/Services/DiezmoCalculoService.cs
Normal file
25
RS_system/Services/DiezmoCalculoService.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class DiezmoCalculoService : IDiezmoCalculoService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public decimal CalcularMontoNeto(decimal montoEntregado, decimal cambioEntregado)
|
||||
=> montoEntregado - cambioEntregado;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DiezmoCierre RecalcularTotales(DiezmoCierre cierre)
|
||||
{
|
||||
var detallesActivos = cierre.Detalles.Where(d => !d.Eliminado).ToList();
|
||||
var salidasActivas = cierre.Salidas.Where(s => !s.Eliminado).ToList();
|
||||
|
||||
cierre.TotalRecibido = detallesActivos.Sum(d => d.MontoEntregado);
|
||||
cierre.TotalCambio = detallesActivos.Sum(d => d.CambioEntregado);
|
||||
cierre.TotalNeto = detallesActivos.Sum(d => d.MontoNeto);
|
||||
cierre.TotalSalidas = salidasActivas.Sum(s => s.Monto);
|
||||
cierre.SaldoFinal = cierre.TotalNeto - cierre.TotalSalidas;
|
||||
|
||||
return cierre;
|
||||
}
|
||||
}
|
||||
262
RS_system/Services/DiezmoCierreService.cs
Normal file
262
RS_system/Services/DiezmoCierreService.cs
Normal file
@@ -0,0 +1,262 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Models.ViewModels;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class DiezmoCierreService : IDiezmoCierreService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IDiezmoCalculoService _calculo;
|
||||
|
||||
public DiezmoCierreService(ApplicationDbContext context, IDiezmoCalculoService calculo)
|
||||
{
|
||||
_context = context;
|
||||
_calculo = calculo;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Catálogos
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task<List<DiezmoTipoSalida>> GetTiposSalidaActivosAsync()
|
||||
=> await _context.DiezmoTiposSalida
|
||||
.Where(t => t.Activo && !t.Eliminado)
|
||||
.OrderBy(t => t.Nombre)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<List<DiezmoBeneficiario>> GetBeneficiariosActivosAsync()
|
||||
=> await _context.DiezmoBeneficiarios
|
||||
.Where(b => b.Activo && !b.Eliminado)
|
||||
.OrderBy(b => b.Nombre)
|
||||
.ToListAsync();
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Cierres
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task<List<DiezmoCierre>> GetCierresAsync(int? anio = null)
|
||||
{
|
||||
var query = _context.DiezmoCierres
|
||||
.Where(c => !c.Eliminado);
|
||||
|
||||
if (anio.HasValue)
|
||||
query = query.Where(c => c.Fecha.Year == anio.Value);
|
||||
|
||||
return await query
|
||||
.OrderByDescending(c => c.Fecha)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<DiezmoCierre?> GetCierreByIdAsync(long id)
|
||||
=> await _context.DiezmoCierres
|
||||
.Include(c => c.Detalles.Where(d => !d.Eliminado))
|
||||
.ThenInclude(d => d.Miembro)
|
||||
.ThenInclude(m => m.Persona)
|
||||
.Include(c => c.Salidas.Where(s => !s.Eliminado))
|
||||
.ThenInclude(s => s.TipoSalida)
|
||||
.Include(c => c.Salidas.Where(s => !s.Eliminado))
|
||||
.ThenInclude(s => s.Beneficiario)
|
||||
.FirstOrDefaultAsync(c => c.Id == id && !c.Eliminado);
|
||||
|
||||
public async Task<DiezmoCierre> CrearCierreAsync(DateOnly fecha, string? observaciones, string creadoPor)
|
||||
{
|
||||
// Verificar que no exista ya un cierre para esa fecha
|
||||
var yaExiste = await _context.DiezmoCierres
|
||||
.AnyAsync(c => c.Fecha == fecha && !c.Eliminado);
|
||||
|
||||
if (yaExiste)
|
||||
throw new InvalidOperationException($"Ya existe un cierre para la fecha {fecha:dd/MM/yyyy}.");
|
||||
|
||||
var cierre = new DiezmoCierre
|
||||
{
|
||||
Fecha = fecha,
|
||||
Observaciones = observaciones,
|
||||
CreadoPor = creadoPor,
|
||||
CreadoEn = DateTime.UtcNow,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.DiezmoCierres.Add(cierre);
|
||||
await _context.SaveChangesAsync();
|
||||
return cierre;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Detalles
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task AgregarDetalleAsync(long cierreId, DiezmoDetalleFormViewModel vm, string usuario)
|
||||
{
|
||||
var cierre = await GetCierreOrThrowAsync(cierreId);
|
||||
GuardarSiAbierto(cierre);
|
||||
|
||||
// Se invierte la lógica: El net (Diezmo) se introduce manual. El cambio es derivado.
|
||||
var neto = vm.MontoNeto;
|
||||
var cambio = vm.MontoEntregado - neto;
|
||||
if (cambio < 0) cambio = 0; // Prevenir errores; si entregó menos del neto se asume cambio 0
|
||||
|
||||
var detalle = new DiezmoDetalle
|
||||
{
|
||||
DiezmoCierreId = cierreId,
|
||||
MiembroId = vm.MiembroId,
|
||||
MontoEntregado = vm.MontoEntregado,
|
||||
CambioEntregado = cambio,
|
||||
MontoNeto = neto,
|
||||
Observaciones = vm.Observaciones,
|
||||
Fecha = DateTime.UtcNow,
|
||||
CreadoPor = usuario,
|
||||
CreadoEn = DateTime.UtcNow,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.DiezmoDetalles.Add(detalle);
|
||||
await _context.SaveChangesAsync();
|
||||
await RecalcularTotalesAsync(cierreId);
|
||||
}
|
||||
|
||||
public async Task EliminarDetalleAsync(long detalleId, string usuario)
|
||||
{
|
||||
var detalle = await _context.DiezmoDetalles
|
||||
.FirstOrDefaultAsync(d => d.Id == detalleId && !d.Eliminado)
|
||||
?? throw new InvalidOperationException("Detalle no encontrado.");
|
||||
|
||||
var cierre = await GetCierreOrThrowAsync(detalle.DiezmoCierreId);
|
||||
GuardarSiAbierto(cierre);
|
||||
|
||||
detalle.Eliminado = true;
|
||||
detalle.ActualizadoEn = DateTime.UtcNow;
|
||||
detalle.ActualizadoPor = usuario;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await RegistrarBitacoraAsync(detalle.DiezmoCierreId, "ELIMINAR_DETALLE",
|
||||
$"Detalle #{detalleId} eliminado", usuario);
|
||||
await RecalcularTotalesAsync(detalle.DiezmoCierreId);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Salidas
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task AgregarSalidaAsync(long cierreId, DiezmoSalidaFormViewModel vm, string usuario)
|
||||
{
|
||||
var cierre = await GetCierreOrThrowAsync(cierreId);
|
||||
GuardarSiAbierto(cierre);
|
||||
|
||||
var salida = new DiezmoSalida
|
||||
{
|
||||
DiezmoCierreId = cierreId,
|
||||
TipoSalidaId = vm.TipoSalidaId,
|
||||
BeneficiarioId = vm.BeneficiarioId,
|
||||
Monto = vm.Monto,
|
||||
Concepto = vm.Concepto,
|
||||
Fecha = DateTime.UtcNow,
|
||||
CreadoPor = usuario,
|
||||
CreadoEn = DateTime.UtcNow,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.DiezmoSalidas.Add(salida);
|
||||
await _context.SaveChangesAsync();
|
||||
await RecalcularTotalesAsync(cierreId);
|
||||
}
|
||||
|
||||
public async Task EliminarSalidaAsync(long salidaId, string usuario)
|
||||
{
|
||||
var salida = await _context.DiezmoSalidas
|
||||
.FirstOrDefaultAsync(s => s.Id == salidaId && !s.Eliminado)
|
||||
?? throw new InvalidOperationException("Salida no encontrada.");
|
||||
|
||||
var cierre = await GetCierreOrThrowAsync(salida.DiezmoCierreId);
|
||||
GuardarSiAbierto(cierre);
|
||||
|
||||
salida.Eliminado = true;
|
||||
salida.ActualizadoEn = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
await RegistrarBitacoraAsync(salida.DiezmoCierreId, "ELIMINAR_SALIDA",
|
||||
$"Salida #{salidaId} eliminada", usuario);
|
||||
await RecalcularTotalesAsync(salida.DiezmoCierreId);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Flujo de cierre / reapertura
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task CerrarCierreAsync(long cierreId, string usuario)
|
||||
{
|
||||
var cierre = await GetCierreByIdAsync(cierreId)
|
||||
?? throw new InvalidOperationException("Cierre no encontrado.");
|
||||
|
||||
if (cierre.Cerrado)
|
||||
throw new InvalidOperationException("El cierre ya se encuentra cerrado.");
|
||||
|
||||
// Recalcular por si hay cambios recientes antes de sellar
|
||||
_calculo.RecalcularTotales(cierre);
|
||||
|
||||
cierre.Cerrado = true;
|
||||
cierre.FechaCierre = DateTime.UtcNow;
|
||||
cierre.CerradoPor = usuario;
|
||||
cierre.ActualizadoEn = DateTime.UtcNow;
|
||||
cierre.ActualizadoPor = usuario;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await RegistrarBitacoraAsync(cierreId, "CIERRE", $"Cierre sellado. Saldo final: {cierre.SaldoFinal:C}", usuario);
|
||||
}
|
||||
|
||||
public async Task ReabrirCierreAsync(long cierreId, string usuario)
|
||||
{
|
||||
var cierre = await GetCierreOrThrowAsync(cierreId);
|
||||
|
||||
if (!cierre.Cerrado)
|
||||
throw new InvalidOperationException("El cierre ya se encuentra abierto.");
|
||||
|
||||
cierre.Cerrado = false;
|
||||
cierre.FechaCierre = null;
|
||||
cierre.CerradoPor = null;
|
||||
cierre.ActualizadoEn = DateTime.UtcNow;
|
||||
cierre.ActualizadoPor = usuario;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await RegistrarBitacoraAsync(cierreId, "REAPERTURA", "Cierre reabierto", usuario);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Totales
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public async Task RecalcularTotalesAsync(long cierreId)
|
||||
{
|
||||
var cierre = await GetCierreByIdAsync(cierreId)
|
||||
?? throw new InvalidOperationException("Cierre no encontrado.");
|
||||
|
||||
_calculo.RecalcularTotales(cierre);
|
||||
cierre.ActualizadoEn = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Helpers privados
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private async Task<DiezmoCierre> GetCierreOrThrowAsync(long id)
|
||||
=> await _context.DiezmoCierres.FirstOrDefaultAsync(c => c.Id == id && !c.Eliminado)
|
||||
?? throw new InvalidOperationException("Cierre no encontrado.");
|
||||
|
||||
private static void GuardarSiAbierto(DiezmoCierre cierre)
|
||||
{
|
||||
if (cierre.Cerrado)
|
||||
throw new InvalidOperationException("No se puede modificar un cierre que ya está cerrado.");
|
||||
}
|
||||
|
||||
private async Task RegistrarBitacoraAsync(long cierreId, string accion, string detalle, string usuario)
|
||||
{
|
||||
await _context.Database.ExecuteSqlRawAsync(
|
||||
"""
|
||||
INSERT INTO public.diezmo_bitacora (diezmo_cierre_id, accion, detalle, realizado_por, realizado_en)
|
||||
VALUES ({0}, {1}, {2}, {3}, {4})
|
||||
""",
|
||||
cierreId, accion, detalle, usuario, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
44
RS_system/Services/DiezmoReciboService.cs
Normal file
44
RS_system/Services/DiezmoReciboService.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class DiezmoReciboService : IDiezmoReciboService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public DiezmoReciboService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<string> GenerarNumeroReciboAsync(long salidaId)
|
||||
{
|
||||
var salida = await _context.DiezmoSalidas
|
||||
.FirstOrDefaultAsync(s => s.Id == salidaId && !s.Eliminado)
|
||||
?? throw new InvalidOperationException("Salida no encontrada.");
|
||||
|
||||
// Si ya tiene correlativo, devolverlo
|
||||
if (!string.IsNullOrEmpty(salida.NumeroRecibo))
|
||||
return salida.NumeroRecibo;
|
||||
|
||||
var anio = salida.CreadoEn.Year;
|
||||
var correlativo = $"RECDZ-{anio}-{salidaId:D6}";
|
||||
|
||||
salida.NumeroRecibo = correlativo;
|
||||
salida.ActualizadoEn = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return correlativo;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<DiezmoSalida?> GetSalidaParaReciboAsync(long salidaId)
|
||||
=> await _context.DiezmoSalidas
|
||||
.Include(s => s.DiezmoCierre)
|
||||
.Include(s => s.TipoSalida)
|
||||
.Include(s => s.Beneficiario)
|
||||
.FirstOrDefaultAsync(s => s.Id == salidaId && !s.Eliminado);
|
||||
}
|
||||
16
RS_system/Services/IDiezmoCalculoService.cs
Normal file
16
RS_system/Services/IDiezmoCalculoService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Models.ViewModels;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IDiezmoCalculoService
|
||||
{
|
||||
/// <summary>Calcula el monto neto de un detalle: MontoEntregado - CambioEntregado.</summary>
|
||||
decimal CalcularMontoNeto(decimal montoEntregado, decimal cambioEntregado);
|
||||
|
||||
/// <summary>
|
||||
/// Recalcula todos los totales del cierre a partir de sus detalles y salidas activos.
|
||||
/// Retorna el cierre con los valores actualizados (sin guardar en BD).
|
||||
/// </summary>
|
||||
DiezmoCierre RecalcularTotales(DiezmoCierre cierre);
|
||||
}
|
||||
31
RS_system/Services/IDiezmoCierreService.cs
Normal file
31
RS_system/Services/IDiezmoCierreService.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Models.ViewModels;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IDiezmoCierreService
|
||||
{
|
||||
// ── Catálogos ──
|
||||
Task<List<DiezmoTipoSalida>> GetTiposSalidaActivosAsync();
|
||||
Task<List<DiezmoBeneficiario>> GetBeneficiariosActivosAsync();
|
||||
|
||||
// ── Cierres ──
|
||||
Task<List<DiezmoCierre>> GetCierresAsync(int? anio = null);
|
||||
Task<DiezmoCierre?> GetCierreByIdAsync(long id);
|
||||
Task<DiezmoCierre> CrearCierreAsync(DateOnly fecha, string? observaciones, string creadoPor);
|
||||
|
||||
// ── Detalles ──
|
||||
Task AgregarDetalleAsync(long cierreId, DiezmoDetalleFormViewModel vm, string usuario);
|
||||
Task EliminarDetalleAsync(long detalleId, string usuario);
|
||||
|
||||
// ── Salidas ──
|
||||
Task AgregarSalidaAsync(long cierreId, DiezmoSalidaFormViewModel vm, string usuario);
|
||||
Task EliminarSalidaAsync(long salidaId, string usuario);
|
||||
|
||||
// ── Flujo de cierre ──
|
||||
Task CerrarCierreAsync(long cierreId, string usuario);
|
||||
Task ReabrirCierreAsync(long cierreId, string usuario);
|
||||
|
||||
// ── Totales ──
|
||||
Task RecalcularTotalesAsync(long cierreId);
|
||||
}
|
||||
16
RS_system/Services/IDiezmoReciboService.cs
Normal file
16
RS_system/Services/IDiezmoReciboService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IDiezmoReciboService
|
||||
{
|
||||
/// <summary>
|
||||
/// Genera (o recupera) el correlativo de recibo para una salida.
|
||||
/// Formato: RECDZ-{AAAA}-{id:D6}
|
||||
/// Persiste el numero_recibo en la tabla diezmo_salidas.
|
||||
/// </summary>
|
||||
Task<string> GenerarNumeroReciboAsync(long salidaId);
|
||||
|
||||
/// <summary>Obtiene todos los datos necesarios para renderizar el recibo.</summary>
|
||||
Task<DiezmoSalida?> GetSalidaParaReciboAsync(long salidaId);
|
||||
}
|
||||
49
RS_system/Views/Diezmo/Create.cshtml
Normal file
49
RS_system/Views/Diezmo/Create.cshtml
Normal file
@@ -0,0 +1,49 @@
|
||||
@model Rs_system.Models.ViewModels.DiezmoCierreCreateViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Nuevo Cierre de Diezmos";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h4 class="mb-1"><i class="bi bi-plus-circle me-2"></i>Nuevo Cierre de Diezmos</h4>
|
||||
<p class="text-muted mb-0">Registra un nuevo período de diezmos</p>
|
||||
</div>
|
||||
<a asp-action="Index" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card-custom">
|
||||
<form asp-action="Create" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-danger mb-3" style="display:none;"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Fecha" class="form-label"></label>
|
||||
<input asp-for="Fecha" type="date" class="form-control" />
|
||||
<span asp-validation-for="Fecha" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label asp-for="Observaciones" class="form-label"></label>
|
||||
<textarea asp-for="Observaciones" class="form-control" rows="3"
|
||||
placeholder="Opcional — notas o descripción del cierre"></textarea>
|
||||
<span asp-validation-for="Observaciones" class="text-danger small"></span>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary-custom">
|
||||
<i class="bi bi-check-lg me-1"></i> Crear Cierre
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
465
RS_system/Views/Diezmo/Detail.cshtml
Normal file
465
RS_system/Views/Diezmo/Detail.cshtml
Normal file
@@ -0,0 +1,465 @@
|
||||
@model Rs_system.Models.ViewModels.DiezmoCierreDetalleViewModel
|
||||
@{
|
||||
ViewData["Title"] = $"Cierre {Model.Fecha:dd/MM/yyyy}";
|
||||
var cerrado = Model.Cerrado;
|
||||
}
|
||||
|
||||
<!-- Cabecera -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h4 class="mb-1">
|
||||
<i class="bi bi-cash-coin me-2"></i>Cierre de Diezmos — @Model.Fecha.ToString("dd/MM/yyyy")
|
||||
<span class="@Model.EstadoBadge ms-2">@Model.EstadoTexto</span>
|
||||
</h4>
|
||||
@if (!string.IsNullOrEmpty(Model.Observaciones))
|
||||
{
|
||||
<p class="text-muted mb-0">@Model.Observaciones</p>
|
||||
}
|
||||
@if (cerrado && Model.FechaCierre.HasValue)
|
||||
{
|
||||
<small class="text-muted">Cerrado por <strong>@Model.CerradoPor</strong> el @Model.FechaCierre.Value.ToLocalTime().ToString("dd/MM/yyyy HH:mm")</small>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Volver
|
||||
</a>
|
||||
@if (!cerrado)
|
||||
{
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="confirmClose(@Model.Id)">
|
||||
<i class="bi bi-lock me-1"></i>Cerrar cierre
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="btn btn-outline-warning btn-sm" onclick="confirmReopen(@Model.Id)">
|
||||
<i class="bi bi-unlock me-1"></i>Reabrir
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle me-1"></i> @TempData["SuccessMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i> @TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (cerrado)
|
||||
{
|
||||
<div class="alert alert-secondary d-flex align-items-center mb-4">
|
||||
<i class="bi bi-lock-fill me-2 fs-5"></i>
|
||||
<strong>Este cierre está sellado.</strong> No se puede modificar. Para editarlo, un Administrador debe reabrirlo.
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- BLOQUE 1 — Resumen de totales -->
|
||||
<div class="row mb-4 g-3">
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card-custom text-center py-3">
|
||||
<h6 class="text-muted small mb-1">Recibido</h6>
|
||||
<h5 class="mb-0" id="uiTotalRecibido">$ @Model.TotalRecibido.ToString("N2")</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card-custom text-center py-3">
|
||||
<h6 class="text-muted small mb-1">Cambio</h6>
|
||||
<h5 class="text-warning mb-0" id="uiTotalCambio">$ @Model.TotalCambio.ToString("N2")</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card-custom text-center py-3">
|
||||
<h6 class="text-muted small mb-1">Neto</h6>
|
||||
<h5 class="text-primary mb-0" id="uiTotalNeto">$ @Model.TotalNeto.ToString("N2")</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card-custom text-center py-3">
|
||||
<h6 class="text-muted small mb-1">Salidas</h6>
|
||||
<h5 class="text-danger mb-0" id="uiTotalSalidas">$ @Model.TotalSalidas.ToString("N2")</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div id="wrapperSaldoFinal" class="card-custom text-center py-3 border border-2 @(Model.SaldoFinal >= 0 ? "border-success" : "border-danger")">
|
||||
<h6 class="text-muted small mb-1">Saldo Final</h6>
|
||||
<h4 id="uiSaldoFinal" class="@(Model.SaldoFinal >= 0 ? "text-success" : "text-danger") mb-0 fw-bold">
|
||||
$ @Model.SaldoFinal.ToString("N2")
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BLOQUE 2 — Diezmos por miembro -->
|
||||
<div class="card-custom mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0"><i class="bi bi-people me-2"></i>Diezmos por Miembro (@Model.Detalles.Count)</h6>
|
||||
@if (!cerrado)
|
||||
{
|
||||
<button type="button" class="btn btn-primary-custom btn-sm" data-bs-toggle="modal" data-bs-target="#modalAddDetalle">
|
||||
<i class="bi bi-plus-lg me-1"></i>Agregar Diezmo
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th>Miembro</th>
|
||||
<th class="text-end">Entregado</th>
|
||||
<th class="text-end">Cambio</th>
|
||||
<th class="text-end">Neto</th>
|
||||
<th>Notas</th>
|
||||
<th class="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Detalles.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">
|
||||
<i class="bi bi-inbox fs-2 d-block mb-1"></i>Sin diezmos registrados
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@{ var i = 1; }
|
||||
@foreach (var d in Model.Detalles)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-muted">@i</td>
|
||||
<td><strong>@d.NombreMiembro</strong></td>
|
||||
<td class="text-end">$ @d.MontoEntregado.ToString("N2")</td>
|
||||
<td class="text-end text-warning">$ @d.CambioEntregado.ToString("N2")</td>
|
||||
<td class="text-end text-primary fw-bold">$ @d.MontoNeto.ToString("N2")</td>
|
||||
<td><small class="text-muted">@d.Observaciones</small></td>
|
||||
<td class="text-center">
|
||||
@if (!cerrado)
|
||||
{
|
||||
<form asp-action="DeleteDetalle" method="post" class="d-inline formDelete" data-confirm-msg="¿Eliminar este diezmo?">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="detalleId" value="@d.Id" />
|
||||
<input type="hidden" name="cierreId" value="@Model.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Eliminar">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
i++;
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BLOQUE 3 — Salidas / Entregas -->
|
||||
<div class="card-custom mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0"><i class="bi bi-box-arrow-up me-2"></i>Salidas y Entregas (@Model.Salidas.Count)</h6>
|
||||
@if (!cerrado)
|
||||
{
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#modalAddSalida">
|
||||
<i class="bi bi-plus-lg me-1"></i>Registrar Salida
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tipo</th>
|
||||
<th>Beneficiario</th>
|
||||
<th>Concepto</th>
|
||||
<th class="text-end">Monto</th>
|
||||
<th>Recibo</th>
|
||||
<th class="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Salidas.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
<i class="bi bi-inbox fs-2 d-block mb-1"></i>Sin salidas registradas
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var s in Model.Salidas)
|
||||
{
|
||||
<tr>
|
||||
<td><span class="badge bg-secondary">@s.TipoSalidaNombre</span></td>
|
||||
<td>@(s.BeneficiarioNombre ?? "—")</td>
|
||||
<td>@s.Concepto</td>
|
||||
<td class="text-end text-danger fw-bold">$ @s.Monto.ToString("N2")</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(s.NumeroRecibo))
|
||||
{
|
||||
<a asp-action="Recibo" asp-route-salidaId="@s.Id" target="_blank"
|
||||
class="badge bg-success text-decoration-none">
|
||||
<i class="bi bi-receipt me-1"></i>@s.NumeroRecibo
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-action="Recibo" asp-route-salidaId="@s.Id" target="_blank"
|
||||
class="btn btn-sm btn-outline-secondary btn-sm py-0">
|
||||
<i class="bi bi-receipt me-1"></i>Generar
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if (!cerrado)
|
||||
{
|
||||
<form asp-action="DeleteSalida" method="post" class="d-inline formDelete" data-confirm-msg="¿Eliminar esta salida?">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="salidaId" value="@s.Id" />
|
||||
<input type="hidden" name="cierreId" value="@Model.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Eliminar">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
MODAL — Agregar Diezmo por Miembro
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
@if (!cerrado)
|
||||
{
|
||||
<div class="modal fade" id="modalAddDetalle" tabindex="-1" aria-labelledby="modalAddDetalleLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalAddDetalleLabel">
|
||||
<i class="bi bi-person-plus me-2"></i>Registrar Diezmo
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="formAddDetalle" asp-action="AddDetalle" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="cierreId" value="@Model.Id" />
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Miembro <span class="text-danger">*</span></label>
|
||||
<select name="MiembroId" class="form-select select2-miembros" required style="width: 100%;">
|
||||
<option value="">— Seleccionar o Escribir —</option>
|
||||
@foreach (var m in Model.MiembrosSelect)
|
||||
{
|
||||
<option value="@m.Value">@m.Text</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Monto entregado <span class="text-danger">*</span></label>
|
||||
<input name="MontoEntregado" type="number" step="0.01" min="0.01"
|
||||
class="form-control fw-bold text-success" id="montoEntregado" oninput="calcCambio()" required />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Diezmo (Neto) <span class="text-danger">*</span></label>
|
||||
<input name="MontoNeto" type="number" step="0.01" min="0.01"
|
||||
class="form-control fw-bold text-primary" id="montoNeto" oninput="calcCambio()" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 bg-light border p-2 rounded text-end">
|
||||
<small class="text-muted mb-0 d-block">Cambio a devolver:</small>
|
||||
<strong id="cambioDisplay" class="text-warning fs-5">$ 0.00</strong>
|
||||
<input type="hidden" name="CambioEntregado" id="cambioEntregado" value="0" />
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="form-label">Observaciones</label>
|
||||
<input name="Observaciones" type="text" class="form-control"
|
||||
placeholder="Opcional" maxlength="300" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary-custom">
|
||||
<i class="bi bi-check-lg me-1"></i>Guardar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
MODAL — Registrar Salida
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
<div class="modal fade" id="modalAddSalida" tabindex="-1" aria-labelledby="modalAddSalidaLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalAddSalidaLabel">
|
||||
<i class="bi bi-box-arrow-up me-2"></i>Registrar Salida
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form id="formAddSalida" asp-action="AddSalida" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="cierreId" value="@Model.Id" />
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tipo de salida <span class="text-danger">*</span></label>
|
||||
<select name="TipoSalidaId" id="tipoSalidaSelect" class="form-select" required>
|
||||
<option value="">— Seleccionar —</option>
|
||||
@foreach (var t in Model.TiposSalidaSelect)
|
||||
{
|
||||
<option value="@t.Value">@t.Text</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Beneficiario</label>
|
||||
<select name="BeneficiarioId" class="form-select">
|
||||
<option value="">— Sin beneficiario —</option>
|
||||
@foreach (var b in Model.BeneficiariosSelect)
|
||||
{
|
||||
<option value="@b.Value">@b.Text</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Monto <span class="text-danger">*</span></label>
|
||||
<input name="Monto" type="number" step="0.01" min="0.01"
|
||||
class="form-control" required />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Concepto <span class="text-danger">*</span></label>
|
||||
<input name="Concepto" id="s_concepto" type="text" class="form-control"
|
||||
placeholder="Descripción de la salida" maxlength="300" required />
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info py-2 small mb-3 text-center">
|
||||
Total del Diezmo Recibido (Neto): <strong>$ @Model.TotalNeto.ToString("N2")</strong>
|
||||
</div>
|
||||
|
||||
@if (Model.SaldoFinal > 0)
|
||||
{
|
||||
<div class="alert alert-warning py-2 small mb-0">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
Saldo disponible: <strong>$ @Model.SaldoFinal.ToString("N2")</strong>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-check-lg me-1"></i>Registrar Salida
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Formularios ocultos para cierre/reapertura -->
|
||||
<form id="formCerrar" asp-action="Close" method="post" style="display:none;">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" value="@Model.Id" />
|
||||
</form>
|
||||
<form id="formReabrir" asp-action="Reopen" method="post" style="display:none;">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" value="@Model.Id" />
|
||||
</form>
|
||||
|
||||
<!-- CSS para Select2 (asumimos que está en el layout o lo cargamos por CDN si no está) -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
||||
|
||||
@section Scripts {
|
||||
<!-- JS para Select2 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Inicializar Select2
|
||||
$('.select2-miembros').select2({
|
||||
theme: "bootstrap-5",
|
||||
dropdownParent: $('#modalAddDetalle'),
|
||||
placeholder: "Buscar por nombre, apellido o num...",
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
language: {
|
||||
noResults: function() { return "No se encontraron miembros"; },
|
||||
searching: function() { return "Buscando..."; }
|
||||
}
|
||||
});
|
||||
|
||||
// Lógica para auto-llenar el Concepto de la salida según el Tipo seleccionado
|
||||
$('#tipoSalidaSelect').on('change', function() {
|
||||
var selectedText = $(this).find("option:selected").text();
|
||||
var currentConcepto = $('#s_concepto').val();
|
||||
if (selectedText && selectedText !== '— Seleccionar —') {
|
||||
if (currentConcepto === '' || currentConcepto === '— Seleccionar —' || currentConcepto !== selectedText) {
|
||||
$('#s_concepto').val(selectedText);
|
||||
}
|
||||
} else {
|
||||
$('#s_concepto').val('');
|
||||
}
|
||||
});
|
||||
|
||||
// Lógica de validación Delete
|
||||
$('.formDelete').on('submit', function (e) {
|
||||
var msg = $(this).data('confirm-msg') || '¿Está seguro de eliminar este registro?';
|
||||
if (!confirm(msg)) e.preventDefault();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Calculo interactivo del cambio (Solo Lectura) en el formulario de Diezmo
|
||||
function calcCambio() {
|
||||
let entregado = parseFloat(document.getElementById('montoEntregado').value) || 0;
|
||||
let neto = parseFloat(document.getElementById('montoNeto').value) || 0;
|
||||
let cambio = entregado - neto;
|
||||
|
||||
if (cambio < 0) cambio = 0;
|
||||
|
||||
document.getElementById('cambioDisplay').textContent = '$ ' + cambio.toFixed(2);
|
||||
document.getElementById('cambioEntregado').value = cambio.toFixed(2);
|
||||
}
|
||||
|
||||
function confirmClose(id) {
|
||||
Swal.fire({
|
||||
title: '¿Cerrar este cierre?',
|
||||
text: 'Una vez cerrado, no se podrán agregar ni modificar diezmos ni salidas.',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#198754',
|
||||
cancelButtonColor: '#6c757d',
|
||||
confirmButtonText: 'Sí, cerrar',
|
||||
cancelButtonText: 'Cancelar'
|
||||
}).then(r => { if (r.isConfirmed) document.getElementById('formCerrar').submit(); });
|
||||
}
|
||||
|
||||
function confirmReopen(id) {
|
||||
Swal.fire({
|
||||
title: '¿Reabrir este cierre?',
|
||||
text: 'Esto permitirá nuevamente editar diezmos y salidas.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#ffc107',
|
||||
cancelButtonColor: '#6c757d',
|
||||
confirmButtonText: 'Sí, reabrir',
|
||||
cancelButtonText: 'Cancelar'
|
||||
}).then(r => { if (r.isConfirmed) document.getElementById('formReabrir').submit(); });
|
||||
}
|
||||
</script>
|
||||
}
|
||||
138
RS_system/Views/Diezmo/Index.cshtml
Normal file
138
RS_system/Views/Diezmo/Index.cshtml
Normal file
@@ -0,0 +1,138 @@
|
||||
@model List<Rs_system.Models.ViewModels.DiezmoCierreListViewModel>
|
||||
@{
|
||||
ViewData["Title"] = "Registro de Diezmos";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h4 class="mb-1"><i class="bi bi-cash-coin me-2"></i>Registro de Diezmos</h4>
|
||||
<p class="text-muted mb-0">Gestión de cierres periódicos de diezmos</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownCatalogos" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-gear me-1"></i> Catálogos
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownCatalogos">
|
||||
<li><a class="dropdown-item" asp-controller="DiezmoCatalogo" asp-action="TiposSalida"><i class="bi bi-tags me-2 text-muted"></i>Tipos de Salida</a></li>
|
||||
<li><a class="dropdown-item" asp-controller="DiezmoCatalogo" asp-action="Beneficiarios"><i class="bi bi-people me-2 text-muted"></i>Beneficiarios</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a asp-action="Create" class="btn btn-primary-custom">
|
||||
<i class="bi bi-plus-lg me-1"></i> Nuevo Cierre
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle me-1"></i> @TempData["SuccessMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i> @TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Filtro por año -->
|
||||
<div class="card-custom mb-4">
|
||||
<form method="get" class="row g-3 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Año</label>
|
||||
<select name="anio" class="form-select" asp-items="@(ViewBag.Anios as List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>)">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
<i class="bi bi-funnel me-1"></i> Filtrar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>document.querySelector('select[name="anio"]').value = '@ViewBag.AnioActual';</script>
|
||||
|
||||
<!-- Tarjetas resumen del período -->
|
||||
@{
|
||||
var totalNeto = Model.Sum(c => c.TotalNeto);
|
||||
var totalSalidas = Model.Sum(c => c.TotalSalidas);
|
||||
var saldoTotal = Model.Sum(c => c.SaldoFinal);
|
||||
}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card-custom text-center">
|
||||
<h6 class="text-muted mb-2">Total Neto del Período</h6>
|
||||
<h3 class="text-primary mb-0">$ @totalNeto.ToString("N2")</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card-custom text-center">
|
||||
<h6 class="text-muted mb-2">Total Salidas</h6>
|
||||
<h3 class="text-warning mb-0">$ @totalSalidas.ToString("N2")</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card-custom text-center">
|
||||
<h6 class="text-muted mb-2">Saldo Acumulado</h6>
|
||||
<h3 class="@(saldoTotal >= 0 ? "text-success" : "text-danger") mb-0">$ @saldoTotal.ToString("N2")</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de cierres -->
|
||||
<div class="card-custom">
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th class="text-center">Estado</th>
|
||||
<th class="text-end">Total Recibido</th>
|
||||
<th class="text-end">Total Neto</th>
|
||||
<th class="text-end">Salidas</th>
|
||||
<th class="text-end">Saldo Final</th>
|
||||
<th class="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-5">
|
||||
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
||||
No hay cierres registrados para el año seleccionado
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var cierre in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<strong>@cierre.Fecha.ToString("dd/MM/yyyy")</strong>
|
||||
<br><small class="text-muted">@cierre.Fecha.DayOfWeek</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="@cierre.EstadoBadge">@cierre.EstadoTexto</span>
|
||||
</td>
|
||||
<td class="text-end">$ @cierre.TotalRecibido.ToString("N2")</td>
|
||||
<td class="text-end">$ @cierre.TotalNeto.ToString("N2")</td>
|
||||
<td class="text-end text-warning">$ @cierre.TotalSalidas.ToString("N2")</td>
|
||||
<td class="text-end @(cierre.SaldoFinal >= 0 ? "text-success" : "text-danger") fw-bold">
|
||||
$ @cierre.SaldoFinal.ToString("N2")
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a asp-action="Detail" asp-route-id="@cierre.Id"
|
||||
class="btn btn-sm btn-outline-primary" title="Ver detalle">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
99
RS_system/Views/Diezmo/Recibo.cshtml
Normal file
99
RS_system/Views/Diezmo/Recibo.cshtml
Normal file
@@ -0,0 +1,99 @@
|
||||
@model Rs_system.Models.DiezmoSalida
|
||||
@{
|
||||
ViewData["Title"] = $"Recibo {ViewBag.NumeroRecibo}";
|
||||
Layout = null; // layout propio para impresión
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Recibo @ViewBag.NumeroRecibo</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 20px; color: #212529; }
|
||||
.recibo { max-width: 600px; margin: auto; border: 2px solid #343a40; border-radius: 8px; padding: 30px; }
|
||||
.recibo-header { text-align: center; border-bottom: 2px dashed #ced4da; padding-bottom: 16px; margin-bottom: 20px; }
|
||||
.recibo-header h2 { margin: 0 0 4px; font-size: 1.6rem; }
|
||||
.recibo-header p { margin: 0; color: #6c757d; font-size: .9rem; }
|
||||
.recibo-nro { font-size: 1.1rem; font-weight: bold; color: #0d6efd; }
|
||||
.recibo-body table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
|
||||
.recibo-body tr td { padding: 8px 12px; }
|
||||
.recibo-body tr td:first-child { font-weight: 600; color: #6c757d; width: 40%; }
|
||||
.recibo-monto { text-align: center; background: #f8f9fa; border-radius: 6px; padding: 16px; margin-bottom: 20px; }
|
||||
.recibo-monto h3 { margin: 0; font-size: 2rem; color: #198754; }
|
||||
.recibo-footer { border-top: 2px dashed #ced4da; padding-top: 16px; font-size: .8rem; color: #6c757d; display: flex; justify-content: space-between; }
|
||||
.firma { margin-top: 40px; text-align: center; }
|
||||
.firma-linea { border-top: 1px solid #343a40; width: 220px; margin: auto; padding-top: 6px; }
|
||||
@@media print {
|
||||
body { padding: 0; }
|
||||
.no-print { display: none !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="no-print mb-3" style="text-align:center">
|
||||
<button onclick="window.print()" style="padding:8px 20px;cursor:pointer">
|
||||
🖨️ Imprimir / Guardar PDF
|
||||
</button>
|
||||
<button onclick="window.close()" style="padding:8px 20px;cursor:pointer;margin-left:8px">
|
||||
✕ Cerrar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="recibo">
|
||||
<!-- Encabezado -->
|
||||
<div class="recibo-header">
|
||||
<h2>Recibo de Diezmos</h2>
|
||||
<p>Iglesia — módulo de Diezmos</p>
|
||||
<div class="recibo-nro mt-2">@ViewBag.NumeroRecibo</div>
|
||||
</div>
|
||||
|
||||
<!-- Datos -->
|
||||
<div class="recibo-body">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Fecha:</td>
|
||||
<td>@Model.Fecha.ToLocalTime().ToString("dd/MM/yyyy HH:mm")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tipo:</td>
|
||||
<td>@(Model.TipoSalida?.Nombre ?? "—")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Beneficiario:</td>
|
||||
<td>@(Model.Beneficiario?.Nombre ?? "No especificado")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Concepto:</td>
|
||||
<td>@Model.Concepto</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cierre:</td>
|
||||
<td>@(Model.DiezmoCierre?.Fecha.ToString("dd/MM/yyyy") ?? "—")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Emitido por:</td>
|
||||
<td>@ViewBag.Emisor</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Monto destacado -->
|
||||
<div class="recibo-monto">
|
||||
<small style="color:#6c757d">MONTO</small>
|
||||
<h3>$ @Model.Monto.ToString("N2")</h3>
|
||||
</div>
|
||||
|
||||
<!-- Firma -->
|
||||
<div class="firma">
|
||||
<div class="firma-linea">Firma del receptor</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pie -->
|
||||
<div class="recibo-footer">
|
||||
<span>Generado: @DateTime.Now.ToString("dd/MM/yyyy HH:mm")</span>
|
||||
<span>@ViewBag.NumeroRecibo</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
136
RS_system/Views/DiezmoCatalogo/Beneficiarios.cshtml
Normal file
136
RS_system/Views/DiezmoCatalogo/Beneficiarios.cshtml
Normal file
@@ -0,0 +1,136 @@
|
||||
@model IEnumerable<Rs_system.Models.DiezmoBeneficiario>
|
||||
@{
|
||||
ViewData["Title"] = "Catálogo de Beneficiarios";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h4 class="mb-1">
|
||||
<i class="bi bi-people me-2"></i>Catálogo de Beneficiarios
|
||||
</h4>
|
||||
<p class="text-muted mb-0">Personas o entidades externas que reciben salidas de fondos.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-controller="Diezmo" asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Volver a Diezmos
|
||||
</a>
|
||||
<button type="button" class="btn btn-primary-custom btn-sm" onclick="openModal(0, '', '')">
|
||||
<i class="bi bi-plus-lg me-1"></i>Nuevo Beneficiario
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle me-1"></i> @TempData["SuccessMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i> @TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-custom">
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom" id="tblBeneficiarios">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Descripción</th>
|
||||
<th class="text-center">Estado</th>
|
||||
<th class="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td class="fw-bold">@item.Nombre</td>
|
||||
<td><small class="text-muted">@item.Descripcion</small></td>
|
||||
<td class="text-center">
|
||||
@if (item.Activo)
|
||||
{
|
||||
<span class="badge bg-success">Activo</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Inactivo</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
onclick="openModal(@item.Id, '@item.Nombre.Replace("'","\\'")', '@(item.Descripcion?.Replace("'","\\'") ?? "")')"
|
||||
title="Editar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<form asp-action="EliminarBeneficiario" method="post" class="d-inline"
|
||||
onsubmit="return confirm('¿Seguro que desea eliminar este beneficiario?')">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" value="@item.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Eliminar">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-4">Sin registros.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal CRUD -->
|
||||
<div class="modal fade" id="modalCrud" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Beneficiario</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form asp-action="GuardarBeneficiario" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="Id" id="b_id" value="0" />
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre <span class="text-danger">*</span></label>
|
||||
<input type="text" name="Nombre" id="b_nombre" class="form-control" required maxlength="150" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Descripción</label>
|
||||
<textarea name="Descripcion" id="b_desc" class="form-control" rows="3" maxlength="300"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary-custom">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function openModal(id, nombre, desc) {
|
||||
document.getElementById('b_id').value = id;
|
||||
document.getElementById('b_nombre').value = nombre;
|
||||
document.getElementById('b_desc').value = desc;
|
||||
|
||||
document.getElementById('modalTitle').innerText = id === 0 ? 'Nuevo Beneficiario' : 'Editar Beneficiario';
|
||||
|
||||
var modal = new bootstrap.Modal(document.getElementById('modalCrud'));
|
||||
modal.show();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
142
RS_system/Views/DiezmoCatalogo/TiposSalida.cshtml
Normal file
142
RS_system/Views/DiezmoCatalogo/TiposSalida.cshtml
Normal file
@@ -0,0 +1,142 @@
|
||||
@model IEnumerable<Rs_system.Models.DiezmoTipoSalida>
|
||||
@{
|
||||
ViewData["Title"] = "Catálogo: Tipos de Salida";
|
||||
}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h4 class="mb-1">
|
||||
<i class="bi bi-tags me-2"></i>Tipos de Salida
|
||||
</h4>
|
||||
<p class="text-muted mb-0">Gestión de conceptos o clasificaciones para salidas de caja.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a asp-controller="Diezmo" asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Volver a Diezmos
|
||||
</a>
|
||||
<button type="button" class="btn btn-primary-custom btn-sm" onclick="openModal(0, '', '', false)">
|
||||
<i class="bi bi-plus-lg me-1"></i>Nuevo Tipo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle me-1"></i> @TempData["SuccessMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i> @TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-custom">
|
||||
<div class="table-responsive">
|
||||
<table class="table-custom">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Descripción</th>
|
||||
<th class="text-center">Tipo Especial</th>
|
||||
<th class="text-center">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td class="fw-bold">@item.Nombre</td>
|
||||
<td><small class="text-muted">@item.Descripcion</small></td>
|
||||
<td class="text-center">
|
||||
@if (item.EsEntregaPastor)
|
||||
{
|
||||
<span class="badge bg-info text-dark"><i class="bi bi-person-check me-1"></i>Entrega Pastor</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">—</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
onclick="openModal(@item.Id, '@item.Nombre.Replace("'","\\'")', '@(item.Descripcion?.Replace("'","\\'") ?? "")', @(item.EsEntregaPastor.ToString().ToLower()))"
|
||||
title="Editar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<form asp-action="EliminarTipoSalida" method="post" class="d-inline"
|
||||
onsubmit="return confirm('¿Seguro que desea eliminar este tipo?')">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="id" value="@item.Id" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Eliminar">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-4">Sin registros.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal CRUD -->
|
||||
<div class="modal fade" id="modalCrud" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Tipo de Salida</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form asp-action="GuardarTipoSalida" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="Id" id="t_id" value="0" />
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nombre <span class="text-danger">*</span></label>
|
||||
<input type="text" name="Nombre" id="t_nombre" class="form-control" required maxlength="100" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Descripción</label>
|
||||
<textarea name="Descripcion" id="t_desc" class="form-control" rows="2" maxlength="300"></textarea>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-3">
|
||||
<input class="form-check-input" type="checkbox" name="EsEntregaPastor" id="t_esPastor" value="true">
|
||||
<label class="form-check-label" for="t_esPastor">Este tipo indica una "Entrega Directa al Pastor"</label>
|
||||
<small class="d-block text-muted">Útil a nivel contable para identificar la obligación central del diezmo.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary-custom">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function openModal(id, nombre, desc, esPastor) {
|
||||
document.getElementById('t_id').value = id;
|
||||
document.getElementById('t_nombre').value = nombre;
|
||||
document.getElementById('t_desc').value = desc;
|
||||
document.getElementById('t_esPastor').checked = esPastor;
|
||||
|
||||
document.getElementById('modalTitle').innerText = id === 0 ? 'Nuevo Tipo de Salida' : 'Editar Tipo de Salida';
|
||||
|
||||
var modal = new bootstrap.Modal(document.getElementById('modalCrud'));
|
||||
modal.show();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
65
RS_system/Views/Miembro/Importar.cshtml
Normal file
65
RS_system/Views/Miembro/Importar.cshtml
Normal file
@@ -0,0 +1,65 @@
|
||||
@{
|
||||
ViewData["Title"] = "Importar Miembros";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Importar Miembros desde CSV</h1>
|
||||
<a asp-action="Index" class="btn btn-sm btn-secondary shadow-sm">
|
||||
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Volver a la Lista
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Cargar Archivo CSV</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
@if (ViewBag.Errors != null)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<h4 class="alert-heading">Errores encontrados:</h4>
|
||||
<p>Por favor corrija los siguientes errores en el archivo CSV y vuelva a intentarlo:</p>
|
||||
<hr>
|
||||
<ul class="mb-0">
|
||||
@foreach (var error in ViewBag.Errors)
|
||||
{
|
||||
<li>@error</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="alert alert-info">
|
||||
<h5>Instrucciones:</h5>
|
||||
<p>El archivo CSV debe tener las siguientes columnas en este orden exacto:</p>
|
||||
<ol>
|
||||
<li>Nombres</li>
|
||||
<li>Apellidos</li>
|
||||
<li>Fecha Nacimiento (formato aceptado por el sistema, e.g. YYYY-MM-DD)</li>
|
||||
<li>Fecha Ingreso Congregación (formato aceptado por el sistema)</li>
|
||||
<li>Teléfono</li>
|
||||
<li>Teléfono de Emergencia</li>
|
||||
<li>Dirección</li>
|
||||
<li><strong>ID</strong> del Grupo de Trabajo (Número)</li>
|
||||
<li>Bautizado en Espíritu Santo (Si/1/True)</li>
|
||||
<li>Activo (Si/1/True)</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<form asp-action="Importar" enctype="multipart/form-data" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="file">Seleccionar Archivo CSV</label>
|
||||
<input type="file" name="file" class="form-control-file" id="file" required accept=".csv">
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-3">
|
||||
<input type="submit" value="Importar" class="btn btn-primary" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Binary file not shown.
Binary file not shown.
@@ -15,7 +15,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("RS_system")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+46bf68cb21fcad11c3f8b5ebbeb6ec4b6567d6c9")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+bec656b105cc858404ace22d1d46cd053a5d0fd7")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("RS_system")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("RS_system")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
7e2d659fefb50453fa3da00dc55dfdaadf1c304f5a0fbc1389094f2c740f7326
|
||||
207587d655de51326030a07a0184e67ca76d93743141a95cdbc5efdc3e4541f1
|
||||
|
||||
@@ -152,6 +152,30 @@ build_metadata.AdditionalFiles.CssScope =
|
||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvQ29udGFiaWxpZGFkR2VuZXJhbC9SZWdpc3Ryb01lbnN1YWwuY3NodG1s
|
||||
build_metadata.AdditionalFiles.CssScope =
|
||||
|
||||
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/Diezmo/Create.cshtml]
|
||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvRGllem1vL0NyZWF0ZS5jc2h0bWw=
|
||||
build_metadata.AdditionalFiles.CssScope =
|
||||
|
||||
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/Diezmo/Detail.cshtml]
|
||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvRGllem1vL0RldGFpbC5jc2h0bWw=
|
||||
build_metadata.AdditionalFiles.CssScope =
|
||||
|
||||
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/Diezmo/Index.cshtml]
|
||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvRGllem1vL0luZGV4LmNzaHRtbA==
|
||||
build_metadata.AdditionalFiles.CssScope =
|
||||
|
||||
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/Diezmo/Recibo.cshtml]
|
||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvRGllem1vL1JlY2liby5jc2h0bWw=
|
||||
build_metadata.AdditionalFiles.CssScope =
|
||||
|
||||
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/DiezmoCatalogo/Beneficiarios.cshtml]
|
||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvRGllem1vQ2F0YWxvZ28vQmVuZWZpY2lhcmlvcy5jc2h0bWw=
|
||||
build_metadata.AdditionalFiles.CssScope =
|
||||
|
||||
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/DiezmoCatalogo/TiposSalida.cshtml]
|
||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvRGllem1vQ2F0YWxvZ28vVGlwb3NTYWxpZGEuY3NodG1s
|
||||
build_metadata.AdditionalFiles.CssScope =
|
||||
|
||||
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/Estados/Create.cshtml]
|
||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvRXN0YWRvcy9DcmVhdGUuY3NodG1s
|
||||
build_metadata.AdditionalFiles.CssScope =
|
||||
|
||||
@@ -1 +1 @@
|
||||
1e149c33148abe9fa6194941c5133295585bfc66f5204d7905e01b09846bdccc
|
||||
cf5eb9535656c70fb44bcab29751e1b8ead46c4e4f39ae6fedbde6c1dae54744
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
615
RS_system/sql_migration_to_guid.sql
Normal file
615
RS_system/sql_migration_to_guid.sql
Normal file
@@ -0,0 +1,615 @@
|
||||
-- =====================================================
|
||||
-- SQL Migration Script: Auto-increment IDs to UUIDs
|
||||
-- =====================================================
|
||||
-- WARNING: This is a BREAKING CHANGE. Make a backup first!
|
||||
-- This script converts all primary keys from BIGSERIAL to UUID
|
||||
-- and updates all foreign key references accordingly.
|
||||
--
|
||||
-- INSTRUCTIONS:
|
||||
-- 1. BACKUP your database first: pg_dump -U postgres rs_system > backup.sql
|
||||
-- 2. Test on a copy of the database first
|
||||
-- 3. Run during maintenance window (minimal user activity)
|
||||
-- 4. Verify all data after migration
|
||||
-- =====================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- =====================================================
|
||||
-- STEP 1: Enable UUID extension
|
||||
-- =====================================================
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- =====================================================
|
||||
-- STEP 2: Drop all foreign key constraints
|
||||
-- =====================================================
|
||||
-- (We'll recreate them later with UUID types)
|
||||
|
||||
-- Colaboraciones relationships
|
||||
ALTER TABLE IF EXISTS detalle_colaboracion DROP CONSTRAINT IF EXISTS fk_detalle_colaboracion_colaboracion;
|
||||
ALTER TABLE IF EXISTS detalle_colaboracion DROP CONSTRAINT IF EXISTS fk_detalle_colaboracion_tipo;
|
||||
ALTER TABLE IF EXISTS colaboraciones DROP CONSTRAINT IF EXISTS fk_colaboracion_miembro;
|
||||
|
||||
-- Miembros relationships
|
||||
ALTER TABLE IF EXISTS colaboraciones DROP CONSTRAINT IF EXISTS colaboraciones_miembro_id_fkey;
|
||||
ALTER TABLE IF EXISTS miembros DROP CONSTRAINT IF EXISTS fk_miembro_grupo_trabajo;
|
||||
ALTER TABLE IF EXISTS asistencia_culto DROP CONSTRAINT IF EXISTS fk_asistencia_miembro;
|
||||
|
||||
-- Prestamos relationships
|
||||
ALTER TABLE IF EXISTS prestamos DROP CONSTRAINT IF EXISTS fk_prestamo_miembro;
|
||||
ALTER TABLE IF EXISTS pagos_prestamo DROP CONSTRAINT IF EXISTS fk_pago_prestamo;
|
||||
|
||||
-- Inventory relationships
|
||||
ALTER TABLE IF EXISTS existencias DROP CONSTRAINT IF EXISTS fk_existencia_articulo;
|
||||
ALTER TABLE IF EXISTS existencias DROP CONSTRAINT IF EXISTS fk_existencia_ubicacion;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP CONSTRAINT IF EXISTS fk_movimiento_articulo;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP CONSTRAINT IF EXISTS fk_movimiento_ubicacion_origen;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP CONSTRAINT IF EXISTS fk_movimiento_ubicacion_destino;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP CONSTRAINT IF EXISTS fk_movimiento_usuario;
|
||||
|
||||
-- Contabilidad relationships
|
||||
ALTER TABLE IF EXISTS movimiento_general DROP CONSTRAINT IF EXISTS fk_movimiento_categoria_ingreso;
|
||||
ALTER TABLE IF EXISTS movimiento_general DROP CONSTRAINT IF EXISTS fk_movimiento_categoria_egreso;
|
||||
ALTER TABLE IF EXISTS movimiento_general DROP CONSTRAINT IF EXISTS fk_movimiento_reporte_mensual;
|
||||
ALTER TABLE IF EXISTS movimiento_general_adjunto DROP CONSTRAINT IF EXISTS fk_adjunto_movimiento;
|
||||
ALTER TABLE IF EXISTS contabilidad_registro DROP CONSTRAINT IF EXISTS fk_contabilidad_reporte;
|
||||
|
||||
-- =====================================================
|
||||
-- STEP 3: Add UUID columns and migrate data
|
||||
-- =====================================================
|
||||
|
||||
-- GRUPOS_TRABAJO
|
||||
ALTER TABLE grupos_trabajo ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE grupos_trabajo SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE grupos_trabajo ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- MIEMBROS
|
||||
ALTER TABLE miembros ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE miembros ADD COLUMN grupo_trabajo_id_uuid UUID;
|
||||
UPDATE miembros SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE miembros ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- Update foreign key references
|
||||
UPDATE miembros m
|
||||
SET grupo_trabajo_id_uuid = gt.id_uuid
|
||||
FROM grupos_trabajo gt
|
||||
WHERE m.grupo_trabajo_id = gt.id;
|
||||
|
||||
-- TIPOS_COLABORACION
|
||||
ALTER TABLE tipos_colaboracion ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE tipos_colaboracion SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE tipos_colaboracion ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- COLABORACIONES
|
||||
ALTER TABLE colaboraciones ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE colaboraciones ADD COLUMN miembro_id_uuid UUID;
|
||||
ALTER TABLE colaboraciones ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
|
||||
UPDATE colaboraciones SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE colaboraciones ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- Update foreign key references
|
||||
UPDATE colaboraciones c
|
||||
SET miembro_id_uuid = m.id_uuid
|
||||
FROM miembros m
|
||||
WHERE c.miembro_id = m.id;
|
||||
|
||||
-- DETALLE_COLABORACION
|
||||
ALTER TABLE detalle_colaboracion ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE detalle_colaboracion ADD COLUMN colaboracion_id_uuid UUID;
|
||||
ALTER TABLE detalle_colaboracion ADD COLUMN tipo_colaboracion_id_uuid UUID;
|
||||
UPDATE detalle_colaboracion SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE detalle_colaboracion ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- Update foreign key references
|
||||
UPDATE detalle_colaboracion dc
|
||||
SET colaboracion_id_uuid = c.id_uuid
|
||||
FROM colaboraciones c
|
||||
WHERE dc.colaboracion_id = c.id;
|
||||
|
||||
UPDATE detalle_colaboracion dc
|
||||
SET tipo_colaboracion_id_uuid = tc.id_uuid
|
||||
FROM tipos_colaboracion tc
|
||||
WHERE dc.tipo_colaboracion_id = tc.id;
|
||||
|
||||
-- PRESTAMOS
|
||||
ALTER TABLE prestamos ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE prestamos ADD COLUMN miembro_id_uuid UUID;
|
||||
UPDATE prestamos SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE prestamos ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
UPDATE prestamos p
|
||||
SET miembro_id_uuid = m.id_uuid
|
||||
FROM miembros m
|
||||
WHERE p.miembro_id = m.id;
|
||||
|
||||
-- PAGOS_PRESTAMO
|
||||
ALTER TABLE pagos_prestamo ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE pagos_prestamo ADD COLUMN prestamo_id_uuid UUID;
|
||||
UPDATE pagos_prestamo SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE pagos_prestamo ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
UPDATE pagos_prestamo pp
|
||||
SET prestamo_id_uuid = p.id_uuid
|
||||
FROM prestamos p
|
||||
WHERE pp.prestamo_id = p.id;
|
||||
|
||||
-- ASISTENCIA_CULTO
|
||||
ALTER TABLE asistencia_culto ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE asistencia_culto ADD COLUMN miembro_id_uuid UUID;
|
||||
UPDATE asistencia_culto SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE asistencia_culto ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
UPDATE asistencia_culto ac
|
||||
SET miembro_id_uuid = m.id_uuid
|
||||
FROM miembros m
|
||||
WHERE ac.miembro_id = m.id;
|
||||
|
||||
-- USUARIOS
|
||||
ALTER TABLE usuarios ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE usuarios SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE usuarios ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- OFERNDAS
|
||||
ALTER TABLE oferndas ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE oferndas SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE oferndas ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- CATEGORIA_INGRESO
|
||||
ALTER TABLE categoria_ingreso ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE categoria_ingreso SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE categoria_ingreso ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- CATEGORIA_EGRESO
|
||||
ALTER TABLE categoria_egreso ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE categoria_egreso SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE categoria_egreso ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- REPORTE_MENSUAL_GENERAL
|
||||
ALTER TABLE reporte_mensual_general ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE reporte_mensual_general SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE reporte_mensual_general ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- REPORTE_MENSUAL_CONTABLE
|
||||
ALTER TABLE reporte_mensual_contable ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE reporte_mensual_contable SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE reporte_mensual_contable ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- MOVIMIENTO_GENERAL
|
||||
ALTER TABLE movimiento_general ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE movimiento_general ADD COLUMN categoria_ingreso_id_uuid UUID;
|
||||
ALTER TABLE movimiento_general ADD COLUMN categoria_egreso_id_uuid UUID;
|
||||
ALTER TABLE movimiento_general ADD COLUMN reporte_mensual_id_uuid UUID;
|
||||
UPDATE movimiento_general SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE movimiento_general ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
UPDATE movimiento_general mg
|
||||
SET categoria_ingreso_id_uuid = ci.id_uuid
|
||||
FROM categoria_ingreso ci
|
||||
WHERE mg.categoria_ingreso_id = ci.id;
|
||||
|
||||
UPDATE movimiento_general mg
|
||||
SET categoria_egreso_id_uuid = ce.id_uuid
|
||||
FROM categoria_egreso ce
|
||||
WHERE mg.categoria_egreso_id = ce.id;
|
||||
|
||||
UPDATE movimiento_general mg
|
||||
SET reporte_mensual_id_uuid = rm.id_uuid
|
||||
FROM reporte_mensual_general rm
|
||||
WHERE mg.reporte_mensual_id = rm.id;
|
||||
|
||||
-- MOVIMIENTO_GENERAL_ADJUNTO
|
||||
ALTER TABLE movimiento_general_adjunto ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE movimiento_general_adjunto ADD COLUMN movimiento_id_uuid UUID;
|
||||
UPDATE movimiento_general_adjunto SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE movimiento_general_adjunto ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
UPDATE movimiento_general_adjunto mga
|
||||
SET movimiento_id_uuid = mg.id_uuid
|
||||
FROM movimiento_general mg
|
||||
WHERE mga.movimiento_id = mg.id;
|
||||
|
||||
-- CONTABILIDAD_REGISTRO
|
||||
ALTER TABLE contabilidad_registro ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE contabilidad_registro ADD COLUMN reporte_id_uuid UUID;
|
||||
UPDATE contabilidad_registro SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE contabilidad_registro ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
UPDATE contabilidad_registro cr
|
||||
SET reporte_id_uuid = rm.id_uuid
|
||||
FROM reporte_mensual_contable rm
|
||||
WHERE cr.reporte_id = rm.id;
|
||||
|
||||
-- ARTICULOS (assuming you have this table)
|
||||
ALTER TABLE IF EXISTS articulos ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE articulos SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE IF EXISTS articulos ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- UBICACIONES (assuming you have this table)
|
||||
ALTER TABLE IF EXISTS ubicaciones ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
UPDATE ubicaciones SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE IF EXISTS ubicaciones ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
-- EXISTENCIAS
|
||||
ALTER TABLE IF EXISTS existencias ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE IF EXISTS existencias ADD COLUMN articulo_id_uuid UUID;
|
||||
ALTER TABLE IF EXISTS existencias ADD COLUMN ubicacion_id_uuid UUID;
|
||||
UPDATE existencias SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE IF EXISTS existencias ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
UPDATE existencias e
|
||||
SET articulo_id_uuid = a.id_uuid
|
||||
FROM articulos a
|
||||
WHERE e.articulo_id = a.id;
|
||||
|
||||
UPDATE existencias e
|
||||
SET ubicacion_id_uuid = u.id_uuid
|
||||
FROM ubicaciones u
|
||||
WHERE e.ubicacion_id = u.id;
|
||||
|
||||
-- MOVIMIENTOS_INVENTARIO
|
||||
ALTER TABLE IF EXISTS movimientos_inventario ADD COLUMN id_uuid UUID DEFAULT uuid_generate_v4();
|
||||
ALTER TABLE IF EXISTS movimientos_inventario ADD COLUMN articulo_id_uuid UUID;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario ADD COLUMN ubicacion_origen_id_uuid UUID;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario ADD COLUMN ubicacion_destino_id_uuid UUID;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario ADD COLUMN usuario_id_uuid UUID;
|
||||
UPDATE movimientos_inventario SET id_uuid = uuid_generate_v4() WHERE id_uuid IS NULL;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario ALTER COLUMN id_uuid SET NOT NULL;
|
||||
|
||||
UPDATE movimientos_inventario mi
|
||||
SET articulo_id_uuid = a.id_uuid
|
||||
FROM articulos a
|
||||
WHERE mi.articulo_id = a.id;
|
||||
|
||||
UPDATE movimientos_inventario mi
|
||||
SET ubicacion_origen_id_uuid = u.id_uuid
|
||||
FROM ubicaciones u
|
||||
WHERE mi.ubicacion_origen_id = u.id;
|
||||
|
||||
UPDATE movimientos_inventario mi
|
||||
SET ubicacion_destino_id_uuid = u.id_uuid
|
||||
FROM ubicaciones u
|
||||
WHERE mi.ubicacion_destino_id = u.id;
|
||||
|
||||
UPDATE movimientos_inventario mi
|
||||
SET usuario_id_uuid = usr.id_uuid
|
||||
FROM usuarios usr
|
||||
WHERE mi.usuario_id = usr.id;
|
||||
|
||||
-- =====================================================
|
||||
-- STEP 4: Drop old ID columns and rename UUID columns
|
||||
-- =====================================================
|
||||
|
||||
-- GRUPOS_TRABAJO
|
||||
ALTER TABLE grupos_trabajo DROP CONSTRAINT IF EXISTS grupos_trabajo_pkey;
|
||||
ALTER TABLE grupos_trabajo DROP COLUMN id;
|
||||
ALTER TABLE grupos_trabajo RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE grupos_trabajo ADD PRIMARY KEY (id);
|
||||
|
||||
-- MIEMBROS
|
||||
ALTER TABLE miembros DROP CONSTRAINT IF EXISTS miembros_pkey;
|
||||
ALTER TABLE miembros DROP COLUMN id;
|
||||
ALTER TABLE miembros DROP COLUMN grupo_trabajo_id;
|
||||
ALTER TABLE miembros RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE miembros RENAME COLUMN grupo_trabajo_id_uuid TO grupo_trabajo_id;
|
||||
ALTER TABLE miembros ADD PRIMARY KEY (id);
|
||||
|
||||
-- TIPOS_COLABORACION
|
||||
ALTER TABLE tipos_colaboracion DROP CONSTRAINT IF EXISTS tipos_colaboracion_pkey;
|
||||
ALTER TABLE tipos_colaboracion DROP COLUMN id;
|
||||
ALTER TABLE tipos_colaboracion RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE tipos_colaboracion ADD PRIMARY KEY (id);
|
||||
|
||||
-- COLABORACIONES
|
||||
ALTER TABLE colaboraciones DROP CONSTRAINT IF EXISTS colaboraciones_pkey;
|
||||
ALTER TABLE colaboraciones DROP COLUMN id;
|
||||
ALTER TABLE colaboraciones DROP COLUMN miembro_id;
|
||||
ALTER TABLE colaboraciones RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE colaboraciones RENAME COLUMN miembro_id_uuid TO miembro_id;
|
||||
ALTER TABLE colaboraciones ADD PRIMARY KEY (id);
|
||||
|
||||
-- DETALLE_COLABORACION
|
||||
ALTER TABLE detalle_colaboracion DROP CONSTRAINT IF EXISTS detalle_colaboracion_pkey;
|
||||
ALTER TABLE detalle_colaboracion DROP COLUMN id;
|
||||
ALTER TABLE detalle_colaboracion DROP COLUMN colaboracion_id;
|
||||
ALTER TABLE detalle_colaboracion DROP COLUMN tipo_colaboracion_id;
|
||||
ALTER TABLE detalle_colaboracion RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE detalle_colaboracion RENAME COLUMN colaboracion_id_uuid TO colaboracion_id;
|
||||
ALTER TABLE detalle_colaboracion RENAME COLUMN tipo_colaboracion_id_uuid TO tipo_colaboracion_id;
|
||||
ALTER TABLE detalle_colaboracion ADD PRIMARY KEY (id);
|
||||
|
||||
-- PRESTAMOS
|
||||
ALTER TABLE prestamos DROP CONSTRAINT IF EXISTS prestamos_pkey;
|
||||
ALTER TABLE prestamos DROP COLUMN id;
|
||||
ALTER TABLE prestamos DROP COLUMN miembro_id;
|
||||
ALTER TABLE prestamos RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE prestamos RENAME COLUMN miembro_id_uuid TO miembro_id;
|
||||
ALTER TABLE prestamos ADD PRIMARY KEY (id);
|
||||
|
||||
-- PAGOS_PRESTAMO
|
||||
ALTER TABLE pagos_prestamo DROP CONSTRAINT IF EXISTS pagos_prestamo_pkey;
|
||||
ALTER TABLE pagos_prestamo DROP COLUMN id;
|
||||
ALTER TABLE pagos_prestamo DROP COLUMN prestamo_id;
|
||||
ALTER TABLE pagos_prestamo RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE pagos_prestamo RENAME COLUMN prestamo_id_uuid TO prestamo_id;
|
||||
ALTER TABLE pagos_prestamo ADD PRIMARY KEY (id);
|
||||
|
||||
-- ASISTENCIA_CULTO
|
||||
ALTER TABLE asistencia_culto DROP CONSTRAINT IF EXISTS asistencia_culto_pkey;
|
||||
ALTER TABLE asistencia_culto DROP COLUMN id;
|
||||
ALTER TABLE asistencia_culto DROP COLUMN miembro_id;
|
||||
ALTER TABLE asistencia_culto RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE asistencia_culto RENAME COLUMN miembro_id_uuid TO miembro_id;
|
||||
ALTER TABLE asistencia_culto ADD PRIMARY KEY (id);
|
||||
|
||||
-- USUARIOS
|
||||
ALTER TABLE usuarios DROP CONSTRAINT IF EXISTS usuarios_pkey;
|
||||
ALTER TABLE usuarios DROP COLUMN id;
|
||||
ALTER TABLE usuarios RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE usuarios ADD PRIMARY KEY (id);
|
||||
|
||||
-- OFERNDAS
|
||||
ALTER TABLE oferndas DROP CONSTRAINT IF EXISTS oferndas_pkey;
|
||||
ALTER TABLE oferndas DROP COLUMN id;
|
||||
ALTER TABLE oferndas RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE oferndas ADD PRIMARY KEY (id);
|
||||
|
||||
-- CATEGORIA_INGRESO
|
||||
ALTER TABLE categoria_ingreso DROP CONSTRAINT IF EXISTS categoria_ingreso_pkey;
|
||||
ALTER TABLE categoria_ingreso DROP COLUMN id;
|
||||
ALTER TABLE categoria_ingreso RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE categoria_ingreso ADD PRIMARY KEY (id);
|
||||
|
||||
-- CATEGORIA_EGRESO
|
||||
ALTER TABLE categoria_egreso DROP CONSTRAINT IF EXISTS categoria_egreso_pkey;
|
||||
ALTER TABLE categoria_egreso DROP COLUMN id;
|
||||
ALTER TABLE categoria_egreso RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE categoria_egreso ADD PRIMARY KEY (id);
|
||||
|
||||
-- REPORTE_MENSUAL_GENERAL
|
||||
ALTER TABLE reporte_mensual_general DROP CONSTRAINT IF EXISTS reporte_mensual_general_pkey;
|
||||
ALTER TABLE reporte_mensual_general DROP COLUMN id;
|
||||
ALTER TABLE reporte_mensual_general RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE reporte_mensual_general ADD PRIMARY KEY (id);
|
||||
|
||||
-- REPORTE_MENSUAL_CONTABLE
|
||||
ALTER TABLE reporte_mensual_contable DROP CONSTRAINT IF EXISTS reporte_mensual_contable_pkey;
|
||||
ALTER TABLE reporte_mensual_contable DROP COLUMN id;
|
||||
ALTER TABLE reporte_mensual_contable RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE reporte_mensual_contable ADD PRIMARY KEY (id);
|
||||
|
||||
-- MOVIMIENTO_GENERAL
|
||||
ALTER TABLE movimiento_general DROP CONSTRAINT IF EXISTS movimiento_general_pkey;
|
||||
ALTER TABLE movimiento_general DROP COLUMN id;
|
||||
ALTER TABLE movimiento_general DROP COLUMN categoria_ingreso_id;
|
||||
ALTER TABLE movimiento_general DROP COLUMN categoria_egreso_id;
|
||||
ALTER TABLE movimiento_general DROP COLUMN reporte_mensual_id;
|
||||
ALTER TABLE movimiento_general RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE movimiento_general RENAME COLUMN categoria_ingreso_id_uuid TO categoria_ingreso_id;
|
||||
ALTER TABLE movimiento_general RENAME COLUMN categoria_egreso_id_uuid TO categoria_egreso_id;
|
||||
ALTER TABLE movimiento_general RENAME COLUMN reporte_mensual_id_uuid TO reporte_mensual_id;
|
||||
ALTER TABLE movimiento_general ADD PRIMARY KEY (id);
|
||||
|
||||
-- MOVIMIENTO_GENERAL_ADJUNTO
|
||||
ALTER TABLE movimiento_general_adjunto DROP CONSTRAINT IF EXISTS movimiento_general_adjunto_pkey;
|
||||
ALTER TABLE movimiento_general_adjunto DROP COLUMN id;
|
||||
ALTER TABLE movimiento_general_adjunto DROP COLUMN movimiento_id;
|
||||
ALTER TABLE movimiento_general_adjunto RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE movimiento_general_adjunto RENAME COLUMN movimiento_id_uuid TO movimiento_id;
|
||||
ALTER TABLE movimiento_general_adjunto ADD PRIMARY KEY (id);
|
||||
|
||||
-- CONTABILIDAD_REGISTRO
|
||||
ALTER TABLE contabilidad_registro DROP CONSTRAINT IF EXISTS contabilidad_registro_pkey;
|
||||
ALTER TABLE contabilidad_registro DROP COLUMN id;
|
||||
ALTER TABLE contabilidad_registro DROP COLUMN reporte_id;
|
||||
ALTER TABLE contabilidad_registro RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE contabilidad_registro RENAME COLUMN reporte_id_uuid TO reporte_id;
|
||||
ALTER TABLE contabilidad_registro ADD PRIMARY KEY (id);
|
||||
|
||||
-- ARTICULOS
|
||||
ALTER TABLE IF EXISTS articulos DROP CONSTRAINT IF EXISTS articulos_pkey;
|
||||
ALTER TABLE IF EXISTS articulos DROP COLUMN id;
|
||||
ALTER TABLE IF EXISTS articulos RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE IF EXISTS articulos ADD PRIMARY KEY (id);
|
||||
|
||||
-- UBICACIONES
|
||||
ALTER TABLE IF EXISTS ubicaciones DROP CONSTRAINT IF EXISTS ubicaciones_pkey;
|
||||
ALTER TABLE IF EXISTS ubicaciones DROP COLUMN id;
|
||||
ALTER TABLE IF EXISTS ubicaciones RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE IF EXISTS ubicaciones ADD PRIMARY KEY (id);
|
||||
|
||||
-- EXISTENCIAS
|
||||
ALTER TABLE IF EXISTS existencias DROP CONSTRAINT IF EXISTS existencias_pkey;
|
||||
ALTER TABLE IF EXISTS existencias DROP COLUMN id;
|
||||
ALTER TABLE IF EXISTS existencias DROP COLUMN articulo_id;
|
||||
ALTER TABLE IF EXISTS existencias DROP COLUMN ubicacion_id;
|
||||
ALTER TABLE IF EXISTS existencias RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE IF EXISTS existencias RENAME COLUMN articulo_id_uuid TO articulo_id;
|
||||
ALTER TABLE IF EXISTS existencias RENAME COLUMN ubicacion_id_uuid TO ubicacion_id;
|
||||
ALTER TABLE IF EXISTS existencias ADD PRIMARY KEY (id);
|
||||
|
||||
-- MOVIMIENTOS_INVENTARIO
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP CONSTRAINT IF EXISTS movimientos_inventario_pkey;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP COLUMN id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP COLUMN articulo_id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP COLUMN ubicacion_origen_id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP COLUMN ubicacion_destino_id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario DROP COLUMN usuario_id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario RENAME COLUMN id_uuid TO id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario RENAME COLUMN articulo_id_uuid TO articulo_id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario RENAME COLUMN ubicacion_origen_id_uuid TO ubicacion_origen_id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario RENAME COLUMN ubicacion_destino_id_uuid TO ubicacion_destino_id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario RENAME COLUMN usuario_id_uuid TO usuario_id;
|
||||
ALTER TABLE IF EXISTS movimientos_inventario ADD PRIMARY KEY (id);
|
||||
|
||||
-- =====================================================
|
||||
-- STEP 5: Recreate foreign key constraints
|
||||
-- =====================================================
|
||||
|
||||
-- Miembros -> Grupos_trabajo
|
||||
ALTER TABLE miembros
|
||||
ADD CONSTRAINT fk_miembro_grupo_trabajo
|
||||
FOREIGN KEY (grupo_trabajo_id)
|
||||
REFERENCES grupos_trabajo(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- Colaboraciones -> Miembros
|
||||
ALTER TABLE colaboraciones
|
||||
ADD CONSTRAINT fk_colaboracion_miembro
|
||||
FOREIGN KEY (miembro_id)
|
||||
REFERENCES miembros(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Detalle_colaboracion -> Colaboraciones
|
||||
ALTER TABLE detalle_colaboracion
|
||||
ADD CONSTRAINT fk_detalle_colaboracion_colaboracion
|
||||
FOREIGN KEY (colaboracion_id)
|
||||
REFERENCES colaboraciones(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Detalle_colaboracion -> Tipos_colaboracion
|
||||
ALTER TABLE detalle_colaboracion
|
||||
ADD CONSTRAINT fk_detalle_colaboracion_tipo
|
||||
FOREIGN KEY (tipo_colaboracion_id)
|
||||
REFERENCES tipos_colaboracion(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Prestamos -> Miembros
|
||||
ALTER TABLE prestamos
|
||||
ADD CONSTRAINT fk_prestamo_miembro
|
||||
FOREIGN KEY (miembro_id)
|
||||
REFERENCES miembros(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Pagos_prestamo -> Prestamos
|
||||
ALTER TABLE pagos_prestamo
|
||||
ADD CONSTRAINT fk_pago_prestamo
|
||||
FOREIGN KEY (prestamo_id)
|
||||
REFERENCES prestamos(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Asistencia_culto -> Miembros
|
||||
ALTER TABLE asistencia_culto
|
||||
ADD CONSTRAINT fk_asistencia_miembro
|
||||
FOREIGN KEY (miembro_id)
|
||||
REFERENCES miembros(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Movimiento_general -> Categoria_ingreso
|
||||
ALTER TABLE movimiento_general
|
||||
ADD CONSTRAINT fk_movimiento_categoria_ingreso
|
||||
FOREIGN KEY (categoria_ingreso_id)
|
||||
REFERENCES categoria_ingreso(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- Movimiento_general -> Categoria_egreso
|
||||
ALTER TABLE movimiento_general
|
||||
ADD CONSTRAINT fk_movimiento_categoria_egreso
|
||||
FOREIGN KEY (categoria_egreso_id)
|
||||
REFERENCES categoria_egreso(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- Movimiento_general -> Reporte_mensual_general
|
||||
ALTER TABLE movimiento_general
|
||||
ADD CONSTRAINT fk_movimiento_reporte_mensual
|
||||
FOREIGN KEY (reporte_mensual_id)
|
||||
REFERENCES reporte_mensual_general(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Movimiento_general_adjunto -> Movimiento_general
|
||||
ALTER TABLE movimiento_general_adjunto
|
||||
ADD CONSTRAINT fk_adjunto_movimiento
|
||||
FOREIGN KEY (movimiento_id)
|
||||
REFERENCES movimiento_general(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Contabilidad_registro -> Reporte_mensual_contable
|
||||
ALTER TABLE contabilidad_registro
|
||||
ADD CONSTRAINT fk_contabilidad_reporte
|
||||
FOREIGN KEY (reporte_id)
|
||||
REFERENCES reporte_mensual_contable(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Existencias -> Articulos
|
||||
ALTER TABLE IF EXISTS existencias
|
||||
ADD CONSTRAINT fk_existencia_articulo
|
||||
FOREIGN KEY (articulo_id)
|
||||
REFERENCES articulos(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Existencias -> Ubicaciones
|
||||
ALTER TABLE IF EXISTS existencias
|
||||
ADD CONSTRAINT fk_existencia_ubicacion
|
||||
FOREIGN KEY (ubicacion_id)
|
||||
REFERENCES ubicaciones(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Movimientos_inventario -> Articulos
|
||||
ALTER TABLE IF EXISTS movimientos_inventario
|
||||
ADD CONSTRAINT fk_movimiento_articulo
|
||||
FOREIGN KEY (articulo_id)
|
||||
REFERENCES articulos(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Movimientos_inventario -> Ubicaciones (origen)
|
||||
ALTER TABLE IF EXISTS movimientos_inventario
|
||||
ADD CONSTRAINT fk_movimiento_ubicacion_origen
|
||||
FOREIGN KEY (ubicacion_origen_id)
|
||||
REFERENCES ubicaciones(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- Movimientos_inventario -> Ubicaciones (destino)
|
||||
ALTER TABLE IF EXISTS movimientos_inventario
|
||||
ADD CONSTRAINT fk_movimiento_ubicacion_destino
|
||||
FOREIGN KEY (ubicacion_destino_id)
|
||||
REFERENCES ubicaciones(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- Movimientos_inventario -> Usuarios
|
||||
ALTER TABLE IF EXISTS movimientos_inventario
|
||||
ADD CONSTRAINT fk_movimiento_usuario
|
||||
FOREIGN KEY (usuario_id)
|
||||
REFERENCES usuarios(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- =====================================================
|
||||
-- STEP 6: Create indexes for better performance
|
||||
-- =====================================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_colaboraciones_miembro_id ON colaboraciones(miembro_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_detalle_colaboracion_id ON detalle_colaboracion(colaboracion_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_detalle_tipo_id ON detalle_colaboracion(tipo_colaboracion_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_colaboraciones_updated_at ON colaboraciones(updated_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_prestamos_miembro_id ON prestamos(miembro_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_asistencia_miembro_id ON asistencia_culto(miembro_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_movimiento_reporte_id ON movimiento_general(reporte_mensual_id);
|
||||
|
||||
-- =====================================================
|
||||
-- VERIFICATION QUERIES
|
||||
-- =====================================================
|
||||
-- Run these after migration to verify success:
|
||||
|
||||
-- SELECT 'colaboraciones' as table_name, COUNT(*) as count FROM colaboraciones;
|
||||
-- SELECT 'miembros' as table_name, COUNT(*) as count FROM miembros;
|
||||
-- SELECT 'detalle_colaboracion' as table_name, COUNT(*) as count FROM detalle_colaboracion;
|
||||
--
|
||||
-- -- Check that all IDs are now UUIDs:
|
||||
-- SELECT id, miembro_id FROM colaboraciones LIMIT 5;
|
||||
-- SELECT id, grupo_trabajo_id FROM miembros LIMIT 5;
|
||||
|
||||
-- =====================================================
|
||||
-- COMMIT or ROLLBACK
|
||||
-- =====================================================
|
||||
-- If everything looks good, COMMIT:
|
||||
COMMIT;
|
||||
|
||||
-- If there are errors, ROLLBACK:
|
||||
-- ROLLBACK;
|
||||
|
||||
-- =====================================================
|
||||
-- POST-MIGRATION NOTES
|
||||
-- =====================================================
|
||||
-- After running this script successfully:
|
||||
-- 1. Update your C# models to use Guid instead of long
|
||||
-- 2. Rebuild your application
|
||||
-- 3. Test thoroughly before deploying to production
|
||||
-- 4. Monitor for any issues with existing records
|
||||
-- =====================================================
|
||||
230
RS_system/wwwroot/js/colaboraciones-offline-db.js
Normal file
230
RS_system/wwwroot/js/colaboraciones-offline-db.js
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* IndexedDB Wrapper for Offline Colaboraciones
|
||||
* Stores pending colaboraciones when offline using GUID-based IDs
|
||||
*/
|
||||
|
||||
const ColaboracionesOfflineDB = {
|
||||
dbName: 'ColaboracionesOfflineDB',
|
||||
version: 1,
|
||||
storeName: 'colaboraciones',
|
||||
|
||||
/**
|
||||
* Initialize the database
|
||||
*/
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.version);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
|
||||
// Create object store if it doesn't exist
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
const objectStore = db.createObjectStore(this.storeName, {
|
||||
keyPath: 'id' // GUID generated client-side
|
||||
});
|
||||
|
||||
// Indexes for querying
|
||||
objectStore.createIndex('syncStatus', 'syncStatus', { unique: false });
|
||||
objectStore.createIndex('timestamp', 'timestamp', { unique: false });
|
||||
objectStore.createIndex('updatedAt', 'updatedAt', { unique: false });
|
||||
objectStore.createIndex('miembroId', 'miembroId', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a GUID (v4 UUID)
|
||||
*/
|
||||
generateGuid() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new colaboracion to offline queue
|
||||
*/
|
||||
async addColaboracion(colaboracionData) {
|
||||
const db = await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
|
||||
// Prepare record with GUID and sync metadata
|
||||
const record = {
|
||||
id: this.generateGuid(), // GUID generated client-side
|
||||
miembroId: colaboracionData.miembroId,
|
||||
mesInicial: colaboracionData.mesInicial,
|
||||
anioInicial: colaboracionData.anioInicial,
|
||||
mesFinal: colaboracionData.mesFinal,
|
||||
anioFinal: colaboracionData.anioFinal,
|
||||
montoTotal: colaboracionData.montoTotal,
|
||||
observaciones: colaboracionData.observaciones || '',
|
||||
tiposSeleccionados: colaboracionData.tiposSeleccionados || [],
|
||||
tipoPrioritario: colaboracionData.tipoPrioritario || null,
|
||||
registradoPor: colaboracionData.registradoPor || 'Usuario',
|
||||
syncStatus: 'pending', // pending, syncing, synced, failed
|
||||
timestamp: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
retryCount: 0
|
||||
};
|
||||
|
||||
const request = store.add(record);
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('[OfflineDB] Colaboración guardada con ID:', record.id);
|
||||
resolve(record);
|
||||
};
|
||||
request.onerror = () => {
|
||||
console.error('[OfflineDB] Error al guardar:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all pending colaboraciones
|
||||
*/
|
||||
async getPending() {
|
||||
const db = await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index('syncStatus');
|
||||
const request = index.getAll('pending');
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all colaboraciones (any status)
|
||||
*/
|
||||
async getAll() {
|
||||
const db = await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Update sync status of a colaboracion
|
||||
*/
|
||||
async updateSyncStatus(id, status, retryCount = 0) {
|
||||
const db = await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const getRequest = store.get(id);
|
||||
|
||||
getRequest.onsuccess = () => {
|
||||
const record = getRequest.result;
|
||||
if (record) {
|
||||
record.syncStatus = status;
|
||||
record.retryCount = retryCount;
|
||||
record.lastSyncAttempt = new Date().toISOString();
|
||||
|
||||
const updateRequest = store.put(record);
|
||||
updateRequest.onsuccess = () => resolve(record);
|
||||
updateRequest.onerror = () => reject(updateRequest.error);
|
||||
} else {
|
||||
reject(new Error('Record not found'));
|
||||
}
|
||||
};
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a colaboracion by ID (after successful sync)
|
||||
*/
|
||||
async remove(id) {
|
||||
const db = await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('[OfflineDB] Colaboración eliminada:', id);
|
||||
resolve();
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get count of pending colaboraciones
|
||||
*/
|
||||
async getPendingCount() {
|
||||
const db = await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index('syncStatus');
|
||||
const request = index.count('pending');
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all records (use with caution)
|
||||
*/
|
||||
async clearAll() {
|
||||
const db = await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('[OfflineDB] All records cleared');
|
||||
resolve();
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a specific colaboracion by ID
|
||||
*/
|
||||
async getById(id) {
|
||||
const db = await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize database when script loads
|
||||
ColaboracionesOfflineDB.init().catch(error => {
|
||||
console.error('[OfflineDB] Initialization failed:', error);
|
||||
});
|
||||
310
RS_system/wwwroot/js/colaboraciones-sync.js
Normal file
310
RS_system/wwwroot/js/colaboraciones-sync.js
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Sync Manager for Colaboraciones
|
||||
* Handles connection monitoring, offline queue, and synchronization
|
||||
*/
|
||||
|
||||
const ColaboracionesSyncManager = {
|
||||
isOnline: navigator.onLine,
|
||||
isSyncing: false,
|
||||
syncInProgress: false,
|
||||
statusIndicator: null,
|
||||
pendingBadge: null,
|
||||
maxRetries: 3,
|
||||
retryDelay: 2000, // milliseconds
|
||||
|
||||
/**
|
||||
* Initialize the sync manager
|
||||
*/
|
||||
init() {
|
||||
console.log('[SyncManager] Initializing...');
|
||||
|
||||
// Get UI elements
|
||||
this.statusIndicator = document.getElementById('offlineStatus');
|
||||
this.pendingBadge = document.getElementById('pendingBadge');
|
||||
|
||||
// Listen for online/offline events
|
||||
window.addEventListener('online', () => this.handleOnline());
|
||||
window.addEventListener('offline', () => this.handleOffline());
|
||||
|
||||
// Set initial status
|
||||
this.updateStatusUI();
|
||||
this.updatePendingBadge();
|
||||
|
||||
// If online, try to sync pending items
|
||||
if (this.isOnline) {
|
||||
setTimeout(() => this.syncPending(), 1000);
|
||||
}
|
||||
|
||||
console.log('[SyncManager] Initialized. Status:', this.isOnline ? 'Online' : 'Offline');
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle online event
|
||||
*/
|
||||
async handleOnline() {
|
||||
console.log('[SyncManager] Connection restored');
|
||||
this.isOnline = true;
|
||||
this.updateStatusUI();
|
||||
|
||||
// Auto-sync after short delay
|
||||
setTimeout(() => this.syncPending(), 500);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle offline event
|
||||
*/
|
||||
handleOffline() {
|
||||
console.log('[SyncManager] Connection lost');
|
||||
this.isOnline = false;
|
||||
this.updateStatusUI();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update status indicator UI
|
||||
*/
|
||||
updateStatusUI() {
|
||||
if (!this.statusIndicator) return;
|
||||
|
||||
if (this.isSyncing) {
|
||||
this.statusIndicator.className = 'badge bg-warning ms-2';
|
||||
this.statusIndicator.innerHTML = '<i class="bi bi-arrow-repeat"></i> Sincronizando';
|
||||
this.statusIndicator.style.display = 'inline-block';
|
||||
} else if (this.isOnline) {
|
||||
this.statusIndicator.className = 'badge bg-success ms-2';
|
||||
this.statusIndicator.innerHTML = '<i class="bi bi-wifi"></i> En línea';
|
||||
this.statusIndicator.style.display = 'inline-block';
|
||||
} else {
|
||||
this.statusIndicator.className = 'badge bg-secondary ms-2';
|
||||
this.statusIndicator.innerHTML = '<i class="bi bi-wifi-off"></i> Sin conexión';
|
||||
this.statusIndicator.style.display = 'inline-block';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update pending items badge
|
||||
*/
|
||||
async updatePendingBadge() {
|
||||
if (!this.pendingBadge) return;
|
||||
|
||||
try {
|
||||
const count = await ColaboracionesOfflineDB.getPendingCount();
|
||||
|
||||
if (count > 0) {
|
||||
this.pendingBadge.textContent = count;
|
||||
this.pendingBadge.style.display = 'inline-block';
|
||||
} else {
|
||||
this.pendingBadge.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SyncManager] Error updating badge:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save colaboracion (online or offline)
|
||||
*/
|
||||
async saveColaboracion(colaboracionData) {
|
||||
if (this.isOnline) {
|
||||
try {
|
||||
// Try to save directly to server
|
||||
const result = await this.sendToServer(colaboracionData);
|
||||
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Colaboración registrada exitosamente',
|
||||
online: true
|
||||
};
|
||||
} else {
|
||||
throw new Error(result.message || 'Error al guardar');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[SyncManager] Online save failed, using offline mode:', error);
|
||||
// Fall back to offline save
|
||||
return await this.saveOffline(colaboracionData);
|
||||
}
|
||||
} else {
|
||||
// Save offline
|
||||
return await this.saveOffline(colaboracionData);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save to offline queue
|
||||
*/
|
||||
async saveOffline(colaboracionData) {
|
||||
try {
|
||||
const record = await ColaboracionesOfflineDB.addColaboracion(colaboracionData);
|
||||
await this.updatePendingBadge();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
offline: true,
|
||||
message: 'Guardado offline. Se sincronizará automáticamente cuando haya conexión.',
|
||||
id: record.id
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[SyncManager] Offline save failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Error al guardar en modo offline: ' + error.message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send colaboracion to server
|
||||
*/
|
||||
async sendToServer(colaboracionData) {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('MiembroId', colaboracionData.miembroId);
|
||||
formData.append('MesInicial', colaboracionData.mesInicial);
|
||||
formData.append('AnioInicial', colaboracionData.anioInicial);
|
||||
formData.append('MesFinal', colaboracionData.mesFinal);
|
||||
formData.append('AnioFinal', colaboracionData.anioFinal);
|
||||
formData.append('MontoTotal', colaboracionData.montoTotal);
|
||||
formData.append('Observaciones', colaboracionData.observaciones || '');
|
||||
|
||||
if (colaboracionData.tipoPrioritario) {
|
||||
formData.append('TipoPrioritario', colaboracionData.tipoPrioritario);
|
||||
}
|
||||
|
||||
if (colaboracionData.tiposSeleccionados && colaboracionData.tiposSeleccionados.length > 0) {
|
||||
colaboracionData.tiposSeleccionados.forEach(tipo => {
|
||||
formData.append('TiposSeleccionados', tipo);
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch('/Colaboracion/Create', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.redirected) {
|
||||
// Success - ASP.NET redirected to Index
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
// Check if response contains success indicators
|
||||
if (text.includes('exitosamente') || response.ok) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
throw new Error('Error en la respuesta del servidor');
|
||||
},
|
||||
|
||||
/**
|
||||
* Synchronize all pending colaboraciones
|
||||
*/
|
||||
async syncPending() {
|
||||
if (!this.isOnline || this.syncInProgress) {
|
||||
console.log('[SyncManager] Sync skipped. Online:', this.isOnline, 'InProgress:', this.syncInProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.syncInProgress = true;
|
||||
this.isSyncing = true;
|
||||
this.updateStatusUI();
|
||||
|
||||
const pending = await ColaboracionesOfflineDB.getPending();
|
||||
|
||||
if (pending.length === 0) {
|
||||
console.log('[SyncManager] No pending items to sync');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[SyncManager] Syncing ${pending.length} pending item(s)...`);
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const item of pending) {
|
||||
try {
|
||||
// Update status to syncing
|
||||
await ColaboracionesOfflineDB.updateSyncStatus(item.id, 'syncing', item.retryCount);
|
||||
|
||||
// Try to send to server
|
||||
const result = await this.sendToServer(item);
|
||||
|
||||
if (result.success) {
|
||||
// Remove from offline DB
|
||||
await ColaboracionesOfflineDB.remove(item.id);
|
||||
successCount++;
|
||||
console.log(`[SyncManager] Item ${item.id} synced successfully`);
|
||||
} else {
|
||||
throw new Error(result.message || 'Unknown error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[SyncManager] Sync failed for item ${item.id}:`, error);
|
||||
|
||||
// Update retry count
|
||||
const newRetryCount = (item.retryCount || 0) + 1;
|
||||
|
||||
if (newRetryCount >= this.maxRetries) {
|
||||
await ColaboracionesOfflineDB.updateSyncStatus(item.id, 'failed', newRetryCount);
|
||||
failCount++;
|
||||
} else {
|
||||
await ColaboracionesOfflineDB.updateSyncStatus(item.id, 'pending', newRetryCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.updatePendingBadge();
|
||||
|
||||
// Show results
|
||||
if (successCount > 0) {
|
||||
toastr.success(`${successCount} colaboración(es) sincronizada(s) exitosamente`);
|
||||
|
||||
// Reload page to show updated data
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
if (failCount > 0) {
|
||||
toastr.error(`${failCount} colaboración(es) no se pudieron sincronizar. Se reintentará automáticamente.`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[SyncManager] Sync error:', error);
|
||||
toastr.error('Error durante la sincronización');
|
||||
} finally {
|
||||
this.syncInProgress = false;
|
||||
this.isSyncing = false;
|
||||
this.updateStatusUI();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Manually trigger sync
|
||||
*/
|
||||
async manualSync() {
|
||||
if (!this.isOnline) {
|
||||
toastr.warning('No hay conexión a internet');
|
||||
return;
|
||||
}
|
||||
|
||||
toastr.info('Iniciando sincronización...');
|
||||
await this.syncPending();
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if currently online
|
||||
*/
|
||||
checkOnlineStatus() {
|
||||
return this.isOnline;
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ColaboracionesSyncManager.init();
|
||||
});
|
||||
} else {
|
||||
ColaboracionesSyncManager.init();
|
||||
}
|
||||
241
RS_system/wwwroot/service-worker.js
Normal file
241
RS_system/wwwroot/service-worker.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Service Worker for RS_system PWA
|
||||
* Implements offline-first architecture with strategic caching
|
||||
* Version: 1.0.0
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = 'rs-system-v1.0.0';
|
||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
|
||||
const API_CACHE = `${CACHE_VERSION}-api`;
|
||||
|
||||
// Critical resources to cache on install
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/Home/Index',
|
||||
'/Colaboracion/Create',
|
||||
'/Colaboracion/Index',
|
||||
'/css/site.css',
|
||||
'/css/bootstrap.min.css',
|
||||
'/css/bootstrap-icons.min.css',
|
||||
'/js/site.js',
|
||||
'/js/colaboraciones-offline-db.js',
|
||||
'/js/colaboraciones-sync.js',
|
||||
'/lib/jquery/dist/jquery.min.js',
|
||||
'/lib/bootstrap/dist/js/bootstrap.bundle.min.js',
|
||||
'/manifest.json',
|
||||
'/Assets/icon-192x192.png',
|
||||
'/Assets/icon-512x512.png'
|
||||
];
|
||||
|
||||
// Install event - cache static assets
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[Service Worker] Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE)
|
||||
.then((cache) => {
|
||||
console.log('[Service Worker] Caching static assets');
|
||||
return cache.addAll(STATIC_ASSETS);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[Service Worker] Installation complete');
|
||||
return self.skipWaiting(); // Activate immediately
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[Service Worker] Installation failed:', error);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[Service Worker] Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((name) => {
|
||||
// Delete old version caches
|
||||
return name.startsWith('rs-system-') && name !== STATIC_CACHE && name !== DYNAMIC_CACHE && name !== API_CACHE;
|
||||
})
|
||||
.map((name) => {
|
||||
console.log('[Service Worker] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('[Service Worker] Activation complete');
|
||||
return self.clients.claim(); // Take control immediately
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - implement caching strategies
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip chrome extension and non-HTTP requests
|
||||
if (!url.protocol.startsWith('http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API requests - Network First, fallback to offline indicator
|
||||
if (url.pathname.includes('/api/') ||
|
||||
url.pathname.includes('/Colaboracion/Sync') ||
|
||||
url.pathname.includes('/Colaboracion/BuscarMiembros') ||
|
||||
url.pathname.includes('/Colaboracion/ObtenerUltimosPagos')) {
|
||||
|
||||
event.respondWith(networkFirstStrategy(request, API_CACHE));
|
||||
return;
|
||||
}
|
||||
|
||||
// POST requests - Network Only (never cache)
|
||||
if (request.method === 'POST') {
|
||||
event.respondWith(
|
||||
fetch(request).catch(() => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
offline: true,
|
||||
message: 'Sin conexión. Por favor intente más tarde.'
|
||||
}),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 503
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets - Cache First, fallback to Network
|
||||
if (isStaticAsset(url.pathname)) {
|
||||
event.respondWith(cacheFirstStrategy(request, STATIC_CACHE));
|
||||
return;
|
||||
}
|
||||
|
||||
// Dynamic content (HTML pages) - Network First, fallback to Cache
|
||||
event.respondWith(networkFirstStrategy(request, DYNAMIC_CACHE));
|
||||
});
|
||||
|
||||
/**
|
||||
* Cache First Strategy
|
||||
* Try cache first, fallback to network, then cache the response
|
||||
*/
|
||||
function cacheFirstStrategy(request, cacheName) {
|
||||
return caches.match(request)
|
||||
.then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
return fetch(request)
|
||||
.then((networkResponse) => {
|
||||
// Clone the response
|
||||
const responseToCache = networkResponse.clone();
|
||||
|
||||
caches.open(cacheName)
|
||||
.then((cache) => {
|
||||
cache.put(request, responseToCache);
|
||||
});
|
||||
|
||||
return networkResponse;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[Service Worker] Fetch failed:', error);
|
||||
// Return offline page if available
|
||||
return caches.match('/offline.html') || new Response('Offline');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Network First Strategy
|
||||
* Try network first, fallback to cache
|
||||
*/
|
||||
function networkFirstStrategy(request, cacheName) {
|
||||
return fetch(request)
|
||||
.then((networkResponse) => {
|
||||
// Clone and cache the response
|
||||
const responseToCache = networkResponse.clone();
|
||||
|
||||
caches.open(cacheName)
|
||||
.then((cache) => {
|
||||
cache.put(request, responseToCache);
|
||||
});
|
||||
|
||||
return networkResponse;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('[Service Worker] Network failed, trying cache:', error);
|
||||
|
||||
return caches.match(request)
|
||||
.then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// If API request and no cache, return offline indicator
|
||||
if (request.url.includes('/api/') || request.url.includes('/Colaboracion/')) {
|
||||
return new Response(
|
||||
JSON.stringify({ offline: true }),
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 503
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is for a static asset
|
||||
*/
|
||||
function isStaticAsset(pathname) {
|
||||
const staticExtensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot', '.ico'];
|
||||
return staticExtensions.some(ext => pathname.endsWith(ext));
|
||||
}
|
||||
|
||||
// Background Sync for future enhancement
|
||||
self.addEventListener('sync', (event) => {
|
||||
console.log('[Service Worker] Background sync triggered:', event.tag);
|
||||
|
||||
if (event.tag === 'sync-colaboraciones') {
|
||||
event.waitUntil(
|
||||
// This will be handled by colaboraciones-sync.js
|
||||
self.registration.showNotification('Sincronización completada', {
|
||||
body: 'Las colaboraciones offline se han sincronizado exitosamente.',
|
||||
icon: '/Assets/icon-192x192.png',
|
||||
badge: '/Assets/icon-192x192.png'
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Message handler for cache updates
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CLEAR_CACHE') {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => caches.delete(cacheName))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Service Worker] Loaded and ready');
|
||||
Reference in New Issue
Block a user