add new
This commit is contained in:
164
RS_system/Views/ContabilidadGeneral/Consolidado.cshtml
Normal file
164
RS_system/Views/ContabilidadGeneral/Consolidado.cshtml
Normal file
@@ -0,0 +1,164 @@
|
||||
@model Rs_system.Models.ReporteMensualGeneral
|
||||
@{
|
||||
ViewData["Title"] = $"Consolidado - {Model.NombreMes} {Model.Anio}";
|
||||
var ingresosPorCat = ViewBag.ConsolidadoIngresos as Dictionary<string, decimal> ?? new Dictionary<string, decimal>();
|
||||
var egresosPorCat = ViewBag.ConsolidadoEgresos as Dictionary<string, decimal> ?? new Dictionary<string, decimal>();
|
||||
|
||||
var totalIngresosMes = ingresosPorCat.Values.Sum();
|
||||
var totalEgresosMes = egresosPorCat.Values.Sum();
|
||||
var saldoFinal = Model.SaldoInicial + totalIngresosMes - totalEgresosMes;
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 text-gray-800">Consolidado: @Model.NombreMes @Model.Anio</h1>
|
||||
<div>
|
||||
<a asp-action="RegistroMensual" asp-route-id="@Model.Id" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-edit"></i> Ver Detalle Registros
|
||||
</a>
|
||||
<a asp-action="Index" class="btn btn-secondary btn-sm ml-2">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
<button onclick="window.print()" class="btn btn-info btn-sm ml-2">
|
||||
<i class="fas fa-print"></i> Imprimir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resumen Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Saldo Inicial</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.SaldoInicial.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Total Ingresos</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@totalIngresosMes.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-danger shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Total Egresos</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@totalEgresosMes.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-info shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Saldo Final</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@saldoFinal.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Ingresos Chart/Table -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-success">Desglose de Ingresos</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Categoría</th>
|
||||
<th class="text-right">Monto</th>
|
||||
<th class="text-right">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in ingresosPorCat.OrderByDescending(x => x.Value))
|
||||
{
|
||||
var porcentaje = totalIngresosMes > 0 ? (item.Value / totalIngresosMes) * 100 : 0;
|
||||
<tr>
|
||||
<td>@item.Key</td>
|
||||
<td class="text-right">@item.Value.ToString("C")</td>
|
||||
<td class="text-right">@porcentaje.ToString("F1")%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-weight-bold">
|
||||
<td>Total</td>
|
||||
<td class="text-right">@totalIngresosMes.ToString("C")</td>
|
||||
<td class="text-right">100%</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Egresos Chart/Table -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-danger">Desglose de Egresos</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Categoría</th>
|
||||
<th class="text-right">Monto</th>
|
||||
<th class="text-right">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in egresosPorCat.OrderByDescending(x => x.Value))
|
||||
{
|
||||
var porcentaje = totalEgresosMes > 0 ? (item.Value / totalEgresosMes) * 100 : 0;
|
||||
<tr>
|
||||
<td>@item.Key</td>
|
||||
<td class="text-right">@item.Value.ToString("C")</td>
|
||||
<td class="text-right">@porcentaje.ToString("F1")%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-weight-bold">
|
||||
<td>Total</td>
|
||||
<td class="text-right">@totalEgresosMes.ToString("C")</td>
|
||||
<td class="text-right">100%</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
222
RS_system/Views/ContabilidadGeneral/GestionCategorias.cshtml
Normal file
222
RS_system/Views/ContabilidadGeneral/GestionCategorias.cshtml
Normal file
@@ -0,0 +1,222 @@
|
||||
@{
|
||||
ViewData["Title"] = "Gestión de Categorías";
|
||||
var categoriasIngreso = ViewBag.CategoriasIngreso as List<Rs_system.Models.CategoriaIngreso>;
|
||||
var categoriasEgreso = ViewBag.CategoriasEgreso as List<Rs_system.Models.CategoriaEgreso>;
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 text-gray-800">Gestión de Categorías</h1>
|
||||
<a asp-action="Index" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success">@TempData["Success"]</div>
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<!-- Categorías de Ingreso -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-success">Categorías de Ingreso</h6>
|
||||
<button class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#modalCrearIngreso">
|
||||
<i class="fas fa-plus"></i> Nueva
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Descripción</th>
|
||||
<th style="width: 100px;">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in categoriasIngreso)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Nombre</td>
|
||||
<td>@item.Descripcion</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-primary btn-sm"
|
||||
onclick="editarIngreso(@item.Id, '@item.Nombre', '@item.Descripcion')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form asp-action="EliminarCategoriaIngreso" asp-route-id="@item.Id" method="post" class="d-inline" onsubmit="return confirm('¿Eliminar esta categoría?');">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categorías de Egreso -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-danger">Categorías de Egreso</h6>
|
||||
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#modalCrearEgreso">
|
||||
<i class="fas fa-plus"></i> Nueva
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Descripción</th>
|
||||
<th style="width: 100px;">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in categoriasEgreso)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Nombre</td>
|
||||
<td>@item.Descripcion</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-primary btn-sm"
|
||||
onclick="editarEgreso(@item.Id, '@item.Nombre', '@item.Descripcion')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form asp-action="EliminarCategoriaEgreso" asp-route-id="@item.Id" method="post" class="d-inline" onsubmit="return confirm('¿Eliminar esta categoría?');">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Crear Ingreso -->
|
||||
<div class="modal fade" id="modalCrearIngreso" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Nueva Categoría de Ingreso</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form asp-action="CrearCategoriaIngreso" method="post">
|
||||
<div class="modal-body">
|
||||
<div class="form-group mb-3">
|
||||
<label>Nombre</label>
|
||||
<input name="Nombre" class="form-control" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label>Descripción</label>
|
||||
<input name="Descripcion" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Crear Egreso -->
|
||||
<div class="modal fade" id="modalCrearEgreso" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Nueva Categoría de Egreso</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form asp-action="CrearCategoriaEgreso" method="post">
|
||||
<div class="modal-body">
|
||||
<div class="form-group mb-3">
|
||||
<label>Nombre</label>
|
||||
<input name="Nombre" class="form-control" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label>Descripción</label>
|
||||
<input name="Descripcion" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Editar (Compartido y poblado por JS) -->
|
||||
<div class="modal fade" id="modalEditar" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="tituloEditar">Editar Categoría</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="formEditar" method="post">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="Id" id="editId" />
|
||||
<div class="form-group mb-3">
|
||||
<label>Nombre</label>
|
||||
<input name="Nombre" id="editNombre" class="form-control" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label>Descripción</label>
|
||||
<input name="Descripcion" id="editDescripcion" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Actualizar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function editarIngreso(id, nombre, descripcion) {
|
||||
$('#tituloEditar').text('Editar Categoría de Ingreso');
|
||||
$('#formEditar').attr('action', '@Url.Action("EditarCategoriaIngreso")');
|
||||
llenarModal(id, nombre, descripcion);
|
||||
}
|
||||
|
||||
function editarEgreso(id, nombre, descripcion) {
|
||||
$('#tituloEditar').text('Editar Categoría de Egreso');
|
||||
$('#formEditar').attr('action', '@Url.Action("EditarCategoriaEgreso")');
|
||||
llenarModal(id, nombre, descripcion);
|
||||
}
|
||||
|
||||
function llenarModal(id, nombre, descripcion) {
|
||||
$('#editId').val(id);
|
||||
$('#editNombre').val(nombre);
|
||||
$('#editDescripcion').val(descripcion);
|
||||
|
||||
var modalEl = document.getElementById('modalEditar');
|
||||
var modal = new bootstrap.Modal(modalEl);
|
||||
modal.show();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
124
RS_system/Views/ContabilidadGeneral/Index.cshtml
Normal file
124
RS_system/Views/ContabilidadGeneral/Index.cshtml
Normal file
@@ -0,0 +1,124 @@
|
||||
@model List<Rs_system.Models.ReporteMensualGeneral>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Contabilidad General";
|
||||
var anioActual = ViewBag.Anio;
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 text-gray-800">Contabilidad General</h1>
|
||||
|
||||
<form asp-action="Index" method="get" class="form-inline">
|
||||
<label class="me-2">Año:</label>
|
||||
<select name="anio" class="form-control me-2" asp-items="ViewBag.Anios" onchange="this.form.submit()">
|
||||
<option value="@anioActual" selected>@anioActual</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="alert alert-danger">@TempData["Error"]</div>
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Reportes Mensuales @anioActual</h6>
|
||||
<div>
|
||||
<a asp-action="GestionCategorias" class="btn btn-info btn-sm">
|
||||
<i class="fas fa-tags"></i> Gestionar Categorías
|
||||
</a>
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#abrirMesModal">
|
||||
<i class="fas fa-plus"></i> Abrir Nuevo Mes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mes</th>
|
||||
<th>Saldo Inicial</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.NombreMes</td>
|
||||
<td>@item.SaldoInicial.ToString("C")</td>
|
||||
<td>
|
||||
@if (item.Cerrado)
|
||||
{
|
||||
<span class="badge bg-secondary">Cerrado</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-success">Abierto</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="RegistroMensual" asp-route-id="@item.Id" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-edit"></i> Gestionar
|
||||
</a>
|
||||
<a asp-action="Consolidado" asp-route-id="@item.Id" class="btn btn-info btn-sm">
|
||||
<i class="fas fa-chart-pie"></i> Ver Consolidado
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">No hay reportes para este año.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Abrir Mes -->
|
||||
<div class="modal fade" id="abrirMesModal" tabindex="-1" role="dialog" aria-labelledby="abrirMesModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="abrirMesModalLabel">Abrir Nuevo Mes Contable</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form asp-action="AbrirMes" method="post">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="anio" value="@anioActual" />
|
||||
<div class="form-group mb-3">
|
||||
<label>Mes</label>
|
||||
<select name="mes" class="form-select" required>
|
||||
@for (int i = 1; i <= 12; i++)
|
||||
{
|
||||
var nMes = new DateTime(2000, i, 1).ToString("MMMM", new System.Globalization.CultureInfo("es-ES"));
|
||||
<option value="@i">@nMes</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-muted small">
|
||||
Nota: Al abrir el mes, se calculará automáticamente el saldo inicial basado en el cierre del mes anterior.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Crear Reporte</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
479
RS_system/Views/ContabilidadGeneral/RegistroMensual.cshtml
Normal file
479
RS_system/Views/ContabilidadGeneral/RegistroMensual.cshtml
Normal file
@@ -0,0 +1,479 @@
|
||||
@model Rs_system.Models.ReporteMensualGeneral
|
||||
@{
|
||||
ViewData["Title"] = $"Registro - {Model.NombreMes} {Model.Anio}";
|
||||
var categoriasIngreso = ViewBag.CategoriasIngreso as List<Rs_system.Models.CategoriaIngreso>;
|
||||
var categoriasEgreso = ViewBag.CategoriasEgreso as List<Rs_system.Models.CategoriaEgreso>;
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h1 class="h3 text-gray-800">Registro Mensual: @Model.NombreMes @Model.Anio</h1>
|
||||
<h5 class="text-secondary">
|
||||
Saldo Actual: <span class="font-weight-bold @(ViewBag.SaldoActual >= 0 ? "text-success" : "text-danger")">@ViewBag.SaldoActual?.ToString("C")</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div>
|
||||
<a asp-action="Index" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
@if (!Model.Cerrado)
|
||||
{
|
||||
<button id="btnGuardar" class="btn btn-primary btn-sm ml-2">
|
||||
<i class="fas fa-save"></i> Guardar Cambios
|
||||
</button>
|
||||
<form asp-action="CerrarMes" asp-route-id="@Model.Id" method="post" class="d-inline ml-2" onsubmit="return confirm('¿Está seguro de cerrar este mes? No podrá realizar más cambios.');">
|
||||
<button type="submit" class="btn btn-warning btn-sm">
|
||||
<i class="fas fa-lock"></i> Cerrar Mes
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge badge-secondary ml-2 p-2">Mes Cerrado</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
@TempData["Success"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
@TempData["Error"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm mb-0" id="tablaRegistros">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th style="width: 120px;">Fecha</th>
|
||||
<th style="width: 120px;">Tipo</th>
|
||||
<th style="width: 200px;">Categoría</th>
|
||||
<th>Descripción</th>
|
||||
<th style="width: 120px;">Comprobante</th>
|
||||
<th style="width: 50px;"></th>
|
||||
<th style="width: 150px;">Monto</th>
|
||||
@if (!Model.Cerrado)
|
||||
{
|
||||
<th style="width: 50px;"></th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbodyRegistros">
|
||||
<!-- Rows rendered by JS -->
|
||||
</tbody>
|
||||
@if (!Model.Cerrado)
|
||||
{
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="9" class="text-center p-2">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="agregarFila()">
|
||||
<i class="fas fa-plus"></i> Agregar Fila
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Adjuntos -->
|
||||
<div class="modal fade" id="modalAdjuntos" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Adjuntos del Movimiento</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="adjuntoMovimientoId" />
|
||||
|
||||
@if (!Model.Cerrado)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subir Archivos (Imágenes o PDF)</label>
|
||||
<!-- Usar 'form' envolvente para resetear fácil -->
|
||||
<form id="formSubirAdjuntos">
|
||||
<div class="input-group">
|
||||
<input type="file" class="form-control" id="inputArchivos" multiple accept="image/*,.pdf">
|
||||
<button class="btn btn-primary" type="button" onclick="subirArchivos()">
|
||||
<i class="fas fa-upload"></i> Subir
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Fecha</th>
|
||||
<th style="width: 150px;">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbodyAdjuntos">
|
||||
<!-- Populated via JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
const esCerrado = @Model.Cerrado.ToString().ToLower();
|
||||
const reporteId = @Model.Id;
|
||||
|
||||
// Data from server
|
||||
let rawRegistros = @Html.Raw(Json.Serialize(Model.Movimientos.Select(m => new {
|
||||
m.Id,
|
||||
m.Tipo, // 1 = Ingreso, 2 = Egreso
|
||||
m.CategoriaIngresoId,
|
||||
m.CategoriaEgresoId,
|
||||
m.Monto,
|
||||
Fecha = m.Fecha.ToString("yyyy-MM-dd"),
|
||||
m.Descripcion,
|
||||
m.NumeroComprobante
|
||||
})));
|
||||
|
||||
// Normalizar datos a mi estructura interna para evitar problemas de mayúsculas/minúsculas
|
||||
let registros = rawRegistros.map(r => ({
|
||||
id: r.Id || r.id || 0,
|
||||
tipo: (r.Tipo !== undefined ? r.Tipo : r.tipo),
|
||||
categoriaIngresoId: r.CategoriaIngresoId || r.categoriaIngresoId,
|
||||
categoriaEgresoId: r.CategoriaEgresoId || r.categoriaEgresoId,
|
||||
monto: r.Monto !== undefined ? r.Monto : r.monto,
|
||||
fecha: r.Fecha || r.fecha,
|
||||
descripcion: r.Descripcion || r.descripcion,
|
||||
numeroComprobante: r.NumeroComprobante || r.numeroComprobante
|
||||
}));
|
||||
|
||||
// Categories helper
|
||||
const rawCatsIngreso = @Html.Raw(Json.Serialize(categoriasIngreso.Select(c => new { c.Id, c.Nombre })));
|
||||
const rawCatsEgreso = @Html.Raw(Json.Serialize(categoriasEgreso.Select(c => new { c.Id, c.Nombre })));
|
||||
|
||||
const catsIngreso = rawCatsIngreso.map(c => ({ id: c.Id || c.id, nombre: c.Nombre || c.nombre }));
|
||||
const catsEgreso = rawCatsEgreso.map(c => ({ id: c.Id || c.id, nombre: c.Nombre || c.nombre }));
|
||||
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('tbodyRegistros');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
registros.forEach((reg, index) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.index = index;
|
||||
|
||||
// Logic for Attachment Button
|
||||
const hasId = reg.id && reg.id > 0;
|
||||
const btnAdjuntoClass = hasId ? "btn-info" : "btn-secondary";
|
||||
const btnAdjuntoTitle = hasId ? "Gestionar Adjuntos" : "Guarde primero para adjuntar";
|
||||
// IMPORTANTE: onclick debe ser cadena vacía si no hay ID, pero el disabled lo controla.
|
||||
const btnAdjuntoAction = hasId ? `abrirAdjuntos(${reg.id})` : "";
|
||||
const btnAdjuntoIcon = '<i class="fas fa-paperclip"></i>';
|
||||
const disabledAttr = hasId ? "" : "disabled";
|
||||
|
||||
if (esCerrado) {
|
||||
// Read-only view
|
||||
const tipoTexto = reg.tipo === 1 ? '<span class="text-success">Ingreso</span>' : '<span class="text-danger">Egreso</span>';
|
||||
const catNombre = reg.tipo === 0
|
||||
? (catsIngreso.find(c => c.id === reg.categoriaIngresoId)?.nombre || '-')
|
||||
: (catsEgreso.find(c => c.id === reg.categoriaEgresoId)?.nombre || '-');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="text-center">${index + 1}</td>
|
||||
<td>${reg.fecha}</td>
|
||||
<td>${tipoTexto}</td>
|
||||
<td>${catNombre}</td>
|
||||
<td>${reg.descripcion || ''}</td>
|
||||
<td>${reg.numeroComprobante || ''}</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm ${btnAdjuntoClass}" onclick="${btnAdjuntoAction}" title="${btnAdjuntoTitle}" ${disabledAttr}>
|
||||
${btnAdjuntoIcon}
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-end">${parseFloat(reg.monto).toFixed(2)}</td>
|
||||
<td></td>
|
||||
`;
|
||||
} else {
|
||||
// Editable view
|
||||
tr.innerHTML = `
|
||||
<td class="text-center align-middle">${index + 1}</td>
|
||||
<td><input type="date" class="form-control form-control-sm" value="${reg.fecha}" onchange="updateReg(${index}, 'fecha', this.value)"></td>
|
||||
<td>
|
||||
<select class="form-control form-control-sm" onchange="cambiarTipo(${index}, this.value)">
|
||||
<option value="1" ${reg.tipo === 1 ? 'selected' : ''}>Ingreso</option>
|
||||
<option value="2" ${reg.tipo === 2 ? 'selected' : ''}>Egreso</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-control form-control-sm" onchange="updateReg(${index}, 'categoria', this.value)">
|
||||
<option value="">Seleccione...</option>
|
||||
${renderCatsOptions(reg.tipo, reg.tipo === 1 ? reg.categoriaIngresoId : reg.categoriaEgresoId)}
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" class="form-control form-control-sm" value="${reg.descripcion || ''}" onchange="updateReg(${index}, 'descripcion', this.value)"></td>
|
||||
<td><input type="text" class="form-control form-control-sm" value="${reg.numeroComprobante || ''}" onchange="updateReg(${index}, 'numeroComprobante', this.value)"></td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm ${btnAdjuntoClass}" onclick="${btnAdjuntoAction}" title="${btnAdjuntoTitle}" ${disabledAttr}>
|
||||
${btnAdjuntoIcon}
|
||||
</button>
|
||||
</td>
|
||||
<td><input type="number" step="0.01" class="form-control form-control-sm text-end" value="${reg.monto}" onchange="updateReg(${index}, 'monto', this.value)"></td>
|
||||
<td class="text-center align-middle">
|
||||
<button class="btn btn-danger btn-sm py-0" onclick="eliminarFila(${index})" title="Eliminar"><i class="fas fa-times"></i></button>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function renderCatsOptions(tipo, selectedId) {
|
||||
const list = tipo == 1 ? catsIngreso : catsEgreso; // loose equality for string/number match
|
||||
return list.map(c => `<option value="${c.id}" ${c.id == selectedId ? 'selected' : ''}>${c.nombre}</option>`).join('');
|
||||
}
|
||||
|
||||
function agregarFila() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
registros.push({
|
||||
id: 0,
|
||||
tipo: 1,
|
||||
categoriaIngresoId: null,
|
||||
categoriaEgresoId: null,
|
||||
monto: 0,
|
||||
fecha: today,
|
||||
descripcion: '',
|
||||
numeroComprobante: ''
|
||||
});
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function eliminarFila(index) {
|
||||
registros.splice(index, 1);
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function cambiarTipo(index, nuevoTipo) {
|
||||
registros[index].tipo = parseInt(nuevoTipo);
|
||||
registros[index].categoriaIngresoId = null;
|
||||
registros[index].categoriaEgresoId = null;
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function updateReg(index, field, value) {
|
||||
const reg = registros[index];
|
||||
if (field === 'categoria') {
|
||||
const val = value ? parseInt(value) : null;
|
||||
if (reg.tipo === 1) reg.categoriaIngresoId = val;
|
||||
else reg.categoriaEgresoId = val;
|
||||
} else if (field === 'monto') {
|
||||
reg.monto = parseFloat(value) || 0;
|
||||
} else {
|
||||
reg[field] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: Payload mapping for Server (Server expects PascalCase usually, but standard API accepts camelCase too)
|
||||
document.getElementById('btnGuardar')?.addEventListener('click', async () => {
|
||||
// Validations
|
||||
for (let i = 0; i < registros.length; i++) {
|
||||
const r = registros[i];
|
||||
if (r.monto <= 0) {
|
||||
alert(`Fila ${i+1}: El monto debe ser mayor a 0.`);
|
||||
return;
|
||||
}
|
||||
if (r.tipo === 1 && !r.categoriaIngresoId) {
|
||||
alert(`Fila ${i+1}: Debe seleccionar una categoría de ingreso.`);
|
||||
return;
|
||||
}
|
||||
if (r.tipo === 2 && !r.categoriaEgresoId) {
|
||||
alert(`Fila ${i+1}: Debe seleccionar una categoría de egreso.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnGuardar');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Guardando...';
|
||||
btn.disabled = true;
|
||||
|
||||
// Map back to PascalCase structure just in case server strictly needs it matches DTO
|
||||
const payloadMovimientos = registros.map(r => ({
|
||||
Id: r.id,
|
||||
Tipo: r.tipo,
|
||||
CategoriaIngresoId: r.categoriaIngresoId,
|
||||
CategoriaEgresoId: r.categoriaEgresoId,
|
||||
Monto: r.monto,
|
||||
Fecha: r.fecha,
|
||||
Descripcion: r.descripcion,
|
||||
NumeroComprobante: r.numeroComprobante
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await fetch('@Url.Action("GuardarBulk")', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ReporteId: reporteId,
|
||||
Movimientos: payloadMovimientos
|
||||
})
|
||||
});
|
||||
// ... rest of logic
|
||||
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// Show toast or alert
|
||||
alert('Guardado exitosamente');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Ocurrió un error al guardar.');
|
||||
} finally {
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ================= ADJUNTOS LOGIC =================
|
||||
|
||||
let currentMovId = 0;
|
||||
var myModalAdjuntos;
|
||||
|
||||
function abrirAdjuntos(id) {
|
||||
currentMovId = id;
|
||||
document.getElementById('adjuntoMovimientoId').value = id;
|
||||
|
||||
// Clear previous entries
|
||||
document.getElementById('tbodyAdjuntos').innerHTML = '<tr><td colspan="3" class="text-center">Cargando...</td></tr>';
|
||||
|
||||
if (!myModalAdjuntos) {
|
||||
var el = document.getElementById('modalAdjuntos');
|
||||
myModalAdjuntos = new bootstrap.Modal(el);
|
||||
}
|
||||
myModalAdjuntos.show();
|
||||
|
||||
cargarAdjuntos(id);
|
||||
}
|
||||
|
||||
async function cargarAdjuntos(id) {
|
||||
try {
|
||||
const url = '@Url.Action("ObtenerAdjuntos")?movimientoId=' + id;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('tbodyAdjuntos');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-center">Sin adjuntos</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(adj => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// Si es imagen, mostrar preview pequeño en popover o algo, pero por ahora link simple
|
||||
let link = `<a href="${adj.url}" target="_blank" class="text-decoration-none"><i class="fas fa-external-link-alt"></i> ${adj.nombre}</a>`;
|
||||
|
||||
let deleteBtn = '';
|
||||
if (!esCerrado) {
|
||||
deleteBtn = `<button class="btn btn-danger btn-sm" onclick="eliminarAdjunto(${adj.id})"><i class="fas fa-trash"></i></button>`;
|
||||
}
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${link}</td>
|
||||
<td>${adj.fecha}</td>
|
||||
<td class="text-center">
|
||||
<a href="${adj.url}" download class="btn btn-secondary btn-sm"><i class="fas fa-download"></i></a>
|
||||
${deleteBtn}
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById('tbodyAdjuntos').innerHTML = '<tr><td colspan="3" class="text-danger">Error al cargar adjuntos</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
async function subirArchivos() {
|
||||
const input = document.getElementById('inputArchivos');
|
||||
if (input.files.length === 0) {
|
||||
alert("Seleccione al menos un archivo.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('movimientoId', currentMovId);
|
||||
for (let i = 0; i < input.files.length; i++) {
|
||||
formData.append('archivos', input.files[i]);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('@Url.Action("SubirAdjunto")', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
// Limpiar input y recargar
|
||||
input.value = '';
|
||||
cargarAdjuntos(currentMovId);
|
||||
} else {
|
||||
alert("Error: " + result.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Error al subir archivos.");
|
||||
}
|
||||
}
|
||||
|
||||
async function eliminarAdjunto(id) {
|
||||
if (!confirm('¿Eliminar este adjunto?')) return;
|
||||
try {
|
||||
const res = await fetch('@Url.Action("EliminarAdjunto")?id=' + id, { method: 'POST' });
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
cargarAdjuntos(currentMovId);
|
||||
} else {
|
||||
alert("No se pudo eliminar.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Error al eliminar.");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
if (registros.length === 0 && !esCerrado) {
|
||||
agregarFila();
|
||||
} else {
|
||||
renderTable();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
Reference in New Issue
Block a user