Compare commits
4 Commits
1784131456
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a73de4a4fa | |||
| bec656b105 | |||
| 46bf68cb21 | |||
| 0a4a3e86e6 |
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>
|
||||||
@@ -109,6 +109,84 @@ public class ContabilidadGeneralController : Controller
|
|||||||
return Json(new { success = false, message = "Error al guardar los movimientos. Verifique que el mes no esté cerrado." });
|
return Json(new { success = false, message = "Error al guardar los movimientos. Verifique que el mes no esté cerrado." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Sincronización Offline ====================
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> SincronizarOffline([FromBody] List<BulkSaveRequest> transacciones)
|
||||||
|
{
|
||||||
|
if (transacciones == null || !transacciones.Any())
|
||||||
|
return BadRequest("No hay transacciones para sincronizar.");
|
||||||
|
|
||||||
|
var resultados = new List<object>();
|
||||||
|
|
||||||
|
foreach (var request in transacciones)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (request.ReporteId <= 0)
|
||||||
|
{
|
||||||
|
resultados.Add(new {
|
||||||
|
success = false,
|
||||||
|
reporteId = request.ReporteId,
|
||||||
|
message = "ID de reporte inválido."
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var movimientos = request.Movimientos.Select(m => new MovimientoGeneral
|
||||||
|
{
|
||||||
|
Id = m.Id,
|
||||||
|
Tipo = m.Tipo,
|
||||||
|
CategoriaIngresoId = m.CategoriaIngresoId,
|
||||||
|
CategoriaEgresoId = m.CategoriaEgresoId,
|
||||||
|
Monto = m.Monto,
|
||||||
|
Fecha = DateTime.SpecifyKind(m.Fecha, DateTimeKind.Utc),
|
||||||
|
Descripcion = m.Descripcion ?? "",
|
||||||
|
NumeroComprobante = m.NumeroComprobante
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var success = await _contabilidadService.GuardarMovimientosBulkAsync(request.ReporteId, movimientos);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
resultados.Add(new {
|
||||||
|
success = true,
|
||||||
|
reporteId = request.ReporteId,
|
||||||
|
message = "Sincronizado exitosamente"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resultados.Add(new {
|
||||||
|
success = false,
|
||||||
|
reporteId = request.ReporteId,
|
||||||
|
message = "Error al guardar. El mes puede estar cerrado."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
resultados.Add(new {
|
||||||
|
success = false,
|
||||||
|
reporteId = request.ReporteId,
|
||||||
|
message = $"Error: {ex.Message}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitosos = resultados.Count(r => (bool)((dynamic)r).success);
|
||||||
|
var fallidos = resultados.Count - exitosos;
|
||||||
|
|
||||||
|
return Json(new {
|
||||||
|
success = exitosos > 0,
|
||||||
|
total = transacciones.Count,
|
||||||
|
exitosos = exitosos,
|
||||||
|
fallidos = fallidos,
|
||||||
|
resultados = resultados
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==================== Cerrar Mes ====================
|
// ==================== Cerrar Mes ====================
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
|||||||
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,10 +17,10 @@ public class MiembroController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GET: Miembro
|
// GET: Miembro
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index(int page = 1, int pageSize = 10, string? search = null)
|
||||||
{
|
{
|
||||||
var miembros = await _miembroService.GetAllAsync();
|
var paginatedMembers = await _miembroService.GetPaginatedAsync(page, pageSize, search);
|
||||||
return View(miembros);
|
return View(paginatedMembers);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: Miembro/Details/5
|
// GET: Miembro/Details/5
|
||||||
@@ -129,4 +129,61 @@ public class MiembroController : Controller
|
|||||||
var grupos = await _miembroService.GetGruposTrabajoAsync();
|
var grupos = await _miembroService.GetGruposTrabajoAsync();
|
||||||
ViewBag.GruposTrabajo = new SelectList(grupos.Select(g => new { g.Id, g.Nombre }), "Id", "Nombre");
|
ViewBag.GruposTrabajo = new SelectList(grupos.Select(g => new { g.Id, g.Nombre }), "Id", "Nombre");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: Miembro/Importar
|
||||||
|
public IActionResult Importar()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Miembro/Importar
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> Importar(IFormFile? file)
|
||||||
|
{
|
||||||
|
if (file == null || file.Length == 0)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("", "Por favor seleccione un archivo CSV.");
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("", "El archivo debe ser un CSV.");
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = file.OpenReadStream();
|
||||||
|
var createdBy = User.Identity?.Name ?? "Sistema";
|
||||||
|
var result = await _miembroService.ImportarMiembrosAsync(stream, createdBy);
|
||||||
|
|
||||||
|
if (result.SuccessCount > 0)
|
||||||
|
{
|
||||||
|
TempData["SuccessMessage"] = $"Se importaron {result.SuccessCount} miembros exitosamente.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Errors.Any())
|
||||||
|
{
|
||||||
|
ViewBag.Errors = result.Errors;
|
||||||
|
if (result.SuccessCount == 0)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("", "No se pudo importar ningún miembro. Revise los errores.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["WarningMessage"] = "Se importaron algunos miembros, pero hubo errores en otras filas.";
|
||||||
|
}
|
||||||
|
return View(); // Stay on page to show errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("", $"Error al procesar el archivo: {ex.Message}");
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public class MovimientosInventarioController : Controller
|
|||||||
{
|
{
|
||||||
var articulo = await _articuloService.GetByIdAsync(articuloId.Value);
|
var articulo = await _articuloService.GetByIdAsync(articuloId.Value);
|
||||||
if (articulo == null) return NotFound();
|
if (articulo == null) return NotFound();
|
||||||
|
|
||||||
ViewBag.ArticuloId = articulo.Id;
|
ViewBag.ArticuloId = articulo.Id;
|
||||||
ViewBag.ArticuloNombre = $"{articulo.Codigo} - {articulo.Nombre}";
|
ViewBag.ArticuloNombre = $"{articulo.Codigo} - {articulo.Nombre}";
|
||||||
ViewBag.UbicacionActual = articulo.UbicacionNombre;
|
ViewBag.UbicacionActual = articulo.UbicacionNombre;
|
||||||
@@ -53,13 +53,44 @@ public class MovimientosInventarioController : Controller
|
|||||||
ViewBag.CantidadGlobal = articulo.CantidadGlobal; // For LOTE validation?
|
ViewBag.CantidadGlobal = articulo.CantidadGlobal; // For LOTE validation?
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewBag.Articulos = new SelectList((await _articuloService.GetAllAsync()).Select(x => new { x.Id, Nombre = $"{x.Codigo} - {x.Nombre}" }), "Id", "Nombre", articuloId);
|
ViewBag.Articulos =
|
||||||
|
new SelectList(
|
||||||
|
(await _articuloService.GetAllAsync()).Select(x => new { x.Id, Nombre = $"{x.Codigo} - {x.Nombre}" }),
|
||||||
|
"Id", "Nombre", articuloId);
|
||||||
ViewBag.Ubicaciones = new SelectList(await _ubicacionService.GetAllAsync(), "Id", "Nombre");
|
ViewBag.Ubicaciones = new SelectList(await _ubicacionService.GetAllAsync(), "Id", "Nombre");
|
||||||
ViewBag.Estados = new SelectList(await _estadoService.GetAllAsync(), "Id", "Nombre");
|
ViewBag.Estados = new SelectList(await _estadoService.GetAllAsync(), "Id", "Nombre");
|
||||||
|
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> BuscarArticulos(string term)
|
||||||
|
{
|
||||||
|
var articulos = await _articuloService.GetAllAsync();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(term))
|
||||||
|
{
|
||||||
|
term = term.ToLower();
|
||||||
|
articulos = articulos
|
||||||
|
.Where(a =>
|
||||||
|
(a.Nombre != null && a.Nombre.ToLower().Contains(term)) ||
|
||||||
|
(a.Codigo != null && a.Codigo.ToLower().Contains(term)) ||
|
||||||
|
(a.Descripcion != null && a.Descripcion.ToLower().Contains(term)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit results
|
||||||
|
var resultados = articulos.Take(20).Select(a => new {
|
||||||
|
a.Id,
|
||||||
|
a.Codigo,
|
||||||
|
a.Nombre,
|
||||||
|
Ubicacion = a.UbicacionNombre ?? "Sin ubicación",
|
||||||
|
Stock = a.CantidadGlobal
|
||||||
|
});
|
||||||
|
|
||||||
|
return Json(resultados);
|
||||||
|
}
|
||||||
|
|
||||||
// POST: MovimientosInventario/RegistrarTraslado
|
// POST: MovimientosInventario/RegistrarTraslado
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ public class ApplicationDbContext : DbContext
|
|||||||
public DbSet<Colaboracion> Colaboraciones { get; set; }
|
public DbSet<Colaboracion> Colaboraciones { get; set; }
|
||||||
public DbSet<DetalleColaboracion> DetalleColaboraciones { 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)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
@@ -217,6 +224,81 @@ public class ApplicationDbContext : DbContext
|
|||||||
.IsUnique();
|
.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
|
// Global configuration: Convert all dates to UTC when saving
|
||||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
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');
|
||||||
@@ -5,8 +5,8 @@ namespace Rs_system.Models;
|
|||||||
|
|
||||||
public enum TipoMovimientoContable
|
public enum TipoMovimientoContable
|
||||||
{
|
{
|
||||||
Ingreso,
|
Ingreso = 1,
|
||||||
Egreso
|
Egreso = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
[Table("contabilidad_registros")]
|
[Table("contabilidad_registros")]
|
||||||
|
|||||||
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<IPrestamoService, PrestamoService>();
|
||||||
builder.Services.AddScoped<IColaboracionService, ColaboracionService>();
|
builder.Services.AddScoped<IColaboracionService, ColaboracionService>();
|
||||||
builder.Services.AddSingleton<IQueryCacheService, QueryCacheService>();
|
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 =>
|
builder.Services.AddMemoryCache(options =>
|
||||||
{
|
{
|
||||||
options.SizeLimit = 1024; // 1024 cache entries max
|
options.SizeLimit = 1024; // 1024 cache entries max
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ public class ArticuloService : IArticuloService
|
|||||||
EstadoColor = a.Estado.Color,
|
EstadoColor = a.Estado.Color,
|
||||||
UbicacionId = a.UbicacionId,
|
UbicacionId = a.UbicacionId,
|
||||||
UbicacionNombre = a.Ubicacion.Nombre,
|
UbicacionNombre = a.Ubicacion.Nombre,
|
||||||
Activo = a.Activo
|
Activo = a.Activo,
|
||||||
|
CantidadGlobal = a.CantidadGlobal
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
@@ -34,4 +34,21 @@ public interface IMiembroService
|
|||||||
/// Gets all active work groups for dropdown
|
/// Gets all active work groups for dropdown
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IEnumerable<(long Id, string Nombre)>> GetGruposTrabajoAsync();
|
Task<IEnumerable<(long Id, string Nombre)>> GetGruposTrabajoAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Imports members from a CSV stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="csvStream">The stream of the CSV file</param>
|
||||||
|
/// <param name="createdBy">The user creating the members</param>
|
||||||
|
/// <returns>A tuple with success count and a list of error messages</returns>
|
||||||
|
Task<(int SuccessCount, List<string> Errors)> ImportarMiembrosAsync(Stream csvStream, string createdBy);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets paginated members with optional search
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page">Current page number (1-based)</param>
|
||||||
|
/// <param name="pageSize">Number of items per page</param>
|
||||||
|
/// <param name="searchQuery">Optional search query to filter by name</param>
|
||||||
|
/// <returns>Paginated result with members</returns>
|
||||||
|
Task<PaginatedViewModel<MiembroViewModel>> GetPaginatedAsync(int page, int pageSize, string? searchQuery = null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,4 +221,265 @@ public class MiembroService : IMiembroService
|
|||||||
.Select(g => new ValueTuple<long, string>(g.Id, g.Nombre))
|
.Select(g => new ValueTuple<long, string>(g.Id, g.Nombre))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(int SuccessCount, List<string> Errors)> ImportarMiembrosAsync(Stream csvStream, string createdBy)
|
||||||
|
{
|
||||||
|
int successCount = 0;
|
||||||
|
var errors = new List<string>();
|
||||||
|
int rowNumber = 1; // 1-based, starting at header
|
||||||
|
|
||||||
|
using var reader = new StreamReader(csvStream);
|
||||||
|
|
||||||
|
// Read valid groups for validation
|
||||||
|
var validGroupIds = await _context.GruposTrabajo
|
||||||
|
.Where(g => g.Activo)
|
||||||
|
.Select(g => g.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
var validGroupIdsSet = new HashSet<long>(validGroupIds);
|
||||||
|
|
||||||
|
while (!reader.EndOfStream)
|
||||||
|
{
|
||||||
|
var line = await reader.ReadLineAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||||
|
|
||||||
|
rowNumber++;
|
||||||
|
|
||||||
|
// Skip header if it looks like one (simple check or just assume first row is header)
|
||||||
|
// The prompt implies a specific format, we'll assume the first row IS the header based on standard CSV practices,
|
||||||
|
// but if the user provides a file without header it might be an issue.
|
||||||
|
// However, usually "loading a csv" implies a header.
|
||||||
|
// I'll skip the first row (header) in the loop logic by adding a check.
|
||||||
|
if (rowNumber == 2) continue; // Skip header row (rowNumber started at 1, so first ReadLine is row 1 (header), loop increments to 2)
|
||||||
|
// Wait, if I increment rowNumber AFTER reading, then:
|
||||||
|
// Start: rowNumber=1.
|
||||||
|
// ReadLine (Header). rowNumber becomes 2.
|
||||||
|
// So if rowNumber == 2, it means we just read the header. Correct.
|
||||||
|
|
||||||
|
// Parse CSV line
|
||||||
|
var values = ParseCsvLine(line);
|
||||||
|
|
||||||
|
// Expected columns:
|
||||||
|
// 0: Nombres
|
||||||
|
// 1: Apellidos
|
||||||
|
// 2: Fecha Nacimiento
|
||||||
|
// 3: Fecha Ingreso Congregacion
|
||||||
|
// 4: Telefono
|
||||||
|
// 5: Telefono Emergencia
|
||||||
|
// 6: Direccion
|
||||||
|
// 7: Grupo de trabajo (ID)
|
||||||
|
// 8: Bautizado en El Espiritu Santo (Si/No or True/False)
|
||||||
|
// 9: Activo (Si/No or True/False)
|
||||||
|
|
||||||
|
if (values.Count < 10)
|
||||||
|
{
|
||||||
|
errors.Add($"Fila {rowNumber}: Número de columnas insuficiente. Se esperaban 10, se encontraron {values.Count}.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validation and Parsing
|
||||||
|
var nombres = values[0].Trim();
|
||||||
|
var apellidos = values[1].Trim();
|
||||||
|
if (string.IsNullOrEmpty(nombres) || string.IsNullOrEmpty(apellidos))
|
||||||
|
{
|
||||||
|
errors.Add($"Fila {rowNumber}: Nombres y Apellidos son obligatorios.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateOnly? fechaNacimiento = ParseDate(values[2]);
|
||||||
|
DateOnly? fechaIngreso = ParseDate(values[3]);
|
||||||
|
|
||||||
|
var telefono = values[4].Trim();
|
||||||
|
var telefonoEmergencia = values[5].Trim();
|
||||||
|
var direccion = values[6].Trim();
|
||||||
|
|
||||||
|
if (!long.TryParse(values[7], out long grupoId))
|
||||||
|
{
|
||||||
|
errors.Add($"Fila {rowNumber}: ID de Grupo de trabajo inválido '{values[7]}'.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validGroupIdsSet.Contains(grupoId))
|
||||||
|
{
|
||||||
|
errors.Add($"Fila {rowNumber}: Grupo de trabajo con ID {grupoId} no existe o no está activo.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bautizado = ParseBool(values[8]);
|
||||||
|
bool activo = ParseBool(values[9]);
|
||||||
|
|
||||||
|
// Create Logic
|
||||||
|
var strategy = _context.Database.CreateExecutionStrategy();
|
||||||
|
await strategy.ExecuteAsync(async () =>
|
||||||
|
{
|
||||||
|
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var persona = new Persona
|
||||||
|
{
|
||||||
|
Nombres = nombres,
|
||||||
|
Apellidos = apellidos,
|
||||||
|
FechaNacimiento = fechaNacimiento,
|
||||||
|
Direccion = string.IsNullOrEmpty(direccion) ? null : direccion,
|
||||||
|
Telefono = string.IsNullOrEmpty(telefono) ? null : telefono,
|
||||||
|
Activo = activo,
|
||||||
|
CreadoEn = DateTime.UtcNow,
|
||||||
|
ActualizadoEn = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_context.Personas.Add(persona);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var miembro = new Miembro
|
||||||
|
{
|
||||||
|
PersonaId = persona.Id,
|
||||||
|
BautizadoEspirituSanto = bautizado,
|
||||||
|
FechaIngresoCongregacion = fechaIngreso,
|
||||||
|
TelefonoEmergencia = string.IsNullOrEmpty(telefonoEmergencia) ? null : telefonoEmergencia,
|
||||||
|
GrupoTrabajoId = grupoId,
|
||||||
|
Activo = activo,
|
||||||
|
CreadoPor = createdBy,
|
||||||
|
CreadoEn = DateTime.UtcNow,
|
||||||
|
ActualizadoEn = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_context.Miembros.Add(miembro);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Add($"Fila {rowNumber}: Error al guardar en base de datos: {ex.Message}");
|
||||||
|
// Transaction rolls back automatically on dispose if not committed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Add($"Fila {rowNumber}: Error inesperado: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (successCount, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> ParseCsvLine(string line)
|
||||||
|
{
|
||||||
|
var values = new List<string>();
|
||||||
|
bool inQuotes = false;
|
||||||
|
string currentValue = "";
|
||||||
|
|
||||||
|
for (int i = 0; i < line.Length; i++)
|
||||||
|
{
|
||||||
|
char c = line[i];
|
||||||
|
|
||||||
|
if (c == '"')
|
||||||
|
{
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
else if (c == ',' && !inQuotes)
|
||||||
|
{
|
||||||
|
values.Add(currentValue);
|
||||||
|
currentValue = "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentValue += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values.Add(currentValue);
|
||||||
|
|
||||||
|
// Remove surrounding quotes if present
|
||||||
|
for (int i = 0; i < values.Count; i++)
|
||||||
|
{
|
||||||
|
var val = values[i].Trim();
|
||||||
|
if (val.StartsWith("\"") && val.EndsWith("\"") && val.Length >= 2)
|
||||||
|
{
|
||||||
|
values[i] = val.Substring(1, val.Length - 2).Replace("\"\"", "\"");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
values[i] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateOnly? ParseDate(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||||
|
if (DateOnly.TryParse(value, out var date)) return date;
|
||||||
|
if (DateTime.TryParse(value, out var dt)) return DateOnly.FromDateTime(dt);
|
||||||
|
return null; // Or throw depending on strictness, currently lenient
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ParseBool(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return false;
|
||||||
|
var val = value.Trim().ToLower();
|
||||||
|
return val == "1" || val == "true" || val == "si" || val == "yes" || val == "s" || val == "verdadero";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PaginatedViewModel<MiembroViewModel>> GetPaginatedAsync(int page, int pageSize, string? searchQuery = null)
|
||||||
|
{
|
||||||
|
// Ensure valid page and pageSize
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1) pageSize = 10;
|
||||||
|
if (pageSize > 100) pageSize = 100; // Max limit
|
||||||
|
|
||||||
|
// Start with base query
|
||||||
|
var query = _context.Miembros
|
||||||
|
.Include(m => m.Persona)
|
||||||
|
.Include(m => m.GrupoTrabajo)
|
||||||
|
.Where(m => !m.Eliminado && m.Activo);
|
||||||
|
|
||||||
|
// Apply search filter if provided
|
||||||
|
if (!string.IsNullOrWhiteSpace(searchQuery))
|
||||||
|
{
|
||||||
|
var search = searchQuery.Trim().ToLower();
|
||||||
|
query = query.Where(m =>
|
||||||
|
m.Persona.Nombres.ToLower().Contains(search) ||
|
||||||
|
m.Persona.Apellidos.ToLower().Contains(search) ||
|
||||||
|
(m.Persona.Nombres + " " + m.Persona.Apellidos).ToLower().Contains(search)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count for pagination
|
||||||
|
var totalItems = await query.CountAsync();
|
||||||
|
|
||||||
|
// Get paginated items
|
||||||
|
var items = await query
|
||||||
|
.OrderBy(m => m.Persona.Apellidos)
|
||||||
|
.ThenBy(m => m.Persona.Nombres)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(m => new MiembroViewModel
|
||||||
|
{
|
||||||
|
Id = m.Id,
|
||||||
|
Nombres = m.Persona.Nombres,
|
||||||
|
Apellidos = m.Persona.Apellidos,
|
||||||
|
FechaNacimiento = m.Persona.FechaNacimiento,
|
||||||
|
BautizadoEspirituSanto = m.BautizadoEspirituSanto,
|
||||||
|
Direccion = m.Persona.Direccion,
|
||||||
|
FechaIngresoCongregacion = m.FechaIngresoCongregacion,
|
||||||
|
Telefono = m.Persona.Telefono,
|
||||||
|
TelefonoEmergencia = m.TelefonoEmergencia,
|
||||||
|
GrupoTrabajoId = m.GrupoTrabajoId,
|
||||||
|
GrupoTrabajoNombre = m.GrupoTrabajo != null ? m.GrupoTrabajo.Nombre : null,
|
||||||
|
Activo = m.Activo,
|
||||||
|
FotoUrl = m.Persona.FotoUrl
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return new PaginatedViewModel<MiembroViewModel>
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
CurrentPage = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalItems = totalItems,
|
||||||
|
SearchQuery = searchQuery
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -261,193 +261,284 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<!-- Offline Support Scripts -->
|
||||||
let timeoutBusqueda = null;
|
<script src="~/js/colaboraciones-offline-db.js"></script>
|
||||||
|
<script src="~/js/colaboraciones-sync.js"></script>
|
||||||
// Búsqueda de miembros
|
|
||||||
document.getElementById('buscarMiembro').addEventListener('input', function(e) {
|
<script>
|
||||||
const termino = e.target.value;
|
let timeoutBusqueda = null;
|
||||||
const resultadosDiv = document.getElementById('resultadosBusqueda');
|
|
||||||
|
// Búsqueda de miembros
|
||||||
clearTimeout(timeoutBusqueda);
|
document.getElementById('buscarMiembro').addEventListener('input', function(e) {
|
||||||
|
const termino = e.target.value;
|
||||||
if (termino.length < 2) {
|
const resultadosDiv = document.getElementById('resultadosBusqueda');
|
||||||
resultadosDiv.style.display = 'none';
|
|
||||||
return;
|
clearTimeout(timeoutBusqueda);
|
||||||
}
|
|
||||||
|
if (termino.length < 2) {
|
||||||
timeoutBusqueda = setTimeout(async () => {
|
resultadosDiv.style.display = 'none';
|
||||||
try {
|
return;
|
||||||
const response = await fetch('@Url.Action("BuscarMiembros", "Colaboracion")?termino=' + encodeURIComponent(termino));
|
}
|
||||||
const miembros = await response.json();
|
|
||||||
|
timeoutBusqueda = setTimeout(async () => {
|
||||||
if (miembros.length === 0) {
|
try {
|
||||||
resultadosDiv.innerHTML = '<div class="list-group-item text-muted">No se encontraron resultados</div>';
|
const response = await fetch('@Url.Action("BuscarMiembros", "Colaboracion")?termino=' + encodeURIComponent(termino));
|
||||||
resultadosDiv.style.display = 'block';
|
const miembros = await response.json();
|
||||||
return;
|
|
||||||
}
|
if (miembros.length === 0) {
|
||||||
|
resultadosDiv.innerHTML = '<div class="list-group-item text-muted">No se encontraron resultados</div>';
|
||||||
let html = '';
|
resultadosDiv.style.display = 'block';
|
||||||
miembros.forEach(miembro => {
|
return;
|
||||||
html += `
|
}
|
||||||
<button type="button" class="list-group-item list-group-item-action" onclick="seleccionarMiembro(${miembro.id}, '${miembro.text}')">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
let html = '';
|
||||||
<div>
|
miembros.forEach(miembro => {
|
||||||
<i class="bi bi-person me-2"></i>
|
html += `
|
||||||
<strong>${miembro.text}</strong>
|
<button type="button" class="list-group-item list-group-item-action" onclick="seleccionarMiembro(${miembro.id}, '${miembro.text}')">
|
||||||
</div>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
${miembro.telefono ? '<small class="text-muted">' + miembro.telefono + '</small>' : ''}
|
<div>
|
||||||
</div>
|
<i class="bi bi-person me-2"></i>
|
||||||
</button>
|
<strong>${miembro.text}</strong>
|
||||||
`;
|
</div>
|
||||||
});
|
${miembro.telefono ? '<small class="text-muted">' + miembro.telefono + '</small>' : ''}
|
||||||
|
</div>
|
||||||
resultadosDiv.innerHTML = html;
|
</button>
|
||||||
resultadosDiv.style.display = 'block';
|
`;
|
||||||
} catch (error) {
|
});
|
||||||
console.error('Error al buscar miembros:', error);
|
|
||||||
}
|
resultadosDiv.innerHTML = html;
|
||||||
}, 300);
|
resultadosDiv.style.display = 'block';
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error('Error al buscar miembros:', error);
|
||||||
// Cerrar resultados cuando se hace clic fuera
|
}
|
||||||
document.addEventListener('click', function(e) {
|
}, 300);
|
||||||
const buscarInput = document.getElementById('buscarMiembro');
|
});
|
||||||
const resultadosDiv = document.getElementById('resultadosBusqueda');
|
|
||||||
|
// Cerrar resultados cuando se hace clic fuera
|
||||||
if (!buscarInput.contains(e.target) && !resultadosDiv.contains(e.target)) {
|
document.addEventListener('click', function(e) {
|
||||||
resultadosDiv.style.display = 'none';
|
const buscarInput = document.getElementById('buscarMiembro');
|
||||||
}
|
const resultadosDiv = document.getElementById('resultadosBusqueda');
|
||||||
});
|
|
||||||
|
if (!buscarInput.contains(e.target) && !resultadosDiv.contains(e.target)) {
|
||||||
function seleccionarMiembro(id, nombre) {
|
resultadosDiv.style.display = 'none';
|
||||||
document.getElementById('miembroIdHidden').value = id;
|
}
|
||||||
document.getElementById('nombreMiembroSeleccionado').textContent = nombre;
|
});
|
||||||
document.getElementById('miembroSeleccionado').style.display = 'block';
|
|
||||||
document.getElementById('buscarMiembro').value = '';
|
function seleccionarMiembro(id, nombre) {
|
||||||
document.getElementById('buscarMiembro').style.display = 'none';
|
document.getElementById('miembroIdHidden').value = id;
|
||||||
document.getElementById('resultadosBusqueda').style.display = 'none';
|
document.getElementById('nombreMiembroSeleccionado').textContent = nombre;
|
||||||
|
document.getElementById('miembroSeleccionado').style.display = 'block';
|
||||||
// Cargar historial de pagos
|
document.getElementById('buscarMiembro').value = '';
|
||||||
cargarHistorialPagos(id);
|
document.getElementById('buscarMiembro').style.display = 'none';
|
||||||
}
|
document.getElementById('resultadosBusqueda').style.display = 'none';
|
||||||
|
|
||||||
function limpiarMiembro() {
|
// Cargar historial de pagos
|
||||||
document.getElementById('miembroIdHidden').value = '';
|
cargarHistorialPagos(id);
|
||||||
document.getElementById('miembroSeleccionado').style.display = 'none';
|
|
||||||
document.getElementById('buscarMiembro').style.display = 'block';
|
|
||||||
document.getElementById('buscarMiembro').focus();
|
|
||||||
|
|
||||||
// Ocultar historial
|
|
||||||
document.getElementById('infoUltimosPagos').style.display = 'none';
|
|
||||||
document.getElementById('listaUltimosPagos').innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cargarHistorialPagos(miembroId) {
|
|
||||||
const contenedor = document.getElementById('infoUltimosPagos');
|
|
||||||
const lista = document.getElementById('listaUltimosPagos');
|
|
||||||
|
|
||||||
lista.innerHTML = '<div class="spinner-border spinner-border-sm text-info" role="status"></div> Cargando historial...';
|
|
||||||
contenedor.style.display = 'block';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('@Url.Action("ObtenerUltimosPagos", "Colaboracion")?miembroId=' + miembroId);
|
|
||||||
const pagos = await response.json();
|
|
||||||
|
|
||||||
if (pagos && pagos.length > 0) {
|
|
||||||
let html = '';
|
|
||||||
pagos.forEach(p => {
|
|
||||||
const colorClass = p.ultimoMes > 0 ? 'bg-white text-info border border-info' : 'bg-secondary text-white';
|
|
||||||
html += `
|
|
||||||
<span class="badge ${colorClass} fw-normal p-2">
|
|
||||||
<strong>${p.nombreTipo}:</strong> ${p.ultimoPeriodoTexto}
|
|
||||||
</span>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
lista.innerHTML = html;
|
|
||||||
} else {
|
|
||||||
lista.innerHTML = '<span class="text-muted small">No hay historial de pagos registrado.</span>';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al cargar historial:', error);
|
|
||||||
lista.innerHTML = '<span class="text-danger small"><i class="bi bi-exclamation-circle"></i> Error al cargar historial</span>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcularSugerido() {
|
|
||||||
try {
|
|
||||||
// Obtener valores
|
|
||||||
const mesInicial = parseInt(document.getElementById('mesInicial').value);
|
|
||||||
const anioInicial = parseInt(document.getElementById('anioInicial').value);
|
|
||||||
const mesFinal = parseInt(document.getElementById('mesFinal').value);
|
|
||||||
const anioFinal = parseInt(document.getElementById('anioFinal').value);
|
|
||||||
|
|
||||||
// Calcular total de meses
|
|
||||||
const fechaInicial = new Date(anioInicial, mesInicial - 1, 1);
|
|
||||||
const fechaFinal = new Date(anioFinal, mesFinal - 1, 1);
|
|
||||||
|
|
||||||
let totalMeses = 0;
|
|
||||||
if (fechaFinal >= fechaInicial) {
|
|
||||||
totalMeses = ((anioFinal - anioInicial) * 12) + (mesFinal - mesInicial) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtener tipos seleccionados y sus montos
|
|
||||||
const tiposCheckboxes = document.querySelectorAll('.tipo-checkbox:checked');
|
|
||||||
const totalTipos = tiposCheckboxes.length;
|
|
||||||
|
|
||||||
// Calcular monto sugerido total
|
|
||||||
let montoSugeridoTotal = 0;
|
|
||||||
tiposCheckboxes.forEach(checkbox => {
|
|
||||||
const montoPorMes = parseFloat(checkbox.getAttribute('data-monto')) || 0;
|
|
||||||
montoSugeridoTotal += montoPorMes * totalMeses;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Actualizar UI
|
|
||||||
document.getElementById('totalMeses').textContent = totalMeses;
|
|
||||||
document.getElementById('totalTipos').textContent = totalTipos;
|
|
||||||
document.getElementById('montoSugerido').textContent = montoSugeridoTotal.toFixed(2);
|
|
||||||
|
|
||||||
// Comparar con monto ingresado
|
|
||||||
const montoIngresado = parseFloat(document.getElementById('montoTotal').value) || 0;
|
|
||||||
const alertaDiv = document.getElementById('alertaDiferencia');
|
|
||||||
const mensajeSpan = document.getElementById('mensajeDiferencia');
|
|
||||||
|
|
||||||
if (montoIngresado > 0) {
|
|
||||||
if (Math.abs(montoIngresado - montoSugeridoTotal) > 0.01) {
|
|
||||||
const diferencia = montoIngresado - montoSugeridoTotal;
|
|
||||||
if (diferencia > 0) {
|
|
||||||
mensajeSpan.textContent = `Sobra: $${diferencia.toFixed(2)}`;
|
|
||||||
alertaDiv.className = 'alert alert-info py-1 px-2 mb-0 mt-2';
|
|
||||||
} else {
|
|
||||||
mensajeSpan.textContent = `Falta: $${Math.abs(diferencia).toFixed(2)}`;
|
|
||||||
alertaDiv.className = 'alert alert-warning py-1 px-2 mb-0 mt-2';
|
|
||||||
}
|
|
||||||
alertaDiv.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
alertaDiv.style.display = 'none';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alertaDiv.style.display = 'none';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al calcular sugerido:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calcular cuando cambia el monto total
|
|
||||||
document.getElementById('montoTotal')?.addEventListener('input', calcularSugerido);
|
|
||||||
|
|
||||||
// Calcular al cargar la página
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
calcularSugerido();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show error messages
|
|
||||||
@if (TempData["Error"] != null)
|
|
||||||
{
|
|
||||||
<text>
|
|
||||||
toastr.error('@TempData["Error"]');
|
|
||||||
</text>
|
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
function limpiarMiembro() {
|
||||||
|
document.getElementById('miembroIdHidden').value = '';
|
||||||
|
document.getElementById('miembroSeleccionado').style.display = 'none';
|
||||||
|
document.getElementById('buscarMiembro').style.display = 'block';
|
||||||
|
document.getElementById('buscarMiembro').focus();
|
||||||
|
|
||||||
|
// Ocultar historial
|
||||||
|
document.getElementById('infoUltimosPagos').style.display = 'none';
|
||||||
|
document.getElementById('listaUltimosPagos').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cargarHistorialPagos(miembroId) {
|
||||||
|
const contenedor = document.getElementById('infoUltimosPagos');
|
||||||
|
const lista = document.getElementById('listaUltimosPagos');
|
||||||
|
|
||||||
|
lista.innerHTML = '<div class="spinner-border spinner-border-sm text-info" role="status"></div> Cargando historial...';
|
||||||
|
contenedor.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('@Url.Action("ObtenerUltimosPagos", "Colaboracion")?miembroId=' + miembroId);
|
||||||
|
const pagos = await response.json();
|
||||||
|
|
||||||
|
if (pagos && pagos.length > 0) {
|
||||||
|
let html = '';
|
||||||
|
pagos.forEach(p => {
|
||||||
|
const colorClass = p.ultimoMes > 0 ? 'bg-white text-info border border-info' : 'bg-secondary text-white';
|
||||||
|
html += `
|
||||||
|
<span class="badge ${colorClass} fw-normal p-2">
|
||||||
|
<strong>${p.nombreTipo}:</strong> ${p.ultimoPeriodoTexto}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
lista.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
lista.innerHTML = '<span class="text-muted small">No hay historial de pagos registrado.</span>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar historial:', error);
|
||||||
|
lista.innerHTML = '<span class="text-danger small"><i class="bi bi-exclamation-circle"></i> Error al cargar historial</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcularSugerido() {
|
||||||
|
try {
|
||||||
|
// Obtener valores
|
||||||
|
const mesInicial = parseInt(document.getElementById('mesInicial').value);
|
||||||
|
const anioInicial = parseInt(document.getElementById('anioInicial').value);
|
||||||
|
const mesFinal = parseInt(document.getElementById('mesFinal').value);
|
||||||
|
const anioFinal = parseInt(document.getElementById('anioFinal').value);
|
||||||
|
|
||||||
|
// Calcular total de meses
|
||||||
|
const fechaInicial = new Date(anioInicial, mesInicial - 1, 1);
|
||||||
|
const fechaFinal = new Date(anioFinal, mesFinal - 1, 1);
|
||||||
|
|
||||||
|
let totalMeses = 0;
|
||||||
|
if (fechaFinal >= fechaInicial) {
|
||||||
|
totalMeses = ((anioFinal - anioInicial) * 12) + (mesFinal - mesInicial) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener tipos seleccionados y sus montos
|
||||||
|
const tiposCheckboxes = document.querySelectorAll('.tipo-checkbox:checked');
|
||||||
|
const totalTipos = tiposCheckboxes.length;
|
||||||
|
|
||||||
|
// Calcular monto sugerido total
|
||||||
|
let montoSugeridoTotal = 0;
|
||||||
|
tiposCheckboxes.forEach(checkbox => {
|
||||||
|
const montoPorMes = parseFloat(checkbox.getAttribute('data-monto')) || 0;
|
||||||
|
montoSugeridoTotal += montoPorMes * totalMeses;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Actualizar UI
|
||||||
|
document.getElementById('totalMeses').textContent = totalMeses;
|
||||||
|
document.getElementById('totalTipos').textContent = totalTipos;
|
||||||
|
document.getElementById('montoSugerido').textContent = montoSugeridoTotal.toFixed(2);
|
||||||
|
|
||||||
|
// Comparar con monto ingresado
|
||||||
|
const montoIngresado = parseFloat(document.getElementById('montoTotal').value) || 0;
|
||||||
|
const alertaDiv = document.getElementById('alertaDiferencia');
|
||||||
|
const mensajeSpan = document.getElementById('mensajeDiferencia');
|
||||||
|
|
||||||
|
if (montoIngresado > 0) {
|
||||||
|
if (Math.abs(montoIngresado - montoSugeridoTotal) > 0.01) {
|
||||||
|
const diferencia = montoSugeridoTotal - montoIngresado; // Corrected calculation for difference
|
||||||
|
if (diferencia > 0) {
|
||||||
|
mensajeSpan.textContent = `Falta: $${diferencia.toFixed(2)}`;
|
||||||
|
alertaDiv.className = 'alert alert-warning py-1 px-2 mb-0 mt-2';
|
||||||
|
} else {
|
||||||
|
mensajeSpan.textContent = `Sobra: $${Math.abs(diferencia).toFixed(2)}`;
|
||||||
|
alertaDiv.className = 'alert alert-info py-1 px-2 mb-0 mt-2';
|
||||||
|
}
|
||||||
|
alertaDiv.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
alertaDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alertaDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al calcular sugerido:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular cuando cambia el monto total
|
||||||
|
document.getElementById('montoTotal')?.addEventListener('input', calcularSugerido);
|
||||||
|
|
||||||
|
// ===== OFFLINE-FIRST FORM SUBMISSION =====
|
||||||
|
document.getElementById('colaboracionForm')?.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault(); // Prevent default form submission
|
||||||
|
|
||||||
|
// Gather form data
|
||||||
|
const miembroId = document.getElementById('miembroIdHidden').value;
|
||||||
|
const mesInicial = document.getElementById('mesInicial').value;
|
||||||
|
const anioInicial = document.getElementById('anioInicial').value;
|
||||||
|
const mesFinal = document.getElementById('mesFinal').value;
|
||||||
|
const anioFinal = document.getElementById('anioFinal').value;
|
||||||
|
const montoTotal = document.getElementById('montoTotal').value;
|
||||||
|
const observaciones = document.querySelector('[name="Observaciones"]').value;
|
||||||
|
const tipoPrioritario = document.getElementById('tipoPrioritario').value;
|
||||||
|
|
||||||
|
// Get selected tipos
|
||||||
|
const tiposSeleccionados = Array.from(document.querySelectorAll('.tipo-checkbox:checked'))
|
||||||
|
.map(cb => cb.value);
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if (!miembroId) {
|
||||||
|
toastr.error('Por favor seleccione un miembro');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tiposSeleccionados.length === 0) {
|
||||||
|
toastr.error('Por favor seleccione al menos un tipo de colaboración');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!montoTotal || parseFloat(montoTotal) <= 0) {
|
||||||
|
toastr.error('Por favor ingrese un monto válido');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data object
|
||||||
|
const colaboracionData = {
|
||||||
|
miembroId: parseInt(miembroId),
|
||||||
|
mesInicial: parseInt(mesInicial),
|
||||||
|
anioInicial: parseInt(anioInicial),
|
||||||
|
mesFinal: parseInt(mesFinal),
|
||||||
|
anioFinal: parseInt(anioFinal),
|
||||||
|
montoTotal: parseFloat(montoTotal),
|
||||||
|
observaciones: observaciones,
|
||||||
|
tiposSeleccionados: tiposSeleccionados,
|
||||||
|
tipoPrioritario: tipoPrioritario || null,
|
||||||
|
registradoPor: '@User.Identity?.Name'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Disable submit button
|
||||||
|
const submitBtn = this.querySelector('button[type="submit"]');
|
||||||
|
const originalBtnText = submitBtn.innerHTML;
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Guardando...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use sync manager to save (handles online/offline automatically)
|
||||||
|
const result = await ColaboracionesSyncManager.saveColaboracion(colaboracionData);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
if (result.offline) {
|
||||||
|
toastr.warning(result.message);
|
||||||
|
|
||||||
|
// Clear form
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '@Url.Action("Index", "Colaboracion")';
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
toastr.success(result.message);
|
||||||
|
|
||||||
|
// Redirect to index
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '@Url.Action("Index", "Colaboracion")';
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toastr.error(result.message || 'Error al guardar');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalBtnText;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Form submission error:', error);
|
||||||
|
toastr.error('Error inesperado: ' + error.message);
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = originalBtnText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calcular al cargar la página
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
calcularSugerido();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show error messages
|
||||||
|
@if (TempData["Error"] != null)
|
||||||
|
{
|
||||||
|
<text>
|
||||||
|
toastr.error('@TempData["Error"]');
|
||||||
|
</text>
|
||||||
|
}
|
||||||
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,8 +99,8 @@
|
|||||||
<td><input type="date" class="form-control form-control-sm row-fecha" value="@item.Fecha.ToString("yyyy-MM-dd")" @(Model.Cerrado ? "disabled" : "") /></td>
|
<td><input type="date" class="form-control form-control-sm row-fecha" value="@item.Fecha.ToString("yyyy-MM-dd")" @(Model.Cerrado ? "disabled" : "") /></td>
|
||||||
<td>
|
<td>
|
||||||
<select class="form-select form-select-sm row-tipo" @(Model.Cerrado ? "disabled" : "") onchange="updateTotals()">
|
<select class="form-select form-select-sm row-tipo" @(Model.Cerrado ? "disabled" : "") onchange="updateTotals()">
|
||||||
<!option value="1" @(item.Tipo == TipoMovimientoContable.Ingreso ? "selected" : "")>Ingreso (+)</!option>
|
<option value="1" selected="@(item.Tipo == TipoMovimientoContable.Ingreso)">Ingreso (+)</option>
|
||||||
<!option value="0" @(item.Tipo == TipoMovimientoContable.Egreso ? "selected" : "")>Egreso (-)</!option>
|
<option value="2" selected="@(item.Tipo == TipoMovimientoContable.Egreso)">Egreso (-)</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td><input type="text" class="form-control form-control-sm row-descripcion" value="@item.Descripcion" placeholder="Motivo del movimiento..." @(Model.Cerrado ? "disabled" : "") /></td>
|
<td><input type="text" class="form-control form-control-sm row-descripcion" value="@item.Descripcion" placeholder="Motivo del movimiento..." @(Model.Cerrado ? "disabled" : "") /></td>
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<select class="form-select form-select-sm row-tipo" onchange="updateTotals()">
|
<select class="form-select form-select-sm row-tipo" onchange="updateTotals()">
|
||||||
<option value="1">Ingreso (+)</option>
|
<option value="1">Ingreso (+)</option>
|
||||||
<option value="0">Egreso (-)</option>
|
<option value="2">Egreso (-)</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td><input type="text" class="form-control form-control-sm row-descripcion" value="" placeholder="Motivo del movimiento..." /></td>
|
<td><input type="text" class="form-control form-control-sm row-descripcion" value="" placeholder="Motivo del movimiento..." /></td>
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
<h5 class="text-secondary">
|
<h5 class="text-secondary">
|
||||||
Saldo Actual: <span class="font-weight-bold @(ViewBag.SaldoActual >= 0 ? "text-success" : "text-danger")">@ViewBag.SaldoActual?.ToString("C")</span>
|
Saldo Actual: <span class="font-weight-bold @(ViewBag.SaldoActual >= 0 ? "text-success" : "text-danger")">@ViewBag.SaldoActual?.ToString("C")</span>
|
||||||
</h5>
|
</h5>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span id="connectionStatus" class="badge bg-secondary"><i class="fas fa-wifi"></i> Verificando...</span>
|
||||||
|
<span id="pendingCount" class="badge bg-warning ml-2" style="display:none;">0 pendientes</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index" class="btn btn-secondary btn-sm">
|
<a asp-action="Index" class="btn btn-secondary btn-sm">
|
||||||
@@ -139,6 +143,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
|
<script src="~/js/offline-db.js"></script>
|
||||||
|
<script src="~/js/offline-manager.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const esCerrado = @Model.Cerrado.ToString().ToLower();
|
const esCerrado = @Model.Cerrado.ToString().ToLower();
|
||||||
const reporteId = @Model.Id;
|
const reporteId = @Model.Id;
|
||||||
@@ -329,26 +335,23 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('@Url.Action("GuardarBulk")', {
|
// Use offline manager for save operation
|
||||||
method: 'POST',
|
const result = await OfflineManager.saveTransaction(
|
||||||
headers: {
|
reporteId,
|
||||||
'Content-Type': 'application/json'
|
payloadMovimientos,
|
||||||
},
|
'@Url.Action("GuardarBulk")'
|
||||||
body: JSON.stringify({
|
);
|
||||||
ReporteId: reporteId,
|
|
||||||
Movimientos: payloadMovimientos
|
|
||||||
})
|
|
||||||
});
|
|
||||||
// ... rest of logic
|
|
||||||
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Show toast or alert
|
if (result.offline) {
|
||||||
alert('Guardado exitosamente');
|
alert(result.message);
|
||||||
location.reload();
|
await OfflineManager.updatePendingCount();
|
||||||
|
} else {
|
||||||
|
alert('Guardado exitosamente');
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + result.message);
|
alert('Error: ' + (result.message || 'Error desconocido'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
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>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@model IEnumerable<Rs_system.Models.ViewModels.MiembroViewModel>
|
@model Rs_system.Models.ViewModels.PaginatedViewModel<Rs_system.Models.ViewModels.MiembroViewModel>
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Miembros de la Iglesia";
|
ViewData["Title"] = "Miembros de la Iglesia";
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,48 @@
|
|||||||
<h4 class="mb-1">Miembros en Propiedad</h4>
|
<h4 class="mb-1">Miembros en Propiedad</h4>
|
||||||
<p class="text-muted mb-0">Gestión de miembros de la congregación</p>
|
<p class="text-muted mb-0">Gestión de miembros de la congregación</p>
|
||||||
</div>
|
</div>
|
||||||
<a asp-action="Create" class="btn btn-primary-custom">
|
<div>
|
||||||
<i class="bi bi-plus-lg me-1"></i> Nuevo Miembro
|
<a asp-action="Importar" class="btn btn-outline-success me-2">
|
||||||
</a>
|
<i class="bi bi-file-earmark-spreadsheet me-1"></i> Importar CSV
|
||||||
|
</a>
|
||||||
|
<a asp-action="Create" class="btn btn-primary-custom">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i> Nuevo Miembro
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search and Page Size Controls -->
|
||||||
|
<div class="card-custom mb-3">
|
||||||
|
<form method="get" asp-action="Index" id="searchForm" class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="search" class="form-label">Buscar Miembro</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="search"
|
||||||
|
name="search"
|
||||||
|
value="@Model.SearchQuery"
|
||||||
|
placeholder="Buscar por nombre o apellido...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="pageSize" class="form-label">Registros por página</label>
|
||||||
|
<select class="form-select"
|
||||||
|
id="pageSize"
|
||||||
|
asp-for="PageSize"
|
||||||
|
onchange="document.getElementById('searchForm').submit();">
|
||||||
|
<option value="5">5</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
<i class="bi bi-search me-1"></i> Buscar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="page" value="1" />
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Summary Cards -->
|
<!-- Summary Cards -->
|
||||||
@@ -18,19 +57,19 @@
|
|||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card-custom text-center">
|
<div class="card-custom text-center">
|
||||||
<h6 class="text-muted mb-2">Total Miembros</h6>
|
<h6 class="text-muted mb-2">Total Miembros</h6>
|
||||||
<h3 class="text-primary mb-0">@Model.Count()</h3>
|
<h3 class="text-primary mb-0">@Model.TotalItems</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card-custom text-center">
|
<div class="card-custom text-center">
|
||||||
<h6 class="text-muted mb-2">Bautizados en el Espíritu Santo</h6>
|
<h6 class="text-muted mb-2">Bautizados en el Espíritu Santo</h6>
|
||||||
<h3 class="text-success mb-0">@Model.Count(m => m.BautizadoEspirituSanto)</h3>
|
<h3 class="text-success mb-0">@Model.Items.Count(m => m.BautizadoEspirituSanto)</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card-custom text-center">
|
<div class="card-custom text-center">
|
||||||
<h6 class="text-muted mb-2">Grupos de Trabajo</h6>
|
<h6 class="text-muted mb-2">Grupos de Trabajo</h6>
|
||||||
<h3 class="text-info mb-0">@Model.Where(m => m.GrupoTrabajoId.HasValue).GroupBy(m => m.GrupoTrabajoId).Count()</h3>
|
<h3 class="text-info mb-0">@Model.Items.Where(m => m.GrupoTrabajoId.HasValue).GroupBy(m => m.GrupoTrabajoId).Count()</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,16 +91,23 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@if (!Model.Any())
|
@if (!Model.Items.Any())
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center text-muted py-4">
|
<td colspan="8" class="text-center text-muted py-4">
|
||||||
<i class="bi bi-people fs-1 d-block mb-2"></i>
|
<i class="bi bi-people fs-1 d-block mb-2"></i>
|
||||||
No hay miembros registrados
|
@if (!string.IsNullOrWhiteSpace(Model.SearchQuery))
|
||||||
|
{
|
||||||
|
<text>No se encontraron miembros con el criterio de búsqueda "@Model.SearchQuery"</text>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<text>No hay miembros registrados</text>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
@foreach (var miembro in Model)
|
@foreach (var miembro in Model.Items)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@@ -152,6 +198,71 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination Controls -->
|
||||||
|
@if (Model.TotalPages > 1)
|
||||||
|
{
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-3 px-3 pb-3">
|
||||||
|
<div class="text-muted">
|
||||||
|
Mostrando @((Model.CurrentPage - 1) * Model.PageSize + 1) a @(Math.Min(Model.CurrentPage * Model.PageSize, Model.TotalItems)) de @Model.TotalItems registros
|
||||||
|
</div>
|
||||||
|
<nav aria-label="Paginación de miembros">
|
||||||
|
<ul class="pagination mb-0">
|
||||||
|
<!-- Previous Button -->
|
||||||
|
<li class="page-item @(!Model.HasPreviousPage ? "disabled" : "")">
|
||||||
|
<a class="page-link"
|
||||||
|
href="@Url.Action("Index", new { page = Model.CurrentPage - 1, pageSize = Model.PageSize, search = Model.SearchQuery })"
|
||||||
|
aria-label="Anterior">
|
||||||
|
<span aria-hidden="true">«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
@{
|
||||||
|
var startPage = Math.Max(1, Model.CurrentPage - 2);
|
||||||
|
var endPage = Math.Min(Model.TotalPages, Model.CurrentPage + 2);
|
||||||
|
|
||||||
|
if (startPage > 1)
|
||||||
|
{
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="@Url.Action("Index", new { page = 1, pageSize = Model.PageSize, search = Model.SearchQuery })">1</a>
|
||||||
|
</li>
|
||||||
|
if (startPage > 2)
|
||||||
|
{
|
||||||
|
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = startPage; i <= endPage; i++)
|
||||||
|
{
|
||||||
|
<li class="page-item @(i == Model.CurrentPage ? "active" : "")">
|
||||||
|
<a class="page-link" href="@Url.Action("Index", new { page = i, pageSize = Model.PageSize, search = Model.SearchQuery })">@i</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage < Model.TotalPages)
|
||||||
|
{
|
||||||
|
if (endPage < Model.TotalPages - 1)
|
||||||
|
{
|
||||||
|
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||||
|
}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="@Url.Action("Index", new { page = Model.TotalPages, pageSize = Model.PageSize, search = Model.SearchQuery })">@Model.TotalPages</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Next Button -->
|
||||||
|
<li class="page-item @(!Model.HasNextPage ? "disabled" : "")">
|
||||||
|
<a class="page-link"
|
||||||
|
href="@Url.Action("Index", new { page = Model.CurrentPage + 1, pageSize = Model.PageSize, search = Model.SearchQuery })"
|
||||||
|
aria-label="Siguiente">
|
||||||
|
<span aria-hidden="true">»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Form -->
|
<!-- Delete Form -->
|
||||||
|
|||||||
@@ -23,14 +23,107 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" asp-action="Create">
|
<form method="get" asp-action="Create">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Buscar Artículo</label>
|
<div class="input-group">
|
||||||
<select name="articuloId" class="form-select" asp-items="ViewBag.Articulos" onchange="this.form.submit()">
|
<input type="text" class="form-control" value="@ViewBag.ArticuloNombre" placeholder="Ningún artículo seleccionado" readonly />
|
||||||
<option value="">-- Seleccione un artículo --</option>
|
<input type="hidden" name="articuloId" id="articuloIdInput" value="@ViewBag.ArticuloId" />
|
||||||
</select>
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalBuscarArticulo">
|
||||||
<div class="form-text">Seleccione para cargar datos actuales.</div>
|
<i class="bi bi-search"></i> Buscar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Busque y seleccione el artículo para cargar sus datos.</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Modal Buscador -->
|
||||||
|
<div class="modal fade" id="modalBuscarArticulo" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Buscar Artículo</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" id="inputBusqueda" class="form-control" placeholder="Nombre, código o descripción...">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="buscarArticulos()">
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">
|
||||||
|
<table class="table table-hover table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Nombre</th>
|
||||||
|
<th>Ubicación</th>
|
||||||
|
<th>Stock</th>
|
||||||
|
<th>Acción</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="resultadoBusqueda">
|
||||||
|
<tr><td colspan="5" class="text-center text-muted">Ingrese un término para buscar...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('inputBusqueda').addEventListener('keypress', function (e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
buscarArticulos();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function buscarArticulos() {
|
||||||
|
const term = document.getElementById('inputBusqueda').value;
|
||||||
|
if (!term || term.length < 2) {
|
||||||
|
alert("Ingrese al menos 2 caracteres.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbody = document.getElementById('resultadoBusqueda');
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm text-primary"></div> Buscando...</td></tr>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`@Url.Action("BuscarArticulos")?term=${encodeURIComponent(term)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
if (data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No se encontraron resultados.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach(item => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td><small>${item.codigo || '-'}</small></td>
|
||||||
|
<td>${item.nombre}</td>
|
||||||
|
<td><small>${item.ubicacion}</small></td>
|
||||||
|
<td><span class="badge bg-secondary">${item.stock}</span></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" onclick="seleccionarArticulo(${item.id})">
|
||||||
|
Seleccionar
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Error al buscar.</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function seleccionarArticulo(id) {
|
||||||
|
// Redirect to same page with id parameter to load details
|
||||||
|
window.location.href = '@Url.Action("Create")?articuloId=' + id;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
@if (ViewBag.ArticuloId != null)
|
@if (ViewBag.ArticuloId != null)
|
||||||
{
|
{
|
||||||
<div class="alert alert-light border mt-3">
|
<div class="alert alert-light border mt-3">
|
||||||
|
|||||||
@@ -22,6 +22,22 @@
|
|||||||
<!--<link rel="stylesheet" href="~/Rs_system.styles.css" asp-append-version="true"/>-->
|
<!--<link rel="stylesheet" href="~/Rs_system.styles.css" asp-append-version="true"/>-->
|
||||||
<link rel="manifest" href="~/manifest.json">
|
<link rel="manifest" href="~/manifest.json">
|
||||||
<meta name="theme-color" content="#1e293b">
|
<meta name="theme-color" content="#1e293b">
|
||||||
|
|
||||||
|
<!-- Service Worker Registration -->
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/service-worker.js')
|
||||||
|
.then(registration => {
|
||||||
|
console.log('Service Worker registered successfully:', registration.scope);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Service Worker registration failed:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
@RenderSection("Styles", required: false)
|
@RenderSection("Styles", required: false)
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -53,6 +69,10 @@
|
|||||||
<h5 class="mb-0 fw-semibold">@ViewData["Title"]</h5>
|
<h5 class="mb-0 fw-semibold">@ViewData["Title"]</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
<span id="offlineStatus" class="badge bg-success" style="display: none;">
|
||||||
|
<i class="bi bi-wifi"></i> En línea
|
||||||
|
</span>
|
||||||
|
<span id="pendingBadge" class="badge bg-warning ms-2" style="display: none;">0</span>
|
||||||
<partial name="_LoginPartial"/>
|
<partial name="_LoginPartial"/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -10,4 +10,4 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -10,4 +10,4 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("RS_system")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("RS_system")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0a4c756479ff6d53fcf26ddccd18fdd64a07c2f6")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+bec656b105cc858404ace22d1d46cd053a5d0fd7")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("RS_system")]
|
[assembly: System.Reflection.AssemblyProductAttribute("RS_system")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("RS_system")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("RS_system")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
a52affd2506f3cdbc9cb36fe76b525426724cc22cc4e60ef2470282155247555
|
207587d655de51326030a07a0184e67ca76d93743141a95cdbc5efdc3e4541f1
|
||||||
|
|||||||
@@ -152,6 +152,30 @@ build_metadata.AdditionalFiles.CssScope =
|
|||||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvQ29udGFiaWxpZGFkR2VuZXJhbC9SZWdpc3Ryb01lbnN1YWwuY3NodG1s
|
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvQ29udGFiaWxpZGFkR2VuZXJhbC9SZWdpc3Ryb01lbnN1YWwuY3NodG1s
|
||||||
build_metadata.AdditionalFiles.CssScope =
|
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]
|
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/Estados/Create.cshtml]
|
||||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvRXN0YWRvcy9DcmVhdGUuY3NodG1s
|
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvRXN0YWRvcy9DcmVhdGUuY3NodG1s
|
||||||
build_metadata.AdditionalFiles.CssScope =
|
build_metadata.AdditionalFiles.CssScope =
|
||||||
@@ -184,6 +208,10 @@ build_metadata.AdditionalFiles.CssScope =
|
|||||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvTWllbWJyby9FZGl0LmNzaHRtbA==
|
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvTWllbWJyby9FZGl0LmNzaHRtbA==
|
||||||
build_metadata.AdditionalFiles.CssScope =
|
build_metadata.AdditionalFiles.CssScope =
|
||||||
|
|
||||||
|
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/Miembro/Importar.cshtml]
|
||||||
|
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvTWllbWJyby9JbXBvcnRhci5jc2h0bWw=
|
||||||
|
build_metadata.AdditionalFiles.CssScope =
|
||||||
|
|
||||||
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/Miembro/Index.cshtml]
|
[/home/adalberto/RiderProjects/RS_system/RS_system/Views/Miembro/Index.cshtml]
|
||||||
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvTWllbWJyby9JbmRleC5jc2h0bWw=
|
build_metadata.AdditionalFiles.TargetPath = Vmlld3MvTWllbWJyby9JbmRleC5jc2h0bWw=
|
||||||
build_metadata.AdditionalFiles.CssScope =
|
build_metadata.AdditionalFiles.CssScope =
|
||||||
|
|||||||
Binary file not shown.
@@ -1 +1 @@
|
|||||||
8269c6b6fd373b3f46013b10aa9c68fa849af0f8b9f46b71c9398c8b327dec83
|
cf5eb9535656c70fb44bcab29751e1b8ead46c4e4f39ae6fedbde6c1dae54744
|
||||||
|
|||||||
@@ -266,7 +266,6 @@
|
|||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/tlmqwhkg3d-ifse5yxmqk.gz
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/tlmqwhkg3d-ifse5yxmqk.gz
|
||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/b6c3bvqukf-ifse5yxmqk.gz
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/b6c3bvqukf-ifse5yxmqk.gz
|
||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets.build.json
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets.build.json
|
||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets.build.json.cache
|
|
||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets.development.json
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets.development.json
|
||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets.build.endpoints.json
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets.build.endpoints.json
|
||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/RS_system.csproj.Up2Date
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/RS_system.csproj.Up2Date
|
||||||
@@ -277,3 +276,14 @@
|
|||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/ref/RS_system.dll
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/ref/RS_system.dll
|
||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/lc3k1q6eo4-cr0snyzw1m.gz
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/lc3k1q6eo4-cr0snyzw1m.gz
|
||||||
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/m7f2490r97-cr0snyzw1m.gz
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/m7f2490r97-cr0snyzw1m.gz
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/e79wfobnuv-lc8ee02c5q.gz
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/z2cv867s5m-ga728ncyli.gz
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/dpe32h769j-rise9grasc.gz
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/ubjjtv0x1g-4bsvp4jd9h.gz
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/compressed/tlvbvx8n5g-pr0jyv6zw7.gz
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets/msbuild.RS_system.Microsoft.AspNetCore.StaticWebAssets.props
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets/msbuild.RS_system.Microsoft.AspNetCore.StaticWebAssetEndpoints.props
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets/msbuild.build.RS_system.props
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets/msbuild.buildMultiTargeting.RS_system.props
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets/msbuild.buildTransitive.RS_system.props
|
||||||
|
/home/adalberto/RiderProjects/RS_system/RS_system/obj/Debug/net9.0/staticwebassets.pack.json
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
FIiThG6o4LKnL0aAIanau0zkgrgpEoNVs6Tge42QuR8=
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -20,7 +20,7 @@
|
|||||||
"net9.0"
|
"net9.0"
|
||||||
],
|
],
|
||||||
"sources": {
|
"sources": {
|
||||||
"/home/adalberto/.dotnet/library-packs": {},
|
"/usr/lib64/dotnet/library-packs": {},
|
||||||
"https://api.nuget.org/v3/index.json": {}
|
"https://api.nuget.org/v3/index.json": {}
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"auditLevel": "low",
|
"auditLevel": "low",
|
||||||
"auditMode": "direct"
|
"auditMode": "direct"
|
||||||
},
|
},
|
||||||
"SdkAnalysisLevel": "9.0.300"
|
"SdkAnalysisLevel": "9.0.100"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
"net9.0": {
|
"net9.0": {
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "/home/adalberto/.dotnet/sdk/9.0.300/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "/usr/lib64/dotnet/sdk/9.0.113/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/adalberto/.nuget/packages/</NuGetPackageRoot>
|
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/adalberto/.nuget/packages/</NuGetPackageRoot>
|
||||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/adalberto/.nuget/packages/</NuGetPackageFolders>
|
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/adalberto/.nuget/packages/</NuGetPackageFolders>
|
||||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.13.2</NuGetToolVersion>
|
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
<SourceRoot Include="/home/adalberto/.nuget/packages/" />
|
<SourceRoot Include="/home/adalberto/.nuget/packages/" />
|
||||||
|
|||||||
@@ -3332,7 +3332,7 @@
|
|||||||
"net9.0"
|
"net9.0"
|
||||||
],
|
],
|
||||||
"sources": {
|
"sources": {
|
||||||
"/home/adalberto/.dotnet/library-packs": {},
|
"/usr/lib64/dotnet/library-packs": {},
|
||||||
"https://api.nuget.org/v3/index.json": {}
|
"https://api.nuget.org/v3/index.json": {}
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
@@ -3351,7 +3351,7 @@
|
|||||||
"auditLevel": "low",
|
"auditLevel": "low",
|
||||||
"auditMode": "direct"
|
"auditMode": "direct"
|
||||||
},
|
},
|
||||||
"SdkAnalysisLevel": "9.0.300"
|
"SdkAnalysisLevel": "9.0.100"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
"net9.0": {
|
"net9.0": {
|
||||||
@@ -3407,7 +3407,7 @@
|
|||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "/home/adalberto/.dotnet/sdk/9.0.300/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "/usr/lib64/dotnet/sdk/9.0.113/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "mE5kunE1L6A=",
|
"dgSpecHash": "3JUQhcRdx7k=",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "/home/adalberto/RiderProjects/RS_system/RS_system/RS_system.csproj",
|
"projectFilePath": "/home/adalberto/RiderProjects/RS_system/RS_system/RS_system.csproj",
|
||||||
"expectedPackageFiles": [
|
"expectedPackageFiles": [
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"restore":{"projectUniqueName":"/home/adalberto/RiderProjects/RS_system/RS_system/RS_system.csproj","projectName":"RS_system","projectPath":"/home/adalberto/RiderProjects/RS_system/RS_system/RS_system.csproj","outputPath":"/home/adalberto/RiderProjects/RS_system/RS_system/obj/","projectStyle":"PackageReference","originalTargetFrameworks":["net9.0"],"sources":{"/home/adalberto/.dotnet/library-packs":{},"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net9.0":{"targetAlias":"net9.0","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"9.0.300"}"frameworks":{"net9.0":{"targetAlias":"net9.0","dependencies":{"BCrypt.Net-Next":{"target":"Package","version":"[4.0.3, )"},"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore":{"target":"Package","version":"[9.0.5, )"},"Microsoft.AspNetCore.Identity.EntityFrameworkCore":{"target":"Package","version":"[9.0.5, )"},"Microsoft.AspNetCore.Identity.UI":{"target":"Package","version":"[9.0.5, )"},"Microsoft.EntityFrameworkCore.Design":{"include":"Runtime, Build, Native, ContentFiles, Analyzers, BuildTransitive","suppressParent":"All","target":"Package","version":"[9.0.5, )"},"Microsoft.EntityFrameworkCore.Tools":{"target":"Package","version":"[9.0.5, )"},"Npgsql.EntityFrameworkCore.PostgreSQL":{"target":"Package","version":"[9.0.3, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.AspNetCore.App":{"privateAssets":"none"},"Microsoft.NETCore.App":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"/home/adalberto/.dotnet/sdk/9.0.300/PortableRuntimeIdentifierGraph.json"}}
|
"restore":{"projectUniqueName":"/home/adalberto/RiderProjects/RS_system/RS_system/RS_system.csproj","projectName":"RS_system","projectPath":"/home/adalberto/RiderProjects/RS_system/RS_system/RS_system.csproj","outputPath":"/home/adalberto/RiderProjects/RS_system/RS_system/obj/","projectStyle":"PackageReference","originalTargetFrameworks":["net9.0"],"sources":{"/usr/lib64/dotnet/library-packs":{},"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net9.0":{"targetAlias":"net9.0","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"9.0.100"}"frameworks":{"net9.0":{"targetAlias":"net9.0","dependencies":{"BCrypt.Net-Next":{"target":"Package","version":"[4.0.3, )"},"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore":{"target":"Package","version":"[9.0.5, )"},"Microsoft.AspNetCore.Identity.EntityFrameworkCore":{"target":"Package","version":"[9.0.5, )"},"Microsoft.AspNetCore.Identity.UI":{"target":"Package","version":"[9.0.5, )"},"Microsoft.EntityFrameworkCore.Design":{"include":"Runtime, Build, Native, ContentFiles, Analyzers, BuildTransitive","suppressParent":"All","target":"Package","version":"[9.0.5, )"},"Microsoft.EntityFrameworkCore.Tools":{"target":"Package","version":"[9.0.5, )"},"Npgsql.EntityFrameworkCore.PostgreSQL":{"target":"Package","version":"[9.0.3, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.AspNetCore.App":{"privateAssets":"none"},"Microsoft.NETCore.App":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"/usr/lib64/dotnet/sdk/9.0.113/PortableRuntimeIdentifierGraph.json"}}
|
||||||
@@ -1 +1 @@
|
|||||||
17677521665296480
|
17717199317703256
|
||||||
@@ -1 +1 @@
|
|||||||
17677521665296480
|
17717199317703256
|
||||||
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();
|
||||||
|
}
|
||||||
123
RS_system/wwwroot/js/offline-db.js
Normal file
123
RS_system/wwwroot/js/offline-db.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* IndexedDB Wrapper for Offline Contabilidad
|
||||||
|
* Stores pending transactions when offline
|
||||||
|
*/
|
||||||
|
|
||||||
|
const OfflineDB = {
|
||||||
|
dbName: 'ContabilidadOfflineDB',
|
||||||
|
version: 1,
|
||||||
|
storeName: 'pendingTransactions',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
autoIncrement: true
|
||||||
|
});
|
||||||
|
objectStore.createIndex('timestamp', 'timestamp', { unique: false });
|
||||||
|
objectStore.createIndex('reporteId', 'reporteId', { unique: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a pending transaction to the queue
|
||||||
|
*/
|
||||||
|
async addPending(reporteId, movimientos) {
|
||||||
|
const db = await this.init();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||||
|
const store = transaction.objectStore(this.storeName);
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
reporteId: reporteId,
|
||||||
|
movimientos: movimientos,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = store.add(record);
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all pending transactions
|
||||||
|
*/
|
||||||
|
async getAllPending() {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a specific pending transaction by id
|
||||||
|
*/
|
||||||
|
async removePending(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 = () => resolve();
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all pending transactions
|
||||||
|
*/
|
||||||
|
async clearPending() {
|
||||||
|
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 = () => resolve();
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get count of pending transactions
|
||||||
|
*/
|
||||||
|
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 request = store.count();
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
229
RS_system/wwwroot/js/offline-manager.js
Normal file
229
RS_system/wwwroot/js/offline-manager.js
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
/**
|
||||||
|
* Offline Manager for Contabilidad
|
||||||
|
* Handles connection monitoring, offline queue, and synchronization
|
||||||
|
*/
|
||||||
|
|
||||||
|
const OfflineManager = {
|
||||||
|
isOnline: navigator.onLine,
|
||||||
|
isSyncing: false,
|
||||||
|
statusBadge: null,
|
||||||
|
pendingCounter: null,
|
||||||
|
syncInProgress: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the offline manager
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.statusBadge = document.getElementById('connectionStatus');
|
||||||
|
this.pendingCounter = document.getElementById('pendingCount');
|
||||||
|
|
||||||
|
// Listen for online/offline events
|
||||||
|
window.addEventListener('online', () => this.handleOnline());
|
||||||
|
window.addEventListener('offline', () => this.handleOffline());
|
||||||
|
|
||||||
|
// Initial status
|
||||||
|
this.updateStatus();
|
||||||
|
this.updatePendingCount();
|
||||||
|
|
||||||
|
// Check for pending transactions on load
|
||||||
|
if (this.isOnline) {
|
||||||
|
this.syncPending();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle online event
|
||||||
|
*/
|
||||||
|
async handleOnline() {
|
||||||
|
this.isOnline = true;
|
||||||
|
this.updateStatus();
|
||||||
|
console.log('Connection restored - starting sync...');
|
||||||
|
await this.syncPending();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle offline event
|
||||||
|
*/
|
||||||
|
handleOffline() {
|
||||||
|
this.isOnline = false;
|
||||||
|
this.updateStatus();
|
||||||
|
console.log('Connection lost - offline mode enabled');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update connection status badge
|
||||||
|
*/
|
||||||
|
updateStatus() {
|
||||||
|
if (!this.statusBadge) return;
|
||||||
|
|
||||||
|
if (this.isSyncing) {
|
||||||
|
this.statusBadge.className = 'badge bg-warning';
|
||||||
|
this.statusBadge.innerHTML = '<i class="fas fa-sync fa-spin"></i> Sincronizando';
|
||||||
|
} else if (this.isOnline) {
|
||||||
|
this.statusBadge.className = 'badge bg-success';
|
||||||
|
this.statusBadge.innerHTML = '<i class="fas fa-wifi"></i> En línea';
|
||||||
|
} else {
|
||||||
|
this.statusBadge.className = 'badge bg-secondary';
|
||||||
|
this.statusBadge.innerHTML = '<i class="fas fa-wifi-slash"></i> Sin conexión';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update pending transaction counter
|
||||||
|
*/
|
||||||
|
async updatePendingCount() {
|
||||||
|
if (!this.pendingCounter) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const count = await OfflineDB.getPendingCount();
|
||||||
|
if (count > 0) {
|
||||||
|
this.pendingCounter.textContent = `${count} pendiente${count > 1 ? 's' : ''}`;
|
||||||
|
this.pendingCounter.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
this.pendingCounter.style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating pending count:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save transaction (online or offline)
|
||||||
|
*/
|
||||||
|
async saveTransaction(reporteId, movimientos, url) {
|
||||||
|
if (this.isOnline) {
|
||||||
|
// Try to save directly
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ReporteId: reporteId,
|
||||||
|
Movimientos: movimientos
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, message: 'Guardado exitosamente', data: result };
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || 'Error desconocido');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If online but request failed, save to queue
|
||||||
|
console.warn('Request failed, saving to offline queue:', error);
|
||||||
|
await this.saveOffline(reporteId, movimientos);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
offline: true,
|
||||||
|
message: 'Guardado en cola offline. Se sincronizará automáticamente.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Save to offline queue
|
||||||
|
await this.saveOffline(reporteId, movimientos);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
offline: true,
|
||||||
|
message: 'Sin conexión. Guardado en cola offline.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save to offline queue
|
||||||
|
*/
|
||||||
|
async saveOffline(reporteId, movimientos) {
|
||||||
|
await OfflineDB.addPending(reporteId, movimientos);
|
||||||
|
await this.updatePendingCount();
|
||||||
|
console.log('Transaction saved to offline queue');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize pending transactions
|
||||||
|
*/
|
||||||
|
async syncPending() {
|
||||||
|
if (!this.isOnline || this.syncInProgress) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.syncInProgress = true;
|
||||||
|
this.isSyncing = true;
|
||||||
|
this.updateStatus();
|
||||||
|
|
||||||
|
const pending = await OfflineDB.getAllPending();
|
||||||
|
|
||||||
|
if (pending.length === 0) {
|
||||||
|
console.log('No pending transactions to sync');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Syncing ${pending.length} pending transaction(s)...`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
for (const transaction of pending) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/ContabilidadGeneral/GuardarBulk', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
ReporteId: transaction.reporteId,
|
||||||
|
Movimientos: transaction.movimientos
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
await OfflineDB.removePending(transaction.id);
|
||||||
|
successCount++;
|
||||||
|
console.log(`Transaction ${transaction.id} synced successfully`);
|
||||||
|
} else {
|
||||||
|
failCount++;
|
||||||
|
console.error(`Transaction ${transaction.id} failed:`, result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failCount++;
|
||||||
|
console.error(`Error syncing transaction ${transaction.id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updatePendingCount();
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
alert(`Sincronización completa: ${successCount} registro(s) guardado(s).${failCount > 0 ? ` ${failCount} fallido(s).` : ''}`);
|
||||||
|
location.reload(); // Refresh to show updated data
|
||||||
|
} else if (failCount > 0) {
|
||||||
|
alert(`Error en sincronización: ${failCount} registro(s) no se pudieron guardar.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Sync error:', error);
|
||||||
|
} finally {
|
||||||
|
this.syncInProgress = false;
|
||||||
|
this.isSyncing = false;
|
||||||
|
this.updateStatus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually trigger sync
|
||||||
|
*/
|
||||||
|
async manualSync() {
|
||||||
|
if (!this.isOnline) {
|
||||||
|
alert('No hay conexión a internet. La sincronización se realizará automáticamente cuando se restaure la conexión.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.syncPending();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => OfflineManager.init());
|
||||||
|
} else {
|
||||||
|
OfflineManager.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