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,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");}
}

View 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>&nbsp;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>
}

View 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>

View 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>

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

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

View 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>