This commit is contained in:
2026-02-22 14:38:53 -06:00
parent bec656b105
commit a73de4a4fa
47 changed files with 4290 additions and 3 deletions

View 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));
}
}

View 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
});
}
}