This commit is contained in:
2026-02-01 14:28:17 -06:00
parent 700af7ea60
commit 1784131456
109 changed files with 19894 additions and 0 deletions

View File

@@ -0,0 +1,208 @@
@model Rs_system.Models.ViewModels.ArticuloViewModel
@{
ViewData["Title"] = "Nuevo Artículo";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Nuevo Artículo</h4>
<p class="text-muted mb-0">Registrar un nuevo activo en el inventario</p>
</div>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Volver a la lista
</a>
</div>
<form asp-action="Create" method="post" enctype="multipart/form-data">
<div class="row">
<!-- Left Column: Basic Info -->
<div class="col-lg-8">
<div class="card-custom mb-4">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0 ps-2 border-start border-4 border-primary">Información General</h5>
</div>
<div class="card-body">
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="row mb-3">
<div class="col-md-4">
<label asp-for="Codigo" class="form-label">Código <span class="text-danger">*</span></label>
<input asp-for="Codigo" class="form-control" placeholder="Ej: SILLA-001" autofocus />
<span asp-validation-for="Codigo" class="text-danger"></span>
</div>
<div class="col-md-8">
<label asp-for="Nombre" class="form-label">Nombre del Artículo <span class="text-danger">*</span></label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Guitarra Acustica Yamaha" />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="3" placeholder="Detalles adicionales, características, color..."></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Marca" class="form-label">Marca</label>
<input asp-for="Marca" class="form-control" placeholder="Ej: Yamaha, Sony" />
<span asp-validation-for="Marca" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Modelo" class="form-label">Modelo</label>
<input asp-for="Modelo" class="form-control" placeholder="Ej: C-40" />
<span asp-validation-for="Modelo" class="text-danger"></span>
</div>
</div>
</div>
</div>
<div class="card-custom">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0 ps-2 border-start border-4 border-info">Detalles del Activo</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="NumeroSerie" class="form-label">Número de Serie</label>
<input asp-for="NumeroSerie" class="form-control" placeholder="S/N..." />
<span asp-validation-for="NumeroSerie" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Precio" class="form-label">Valor Estimado (Moneda)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="Precio" class="form-control" type="number" step="0.01" />
</div>
<span asp-validation-for="Precio" class="text-danger"></span>
</div>
</div>
<div class="mb-3">
<label asp-for="FechaAdquisicion" class="form-label">Fecha de Adquisición</label>
<input asp-for="FechaAdquisicion" type="date" class="form-control" />
<span asp-validation-for="FechaAdquisicion" class="text-danger"></span>
</div>
</div>
</div>
</div>
<!-- Right Column: Classification & Image -->
<div class="col-lg-4">
<div class="card-custom mb-4">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0 ps-2 border-start border-4 border-warning">Clasificación</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label asp-for="TipoControl" class="form-label fw-bold">Tipo de Inventario <span class="text-danger">*</span></label>
<select asp-for="TipoControl" class="form-select" id="tipoControlSelect">
<option value="UNITARIO">Unitario (Laptops, Proyectores)</option>
<option value="LOTE">Por Lote (Sillas, Cables)</option>
</select>
<div class="form-text">Unitario: Control 1 a 1 por Serie. Lote: Control por Cantidad.</div>
<span asp-validation-for="TipoControl" class="text-danger"></span>
</div>
<div class="mb-3" id="cantidadContainer" style="display:none;">
<label asp-for="CantidadInicial" class="form-label fw-bold text-primary">Cantidad Inicial <span class="text-danger">*</span></label>
<input asp-for="CantidadInicial" class="form-control" type="number" min="1" value="1" />
<span asp-validation-for="CantidadInicial" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="CategoriaId" class="form-label">Categoría <span class="text-danger">*</span></label>
<select asp-for="CategoriaId" class="form-select" asp-items="ViewBag.Categorias">
<option value="">-- Seleccionar --</option>
</select>
<span asp-validation-for="CategoriaId" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="UbicacionId" class="form-label">Ubicación Inicial <span class="text-danger">*</span></label>
<select asp-for="UbicacionId" class="form-select" asp-items="ViewBag.Ubicaciones">
<option value="">-- Seleccionar --</option>
</select>
<span asp-validation-for="UbicacionId" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="EstadoId" class="form-label">Estado Físico <span class="text-danger">*</span></label>
<select asp-for="EstadoId" class="form-select" asp-items="ViewBag.Estados">
<option value="">-- Seleccionar --</option>
</select>
<span asp-validation-for="EstadoId" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input asp-for="Activo" class="form-check-input" type="checkbox" role="switch" checked />
<label asp-for="Activo" class="form-check-label">Artículo Activo</label>
</div>
</div>
</div>
</div>
<div class="card-custom">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0 ps-2 border-start border-4 border-secondary">Imagen</h5>
</div>
<div class="card-body text-center">
<div class="mb-3">
<div class="image-preview-container bg-light rounded d-flex align-items-center justify-content-center mb-2" style="height: 200px; border: 2px dashed #ccc;">
<img id="imagePreview" src="#" alt="Vista Previa" style="max-height: 100%; max-width: 100%; display: none;" />
<span id="placeholderText" class="text-muted">Sin Imagen</span>
</div>
<input asp-for="ImagenFile" class="form-control" type="file" accept="image/*" onchange="previewImage(this)" />
<span asp-validation-for="ImagenFile" class="text-danger"></span>
</div>
</div>
</div>
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-primary-custom btn-lg">
<i class="bi bi-save me-2"></i> Guardar Artículo
</button>
<a asp-action="Index" class="btn btn-light border">Cancelar</a>
</div>
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script>
function previewImage(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
$('#imagePreview').attr('src', e.target.result).show();
$('#placeholderText').hide();
}
reader.readAsDataURL(input.files[0]);
} else {
$('#imagePreview').hide();
$('#placeholderText').show();
}
}
$(document).ready(function() {
function toggleFields() {
var tipo = $('#tipoControlSelect').val();
if (tipo === 'LOTE') {
$('#cantidadContainer').show();
$('#NumeroSerie').parent().hide(); // Hide Serial for Lote
$('#NumeroSerie').val('');
} else {
$('#cantidadContainer').hide();
$('#NumeroSerie').parent().show(); // Show Serial for Unitario
$('#CantidadInicial').val(1);
}
}
$('#tipoControlSelect').change(toggleFields);
toggleFields(); // Init
});
</script>
}

View File

@@ -0,0 +1,128 @@
@model Rs_system.Models.ViewModels.ArticuloViewModel
@{
ViewData["Title"] = "Ficha Técnica";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Ficha Técnica</h4>
<p class="text-muted mb-0">Detalles del activo: @Model.Codigo</p>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning text-white me-2">
<i class="bi bi-pencil me-1"></i> Editar
</a>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Volver
</a>
</div>
</div>
<div class="row">
<!-- Left Column: Image and Status -->
<div class="col-md-4 mb-4">
<div class="card-custom text-center mb-3">
<div class="card-body">
@if (!string.IsNullOrEmpty(Model.ImagenUrl))
{
<img src="@Model.ImagenUrl" class="img-fluid rounded mb-3" style="max-height: 300px;" alt="Imagen del Artículo">
}
else
{
<div class="bg-light rounded d-flex align-items-center justify-content-center mx-auto mb-3" style="height: 250px; width: 100%;">
<div class="text-center text-muted">
<i class="bi bi-image fs-1 d-block mb-2"></i>
<span>Sin Imagen</span>
</div>
</div>
}
<h5 class="card-title fw-bold text-primary">@Model.Nombre</h5>
<p class="card-text text-muted mb-3">@Model.Codigo</p>
<div class="d-flex justify-content-center gap-2">
<span class="badge bg-@(Model.EstadoColor ?? "secondary") fs-6 px-3 py-2">
Estado: @Model.EstadoNombre
</span>
@if (Model.Activo)
{
<span class="badge bg-success fs-6 px-3 py-2">Activo</span>
}
else
{
<span class="badge bg-danger fs-6 px-3 py-2">Inactivo</span>
}
</div>
</div>
</div>
</div>
<!-- Right Column: Details -->
<div class="col-md-8">
<div class="card-custom">
<div class="card-header bg-transparent py-3 border-bottom">
<h5 class="card-title mb-0">Información Detallada</h5>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-sm-4 text-muted">Descripción</div>
<div class="col-sm-8 fw-semibold">
@(!string.IsNullOrEmpty(Model.Descripcion) ? Model.Descripcion : "-")
</div>
</div>
<div class="row mb-4">
<div class="col-sm-4 text-muted">Ubicación Actual</div>
<div class="col-sm-8">
<span class="d-flex align-items-center">
<i class="bi bi-geo-alt-fill text-danger me-2"></i>
<span class="fw-bold">@Model.UbicacionNombre</span>
</span>
</div>
</div>
<div class="row mb-4">
<div class="col-sm-4 text-muted">Categoría</div>
<div class="col-sm-8 fw-semibold">@Model.CategoriaNombre</div>
</div>
<hr class="my-4 text-muted" />
<div class="row mb-3">
<div class="col-sm-6">
<small class="text-muted d-block mb-1">Marca</small>
<span class="fs-5">@(!string.IsNullOrEmpty(Model.Marca) ? Model.Marca : "-")</span>
</div>
<div class="col-sm-6">
<small class="text-muted d-block mb-1">Modelo</small>
<span class="fs-5">@(!string.IsNullOrEmpty(Model.Modelo) ? Model.Modelo : "-")</span>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-6">
<small class="text-muted d-block mb-1">Número de Serie</small>
<span class="font-monospace bg-light px-2 py-1 rounded">
@(!string.IsNullOrEmpty(Model.NumeroSerie) ? Model.NumeroSerie : "N/A")
</span>
</div>
<div class="col-sm-6">
<small class="text-muted d-block mb-1">Precio Estimado</small>
<span class="text-success fw-bold">
@Model.Precio.ToString("C")
</span>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<small class="text-muted d-block mb-1">Fecha Adquisición</small>
<span>
@(Model.FechaAdquisicion.HasValue ? Model.FechaAdquisicion.Value.ToString("dd/MM/yyyy") : "-")
</span>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,215 @@
@model Rs_system.Models.ViewModels.ArticuloViewModel
@{
ViewData["Title"] = "Editar Artículo";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Editar Artículo</h4>
<p class="text-muted mb-0">Modificar información del activo</p>
</div>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Volver
</a>
</div>
<form asp-action="Edit" method="post" enctype="multipart/form-data">
<input type="hidden" asp-for="Id" />
<div class="row">
<!-- Left Column: Basic Info -->
<div class="col-lg-8">
<div class="card-custom mb-4">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0 ps-2 border-start border-4 border-primary">Información General</h5>
</div>
<div class="card-body">
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="row mb-3">
<div class="col-md-4">
<label asp-for="Codigo" class="form-label">Código <span class="text-danger">*</span></label>
<input asp-for="Codigo" class="form-control" />
<span asp-validation-for="Codigo" class="text-danger"></span>
</div>
<div class="col-md-8">
<label asp-for="Nombre" class="form-label">Nombre del Artículo <span class="text-danger">*</span></label>
<input asp-for="Nombre" class="form-control" />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="3"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Marca" class="form-label">Marca</label>
<input asp-for="Marca" class="form-control" />
<span asp-validation-for="Marca" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Modelo" class="form-label">Modelo</label>
<input asp-for="Modelo" class="form-control" />
<span asp-validation-for="Modelo" class="text-danger"></span>
</div>
</div>
</div>
</div>
<div class="card-custom">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0 ps-2 border-start border-4 border-info">Detalles del Activo</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="NumeroSerie" class="form-label">Número de Serie</label>
<input asp-for="NumeroSerie" class="form-control" />
<span asp-validation-for="NumeroSerie" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Precio" class="form-label">Valor Estimado (Moneda)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="Precio" class="form-control" type="number" step="0.01" />
</div>
<span asp-validation-for="Precio" class="text-danger"></span>
</div>
</div>
<div class="mb-3">
<label asp-for="FechaAdquisicion" class="form-label">Fecha de Adquisición</label>
<input asp-for="FechaAdquisicion" type="date" class="form-control" />
<span asp-validation-for="FechaAdquisicion" class="text-danger"></span>
</div>
</div>
</div>
</div>
<!-- Right Column: Classification & Image -->
<div class="col-lg-4">
<div class="card-custom mb-4">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0 ps-2 border-start border-4 border-warning">Clasificación</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label fw-bold">Tipo de Inventario</label>
<input type="text" class="form-control" value="@Model.TipoControl" readonly disabled />
<input type="hidden" asp-for="TipoControl" />
</div>
<div class="col-md-6">
@if (Model.TipoControl == "LOTE")
{
<label class="form-label fw-bold text-primary">Cantidad Global</label>
<input type="number" class="form-control" value="@Model.CantidadInicial" readonly disabled />
<div class="form-text">Gestionar cantidad vía Movimientos.</div>
}
else
{
<label class="form-label fw-bold">Cantidad</label>
<input type="text" class="form-control" value="1 (Unitario)" readonly disabled />
}
</div>
</div>
<div class="mb-3">
<label asp-for="CategoriaId" class="form-label">Categoría <span class="text-danger">*</span></label>
<select asp-for="CategoriaId" class="form-select" asp-items="ViewBag.Categorias">
<option value="">-- Seleccionar --</option>
</select>
<span asp-validation-for="CategoriaId" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="UbicacionId" class="form-label">Ubicación Actual <span class="text-danger">*</span></label>
@if (Model.TipoControl == "LOTE")
{
<select asp-for="UbicacionId" class="form-select" asp-items="ViewBag.Ubicaciones" disabled>
<option value="">-- Seleccionar --</option>
</select>
<input type="hidden" asp-for="UbicacionId" />
<div class="form-text">La ubicación se gestiona por Existencias en Lotes.</div>
}
else
{
<select asp-for="UbicacionId" class="form-select" asp-items="ViewBag.Ubicaciones">
<option value="">-- Seleccionar --</option>
</select>
<span asp-validation-for="UbicacionId" class="text-danger"></span>
}
</div>
<div class="mb-3">
<label asp-for="EstadoId" class="form-label">Estado Físico <span class="text-danger">*</span></label>
<select asp-for="EstadoId" class="form-select" asp-items="ViewBag.Estados">
<option value="">-- Seleccionar --</option>
</select>
<span asp-validation-for="EstadoId" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input asp-for="Activo" class="form-check-input" type="checkbox" role="switch" />
<label asp-for="Activo" class="form-check-label">Artículo Activo</label>
</div>
</div>
</div>
</div>
<div class="card-custom">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0 ps-2 border-start border-4 border-secondary">Imagen</h5>
</div>
<div class="card-body text-center">
<div class="mb-3">
<div class="image-preview-container bg-light rounded d-flex align-items-center justify-content-center mb-2" style="height: 200px; border: 2px dashed #ccc;">
@if (!string.IsNullOrEmpty(Model.ImagenUrl))
{
<img id="imagePreview" src="@Model.ImagenUrl" alt="Vista Previa" style="max-height: 100%; max-width: 100%;" />
<span id="placeholderText" class="text-muted" style="display: none;">Sin Imagen</span>
}
else
{
<img id="imagePreview" src="#" alt="Vista Previa" style="max-height: 100%; max-width: 100%; display: none;" />
<span id="placeholderText" class="text-muted">Sin Imagen</span>
}
</div>
<input asp-for="ImagenFile" class="form-control" type="file" accept="image/*" onchange="previewImage(this)" />
<span asp-validation-for="ImagenFile" class="text-danger"></span>
</div>
</div>
</div>
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-primary-custom btn-lg">
<i class="bi bi-save me-2"></i> Guardar Cambios
</button>
<a asp-action="Index" class="btn btn-light border">Cancelar</a>
</div>
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script>
function previewImage(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
$('#imagePreview').attr('src', e.target.result).show();
$('#placeholderText').hide();
}
reader.readAsDataURL(input.files[0]);
} else {
// If clearing, we might want to keep the old image or show placebo
// For simplicity here, if user cancels file dialog, it keeps previous
}
}
</script>
}

View File

@@ -0,0 +1,141 @@
@model IEnumerable<Rs_system.Models.ViewModels.ArticuloViewModel>
@{
ViewData["Title"] = "Inventario de Artículos";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Inventario de Artículos</h4>
<p class="text-muted mb-0">Gestión de bienes y activos fijos</p>
</div>
<a asp-action="Create" class="btn btn-primary-custom">
<i class="bi bi-plus-lg me-1"></i> Nuevo Artículo
</a>
</div>
<!-- Filters Card -->
<div class="card-custom mb-4">
<div class="card-body py-3">
<form asp-action="Index" method="get" class="row g-3">
<div class="col-md-3">
<div class="input-group">
<span class="input-group-text bg-light border-end-0"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-start-0" placeholder="Buscar por nombre, código..." value="@ViewBag.CurrentSearch">
</div>
</div>
<div class="col-md-3">
<select name="categoriaId" class="form-select" asp-items="ViewBag.Categorias">
<option value="">Todas las Categorías</option>
</select>
</div>
<div class="col-md-3">
<select name="ubicacionId" class="form-select" asp-items="ViewBag.Ubicaciones">
<option value="">Todas las Ubicaciones</option>
</select>
</div>
<div class="col-md-2">
<select name="estadoId" class="form-select" asp-items="ViewBag.Estados">
<option value="">Todos los Estados</option>
</select>
</div>
<div class="col-md-1 d-grid">
<button type="submit" class="btn btn-secondary" title="Filtrar">
<i class="bi bi-funnel"></i>
</button>
</div>
</form>
</div>
</div>
<div class="card-custom">
<div class="table-responsive">
<table class="table-custom align-middle">
<thead>
<tr>
<th style="width: 60px;">Img</th>
<th>Código</th>
<th>Nombre / Marca</th>
<th>Ubicación</th>
<th>Categoría</th>
<th class="text-center">Estado</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-box-seam fs-1 d-block mb-2"></i>
No se encontraron artículos
</td>
</tr>
}
else
{
@foreach (var item in Model)
{
<tr>
<td class="text-center">
@if (!string.IsNullOrEmpty(item.ImagenUrl))
{
<img src="@item.ImagenUrl" alt="Img" class="rounded" style="width: 40px; height: 40px; object-fit: cover;" />
}
else
{
<div class="rounded bg-light d-flex align-items-center justify-content-center text-muted" style="width: 40px; height: 40px;">
<i class="bi bi-image"></i>
</div>
}
</td>
<td>
<span class="badge bg-light text-dark border">@item.Codigo</span>
</td>
<td>
<div class="fw-bold">@item.Nombre</div>
@if (!string.IsNullOrEmpty(item.Marca))
{
<small class="text-muted">@item.Marca @item.Modelo</small>
}
</td>
<td>
<i class="bi bi-geo-alt-fill text-muted me-1" style="font-size: 0.8rem;"></i>
@item.UbicacionNombre
</td>
<td>@item.CategoriaNombre</td>
<td class="text-center">
<span class="badge bg-@(item.EstadoColor ?? "secondary")">
@item.EstadoNombre
</span>
</td>
<td class="text-center">
<div class="btn-group">
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-primary" title="Ver Ficha Técnica">
<i class="bi bi-eye"></i>
</a>
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary" title="Editar">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
@section Scripts {
<script>
@if (TempData["SuccessMessage"] != null)
{
<text>toastr.success('@TempData["SuccessMessage"]');</text>
}
@if (TempData["ErrorMessage"] != null)
{
<text>toastr.error('@TempData["ErrorMessage"]');</text>
}
</script>
}

View File

@@ -0,0 +1,53 @@
@model Rs_system.Models.Categoria
@{
ViewData["Title"] = "Nueva Categoría";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Nueva Categoría</h4>
<p class="text-muted mb-0">Registrar una nueva categoría de inventario</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="card-custom" style="max-width: 800px; margin: 0 auto;">
<div class="card-body">
<form asp-action="Create" method="post">
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="mb-3">
<label asp-for="Nombre" class="form-label">Nombre <span class="text-danger">*</span></label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Electrónica, Muebles, Papelería" autofocus />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="3" placeholder="Breve descripción de la categoría (opcional)"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input asp-for="Activo" class="form-check-input" type="checkbox" role="switch" checked />
<label asp-for="Activo" class="form-check-label">Categoría Activa</label>
</div>
<div class="form-text">Si está inactiva, no aparecerá en las selecciones de nuevos artículos.</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Guardar
</button>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,54 @@
@model Rs_system.Models.Categoria
@{
ViewData["Title"] = "Editar Categoría";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Editar Categoría</h4>
<p class="text-muted mb-0">Modificar información de la categoría</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="card-custom" style="max-width: 800px; margin: 0 auto;">
<div class="card-body">
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="Id" />
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="mb-3">
<label asp-for="Nombre" class="form-label">Nombre <span class="text-danger">*</span></label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Electrónica, Muebles, Papelería" />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="3" placeholder="Breve descripción de la categoría (opcional)"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input asp-for="Activo" class="form-check-input" type="checkbox" role="switch" />
<label asp-for="Activo" class="form-check-label">Categoría Activa</label>
</div>
<div class="form-text">Si está inactiva, no aparecerá en las selecciones de nuevos artículos.</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Guardar Cambios
</button>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,117 @@
@model IEnumerable<Rs_system.Models.Categoria>
@{
ViewData["Title"] = "Categorías";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Categorías de Inventario</h4>
<p class="text-muted mb-0">Gestión de categorías para clasificación de artículos</p>
</div>
<a asp-action="Create" class="btn btn-primary-custom">
<i class="bi bi-plus-lg me-1"></i> Nueva Categoría
</a>
</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">Estado</th>
<th class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
@if (!Model.Any())
{
<tr>
<td colspan="4" class="text-center text-muted py-4">
<i class="bi bi-tags fs-1 d-block mb-2"></i>
No hay categorías registradas
</td>
</tr>
}
@foreach (var item in Model)
{
<tr>
<td>
<strong>@item.Nombre</strong>
</td>
<td>
@if (!string.IsNullOrEmpty(item.Descripcion))
{
<span>@item.Descripcion</span>
}
else
{
<span class="text-muted">-</span>
}
</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">
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary" title="Editar">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="confirmDelete(@item.Id)" title="Eliminar">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Delete Form -->
<form id="deleteForm" asp-action="Delete" method="post" style="display: none;">
<input type="hidden" name="id" id="deleteId" />
</form>
@section Scripts {
<script>
function confirmDelete(id) {
Swal.fire({
title: '¿Eliminar categoría?',
text: 'Esta acción moverá la categoría a la papelera.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Sí, eliminar',
cancelButtonText: 'Cancelar'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
});
}
// Show success/error messages
@if (TempData["SuccessMessage"] != null)
{
<text>
toastr.success('@TempData["SuccessMessage"]');
</text>
}
@if (TempData["ErrorMessage"] != null)
{
<text>
toastr.error('@TempData["ErrorMessage"]');
</text>
}
</script>
}

View File

@@ -0,0 +1,453 @@
@model Rs_system.Models.ViewModels.RegistrarColaboracionViewModel
@{
ViewData["Title"] = "Nueva Colaboración";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Nueva Colaboración</h4>
<p class="text-muted mb-0">Registrar colaboración económica mensual</p>
</div>
<div>
<a asp-controller="TipoColaboracion" asp-action="Index" class="btn btn-outline-primary me-2">
<i class="bi bi-gear me-1"></i> Gestionar Tipos
</a>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Volver
</a>
</div>
</div>
<div class="card-custom">
<form asp-action="Create" method="post" id="colaboracionForm">
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<!-- Selección de Miembro con Búsqueda -->
<div class="mb-4">
<h6 class="text-primary border-bottom pb-2">Información del Miembro</h6>
<div class="row">
<div class="col-md-12">
<label for="buscarMiembro" class="form-label">Buscar Miembro</label>
<input type="text"
id="buscarMiembro"
class="form-control"
placeholder="Escriba el nombre del miembro..."
autocomplete="off" />
<div id="resultadosBusqueda" class="list-group mt-2" style="display: none; position: absolute; z-index: 1000; max-height: 300px; overflow-y: auto; width: 90%;"></div>
<input type="hidden" asp-for="MiembroId" id="miembroIdHidden" />
<!-- Mostrar miembro seleccionado -->
<div id="miembroSeleccionado" class="mt-3" style="display: none;">
<div class="alert alert-success d-flex justify-content-between align-items-center mb-2">
<div>
<i class="bi bi-person-check me-2"></i>
<strong id="nombreMiembroSeleccionado"></strong>
</div>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="limpiarMiembro()">
<i class="bi bi-x"></i> Cambiar
</button>
</div>
<!-- Historial de Últimos Pagos -->
<div id="infoUltimosPagos" style="display: none;">
<div class="card border-info bg-light">
<div class="card-header bg-transparent border-info py-1 px-3">
<small class="text-info fw-bold"><i class="bi bi-clock-history me-1"></i> Últimos meses pagados</small>
</div>
<div class="card-body py-2 px-3">
<div id="listaUltimosPagos" class="d-flex flex-wrap gap-2"></div>
</div>
</div>
</div>
</div>
<span asp-validation-for="MiembroId" class="text-danger"></span>
</div>
</div>
</div>
<!-- Período de Colaboración -->
<div class="mb-4">
<h6 class="text-primary border-bottom pb-2">Período a Cubrir</h6>
<div class="row">
<div class="col-md-3">
<label asp-for="MesInicial" class="form-label"></label>
<select asp-for="MesInicial" class="form-select" id="mesInicial" onchange="calcularSugerido()">
<option value="1">Enero</option>
<option value="2">Febrero</option>
<option value="3">Marzo</option>
<option value="4">Abril</option>
<option value="5">Mayo</option>
<option value="6">Junio</option>
<option value="7">Julio</option>
<option value="8">Agosto</option>
<option value="9">Septiembre</option>
<option value="10">Octubre</option>
<option value="11">Noviembre</option>
<option value="12">Diciembre</option>
</select>
<span asp-validation-for="MesInicial" class="text-danger"></span>
</div>
<div class="col-md-3">
<label asp-for="AnioInicial" class="form-label"></label>
<select asp-for="AnioInicial" class="form-select" id="anioInicial" onchange="calcularSugerido()">
@for (int i = DateTime.Now.Year; i >= DateTime.Now.Year - 5; i--)
{
<option value="@i">@i</option>
}
</select>
<span asp-validation-for="AnioInicial" class="text-danger"></span>
</div>
<div class="col-md-3">
<label asp-for="MesFinal" class="form-label"></label>
<select asp-for="MesFinal" class="form-select" id="mesFinal" onchange="calcularSugerido()">
<option value="1">Enero</option>
<option value="2">Febrero</option>
<option value="3">Marzo</option>
<option value="4">Abril</option>
<option value="5">Mayo</option>
<option value="6">Junio</option>
<option value="7">Julio</option>
<option value="8">Agosto</option>
<option value="9">Septiembre</option>
<option value="10">Octubre</option>
<option value="11">Noviembre</option>
<option value="12">Diciembre</option>
</select>
<span asp-validation-for="MesFinal" class="text-danger"></span>
</div>
<div class="col-md-3">
<label asp-for="AnioFinal" class="form-label"></label>
<select asp-for="AnioFinal" class="form-select" id="anioFinal" onchange="calcularSugerido()">
@for (int i = DateTime.Now.Year + 1; i >= DateTime.Now.Year - 5; i--)
{
<option value="@i" selected="@(i == DateTime.Now.Year)">@i</option>
}
</select>
<span asp-validation-for="AnioFinal" class="text-danger"></span>
</div>
</div>
</div>
<!-- Tipos de Colaboración -->
<div class="mb-4">
<h6 class="text-primary border-bottom pb-2">Tipos de Colaboración</h6>
<div class="row">
@if (Model.TiposDisponibles != null && Model.TiposDisponibles.Any())
{
@foreach (var tipo in Model.TiposDisponibles)
{
var esTransporteOLimpieza = tipo.Nombre.Equals("Transporte", StringComparison.OrdinalIgnoreCase) ||
tipo.Nombre.Equals("Limpieza", StringComparison.OrdinalIgnoreCase);
<div class="col-md-6 mb-2">
<div class="form-check">
<input class="form-check-input tipo-checkbox"
type="checkbox"
name="TiposSeleccionados"
value="@tipo.Id"
id="tipo_@tipo.Id"
data-monto="@tipo.MontoSugerido"
@(esTransporteOLimpieza ? "checked" : "")
onchange="calcularSugerido()">
<label class="form-check-label" for="tipo_@tipo.Id">
<strong>@tipo.Nombre</strong>
<span class="badge bg-success ms-2">$@tipo.MontoSugerido.ToString("N2")/mes</span>
@if (!string.IsNullOrEmpty(tipo.Descripcion))
{
<br><small class="text-muted">@tipo.Descripcion</small>
}
</label>
</div>
</div>
}
}
else
{
<div class="col-12">
<div class="alert alert-warning">
No hay tipos de colaboración activos. <a asp-controller="TipoColaboracion" asp-action="Index">Gestionar tipos</a>
</div>
</div>
}
</div>
<span asp-validation-for="TiposSeleccionados" class="text-danger"></span>
</div>
<!-- Tipo Prioritario -->
<div class="mb-4">
<h6 class="text-primary border-bottom pb-2">Priorización (Opcional)</h6>
<div class="row">
<div class="col-md-12">
<label asp-for="TipoPrioritario" class="form-label"></label>
<select asp-for="TipoPrioritario" class="form-select" id="tipoPrioritario">
<option value="">Sin prioridad - Distribuir equitativamente</option>
@if (Model.TiposDisponibles != null)
{
@foreach (var tipo in Model.TiposDisponibles)
{
<option value="@tipo.Id">@tipo.Nombre (se pagará primero $@tipo.MontoSugerido por mes)</option>
}
}
</select>
<small class="form-text text-muted">
Si selecciona un tipo prioritario, el monto se asignará primero a ese tipo usando su monto sugerido,
y el resto se distribuirá equitativamente entre los demás tipos.
</small>
</div>
</div>
</div>
<!-- Monto Total -->
<div class="mb-4">
<h6 class="text-primary border-bottom pb-2">Monto de la Colaboración</h6>
<div class="row">
<div class="col-md-6">
<label asp-for="MontoTotal" class="form-label"></label>
<div class="input-group input-group-lg">
<span class="input-group-text">$</span>
<input asp-for="MontoTotal"
type="number"
step="0.01"
class="form-control"
id="montoTotal"
placeholder="0.00"
required />
</div>
<span asp-validation-for="MontoTotal" class="text-danger"></span>
<small class="form-text text-muted">Monto total que entrega el miembro</small>
</div>
<div class="col-md-6">
<label class="form-label">Resumen</label>
<div class="card bg-light">
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span>Meses a cubrir:</span>
<strong id="totalMeses">0</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Tipos seleccionados:</span>
<strong id="totalTipos">0</strong>
</div>
<hr class="my-2">
<div class="d-flex justify-content-between mb-2">
<span class="text-info">Monto sugerido:</span>
<strong class="text-info">$<span id="montoSugerido">0.00</span></strong>
</div>
<div id="alertaDiferencia" class="alert alert-warning py-1 px-2 mb-0 mt-2" style="display: none; font-size: 0.85rem;">
<i class="bi bi-exclamation-triangle me-1"></i>
<span id="mensajeDiferencia"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Observaciones -->
<div class="mb-4">
<label asp-for="Observaciones" class="form-label"></label>
<textarea asp-for="Observaciones" class="form-control" rows="3" placeholder="Notas opcionales sobre esta colaboración"></textarea>
<span asp-validation-for="Observaciones" class="text-danger"></span>
</div>
<div class="d-flex justify-content-end gap-2">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Registrar Colaboración
</button>
</div>
</form>
</div>
@section Scripts {
<script>
let timeoutBusqueda = null;
// Búsqueda de miembros
document.getElementById('buscarMiembro').addEventListener('input', function(e) {
const termino = e.target.value;
const resultadosDiv = document.getElementById('resultadosBusqueda');
clearTimeout(timeoutBusqueda);
if (termino.length < 2) {
resultadosDiv.style.display = 'none';
return;
}
timeoutBusqueda = setTimeout(async () => {
try {
const response = await fetch('@Url.Action("BuscarMiembros", "Colaboracion")?termino=' + encodeURIComponent(termino));
const miembros = await response.json();
if (miembros.length === 0) {
resultadosDiv.innerHTML = '<div class="list-group-item text-muted">No se encontraron resultados</div>';
resultadosDiv.style.display = 'block';
return;
}
let html = '';
miembros.forEach(miembro => {
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">
<div>
<i class="bi bi-person me-2"></i>
<strong>${miembro.text}</strong>
</div>
${miembro.telefono ? '<small class="text-muted">' + miembro.telefono + '</small>' : ''}
</div>
</button>
`;
});
resultadosDiv.innerHTML = html;
resultadosDiv.style.display = 'block';
} catch (error) {
console.error('Error al buscar miembros:', error);
}
}, 300);
});
// Cerrar resultados cuando se hace clic fuera
document.addEventListener('click', function(e) {
const buscarInput = document.getElementById('buscarMiembro');
const resultadosDiv = document.getElementById('resultadosBusqueda');
if (!buscarInput.contains(e.target) && !resultadosDiv.contains(e.target)) {
resultadosDiv.style.display = 'none';
}
});
function seleccionarMiembro(id, nombre) {
document.getElementById('miembroIdHidden').value = id;
document.getElementById('nombreMiembroSeleccionado').textContent = nombre;
document.getElementById('miembroSeleccionado').style.display = 'block';
document.getElementById('buscarMiembro').value = '';
document.getElementById('buscarMiembro').style.display = 'none';
document.getElementById('resultadosBusqueda').style.display = 'none';
// Cargar historial de pagos
cargarHistorialPagos(id);
}
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 = 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>
}

View File

@@ -0,0 +1,130 @@
@model Rs_system.Models.Colaboracion
@{
ViewData["Title"] = "Detalle de Colaboración";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Detalle de Colaboración #@Model.Id</h4>
<p class="text-muted mb-0">Información completa de la colaboración</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">
<!-- Información Principal -->
<div class="col-md-6 mb-4">
<div class="card-custom">
<h6 class="text-primary border-bottom pb-2 mb-3">Información de Registro</h6>
<div class="mb-3">
<label class="text-muted small">Miembro</label>
<div><strong>@Model.Miembro.Persona.Nombres @Model.Miembro.Persona.Apellidos</strong></div>
</div>
<div class="mb-3">
<label class="text-muted small">Fecha de Registro</label>
<div>@Model.FechaRegistro.ToString("dd/MM/yyyy HH:mm")</div>
</div>
<div class="mb-3">
<label class="text-muted small">Registrado por</label>
<div>@(Model.RegistradoPor ?? "Sistema")</div>
</div>
<div class="mb-3">
<label class="text-muted small">Monto Total</label>
<div><h4 class="text-success mb-0">$@Model.MontoTotal.ToString("N2")</h4></div>
</div>
@if (!string.IsNullOrEmpty(Model.Observaciones))
{
<div class="mb-3">
<label class="text-muted small">Observaciones</label>
<div class="alert alert-info mb-0">@Model.Observaciones</div>
</div>
}
</div>
</div>
<!-- Desglose Detallado -->
<div class="col-md-6 mb-4">
<div class="card-custom">
<h6 class="text-primary border-bottom pb-2 mb-3">Desglose por Tipo</h6>
@{
var porTipo = Model.Detalles.GroupBy(d => d.TipoColaboracion.Nombre);
}
@foreach (var grupo in porTipo)
{
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">@grupo.Key</h6>
<span class="badge bg-primary">@grupo.Count() meses</span>
</div>
<div class="text-muted small">
Total: <strong class="text-success">$@grupo.Sum(d => d.Monto).ToString("N2")</strong>
</div>
</div>
}
</div>
</div>
</div>
<!-- Tabla de Detalles Mensuales -->
<div class="card-custom">
<h6 class="text-primary border-bottom pb-2 mb-3">Meses Cubiertos</h6>
<div class="table-responsive">
<table class="table-custom">
<thead>
<tr>
<th>Mes</th>
<th>Año</th>
<th>Tipo</th>
<th class="text-end">Monto</th>
</tr>
</thead>
<tbody>
@foreach (var detalle in Model.Detalles.OrderBy(d => d.Anio).ThenBy(d => d.Mes))
{
<tr>
<td>
@{
var fecha = new DateTime(detalle.Anio, detalle.Mes, 1);
}
@fecha.ToString("MMMM", new System.Globalization.CultureInfo("es-ES"))
</td>
<td>@detalle.Anio</td>
<td>
<span class="badge bg-primary">@detalle.TipoColaboracion.Nombre</span>
</td>
<td class="text-end">
<strong>$@detalle.Monto.ToString("N2")</strong>
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="table-active">
<td colspan="3" class="text-end"><strong>TOTAL:</strong></td>
<td class="text-end">
<h5 class="mb-0 text-success">$@Model.Detalles.Sum(d => d.Monto).ToString("N2")</h5>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<a asp-action="Index" class="btn btn-secondary">
<i class="bi bi-arrow-left me-1"></i> Volver al Listado
</a>
<a asp-action="EstadoCuenta" asp-route-id="@Model.MiembroId" class="btn btn-primary-custom">
<i class="bi bi-file-text me-1"></i> Ver Estado de Cuenta del Miembro
</a>
</div>

View File

@@ -0,0 +1,133 @@
@model Rs_system.Models.ViewModels.EstadoCuentaViewModel
@{
ViewData["Title"] = "Estado de Cuenta";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Estado de Cuenta</h4>
<p class="text-muted mb-0">@Model.NombreMiembro</p>
</div>
<div>
<button onclick="window.print()" class="btn btn-outline-secondary me-2">
<i class="bi bi-printer me-1"></i> Imprimir
</button>
<a asp-action="Index" class="btn btn-primary-custom">
<i class="bi bi-arrow-left me-1"></i> Volver
</a>
</div>
</div>
<!-- Encabezado del Estado de Cuenta -->
<div class="card-custom mb-4" style="border: 2px solid #198754;">
<div class="text-center py-4">
<h3 class="mb-3">ESTADO DE CUENTA DE COLABORACIONES</h3>
<h5 class="text-primary mb-2">@Model.NombreMiembro</h5>
<p class="text-muted mb-0">Fecha de consulta: @Model.FechaConsulta.ToString("dd/MM/yyyy HH:mm")</p>
</div>
<hr>
<div class="text-center py-3">
<h6 class="text-muted mb-2">Total Aportado</h6>
<h2 class="text-success mb-0">$@Model.TotalAportado.ToString("N2")</h2>
</div>
</div>
<!-- Historial por Tipo de Colaboración -->
@if (Model.HistorialPorTipos.Any())
{
@foreach (var historial in Model.HistorialPorTipos)
{
<div class="card-custom mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
<span class="badge bg-primary">@historial.TipoNombre</span>
</h5>
<h6 class="mb-0 text-success">
Total: $@historial.TotalTipo.ToString("N2")
</h6>
</div>
<div class="table-responsive">
<table class="table-custom">
<thead>
<tr>
<th>Mes / Año</th>
<th>Fecha de Pago</th>
<th class="text-end">Monto</th>
</tr>
</thead>
<tbody>
@foreach (var registro in historial.Registros.OrderByDescending(r => r.Anio).ThenByDescending(r => r.Mes))
{
<tr>
<td><strong>@registro.MesTexto</strong></td>
<td>@registro.FechaRegistro.ToString("dd/MM/yyyy")</td>
<td class="text-end">
<strong>$@registro.Monto.ToString("N2")</strong>
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="table-active">
<td colspan="2" class="text-end"><strong>Subtotal @historial.TipoNombre:</strong></td>
<td class="text-end">
<strong class="text-success">$@historial.TotalTipo.ToString("N2")</strong>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
}
<!-- Total General -->
<div class="card-custom" style="border: 2px solid #198754;">
<div class="d-flex justify-content-between align-items-center py-3">
<h4 class="mb-0">TOTAL GENERAL</h4>
<h3 class="mb-0 text-success">$@Model.TotalAportado.ToString("N2")</h3>
</div>
</div>
}
else
{
<div class="card-custom">
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle me-2"></i>
Este miembro aún no tiene colaboraciones registradas.
</div>
</div>
}
<!-- Pie de página para impresión -->
<div class="mt-5 text-center text-muted" style="page-break-before: auto;">
<hr>
<p class="mb-1"><small>Este documento es un comprobante de colaboraciones realizadas</small></p>
<p class="mb-0"><small>Generado el @DateTime.Now.ToString("dd/MM/yyyy HH:mm")</small></p>
</div>
<style>
@@media print {
.btn, .no-print {
display: none !important;
}
.card-custom {
border: 1px solid #dee2e6;
page-break-inside: avoid;
}
body {
background: white;
}
}
</style>
@section Scripts {
<script>
@if (TempData["Error"] != null)
{
<text>
toastr.error('@TempData["Error"]');
</text>
}
</script>
}

View File

@@ -0,0 +1,147 @@
@model IEnumerable<Rs_system.Models.Colaboracion>
@{
ViewData["Title"] = "Colaboraciones";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Colaboraciones Económicas</h4>
<p class="text-muted mb-0">Registro de colaboraciones mensuales de los miembros</p>
</div>
<div>
<a asp-action="Reportes" class="btn btn-outline-primary me-2">
<i class="bi bi-file-earmark-bar-graph me-1"></i> Reportes
</a>
<a asp-action="Create" class="btn btn-primary-custom">
<i class="bi bi-plus-lg me-1"></i> Nueva Colaboración
</a>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card-custom text-center">
<h6 class="text-muted mb-2">Total Recaudado Hoy</h6>
<h3 class="text-primary mb-0">
$@Model.Where(c => c.FechaRegistro.Date == DateTime.Today).Sum(c => c.MontoTotal).ToString("N2")
</h3>
</div>
</div>
<div class="col-md-4">
<div class="card-custom text-center">
<h6 class="text-muted mb-2">Colaboraciones Hoy</h6>
<h3 class="text-success mb-0">
@Model.Count(c => c.FechaRegistro.Date == DateTime.Today)
</h3>
</div>
</div>
<div class="col-md-4">
<div class="card-custom text-center">
<h6 class="text-muted mb-2">Total Registros</h6>
<h3 class="text-info mb-0">@Model.Count()</h3>
</div>
</div>
</div>
<!-- Collaborations Table -->
<div class="card-custom">
<div class="table-responsive">
<table class="table-custom">
<thead>
<tr>
<th>Fecha</th>
<th>Miembro</th>
<th>Tipos</th>
<th>Período</th>
<th>Monto</th>
<th>Registrado por</th>
<th class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
@if (!Model.Any())
{
<tr>
<td colspan="7" class="text-center text-muted py-4">
<i class="bi bi-cash-coin fs-1 d-block mb-2"></i>
No hay colaboraciones registradas
</td>
</tr>
}
@foreach (var colaboracion in Model)
{
<tr>
<td>
<strong>@colaboracion.FechaRegistro.ToString("dd/MM/yyyy")</strong><br>
<small class="text-muted">@colaboracion.FechaRegistro.ToString("HH:mm")</small>
</td>
<td>
<strong>@colaboracion.Miembro.Persona.Nombres @colaboracion.Miembro.Persona.Apellidos</strong>
</td>
<td>
@{
var tipos = colaboracion.Detalles.Select(d => d.TipoColaboracion.Nombre).Distinct();
}
@foreach (var tipo in tipos)
{
<span class="badge bg-primary me-1">@tipo</span>
}
</td>
<td>
@{
var ordenados = colaboracion.Detalles.OrderBy(d => d.Anio).ThenBy(d => d.Mes).ToList();
var primero = ordenados.First();
var ultimo = ordenados.Last();
if (primero.Anio == ultimo.Anio && primero.Mes == ultimo.Mes)
{
var fecha = new DateTime(primero.Anio, primero.Mes, 1);
<text>@fecha.ToString("MMMM yyyy", new System.Globalization.CultureInfo("es-ES"))</text>
}
else
{
var fechaInicio = new DateTime(primero.Anio, primero.Mes, 1);
var fechaFin = new DateTime(ultimo.Anio, ultimo.Mes, 1);
<text>@fechaInicio.ToString("MMM yyyy", new System.Globalization.CultureInfo("es-ES")) - @fechaFin.ToString("MMM yyyy", new System.Globalization.CultureInfo("es-ES"))</text>
}
}
</td>
<td>
<strong class="text-success">$@colaboracion.MontoTotal.ToString("N2")</strong>
</td>
<td>
<small>@(colaboracion.RegistradoPor ?? "Sistema")</small>
</td>
<td class="text-center">
<a asp-action="Details" asp-route-id="@colaboracion.Id" class="btn btn-sm btn-outline-primary" title="Ver detalles">
<i class="bi bi-eye"></i>
</a>
<a asp-action="EstadoCuenta" asp-route-id="@colaboracion.MiembroId" class="btn btn-sm btn-outline-info" title="Estado de cuenta">
<i class="bi bi-file-text"></i>
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@section Scripts {
<script>
// Show success/error messages
@if (TempData["Success"] != null)
{
<text>
toastr.success('@TempData["Success"]');
</text>
}
@if (TempData["Error"] != null)
{
<text>
toastr.error('@TempData["Error"]');
</text>
}
</script>
}

View File

@@ -0,0 +1,150 @@
@model Rs_system.Models.ViewModels.ReporteColaboracionesViewModel
@{
ViewData["Title"] = "Reporte de Colaboraciones";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Reporte de Colaboraciones</h4>
<p class="text-muted mb-0">
Del @Model.FechaInicio.ToString("dd/MM/yyyy") al @Model.FechaFin.ToString("dd/MM/yyyy")
</p>
</div>
<div>
<button onclick="window.print()" class="btn btn-outline-secondary me-2">
<i class="bi bi-printer me-1"></i> Imprimir
</button>
<a asp-action="Reportes" class="btn btn-primary-custom">
<i class="bi bi-arrow-left me-1"></i> Nuevo Reporte
</a>
</div>
</div>
<!-- Resumen General -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card-custom text-center">
<h6 class="text-muted mb-2">Total Recaudado</h6>
<h2 class="text-success mb-0">$@Model.TotalRecaudado.ToString("N2")</h2>
</div>
</div>
<div class="col-md-4">
<div class="card-custom text-center">
<h6 class="text-muted mb-2">Total Movimientos</h6>
<h2 class="text-primary mb-0">@Model.Movimientos.Count</h2>
</div>
</div>
<div class="col-md-4">
<div class="card-custom text-center">
<h6 class="text-muted mb-2">Tipos de Colaboración</h6>
<h2 class="text-info mb-0">@Model.DesglosePorTipos.Count</h2>
</div>
</div>
</div>
<!-- Desglose por Tipo -->
<div class="card-custom mb-4">
<h6 class="text-primary border-bottom pb-2 mb-3">Desglose por Tipo de Colaboración</h6>
@if (Model.DesglosePorTipos.Any())
{
<div class="table-responsive">
<table class="table-custom">
<thead>
<tr>
<th>Tipo de Colaboración</th>
<th class="text-center">Cantidad de Meses</th>
<th class="text-end">Total Recaudado</th>
</tr>
</thead>
<tbody>
@foreach (var tipo in Model.DesglosePorTipos.OrderByDescending(t => t.TotalRecaudado))
{
<tr>
<td>
<span class="badge bg-primary">@tipo.TipoNombre</span>
</td>
<td class="text-center">@tipo.CantidadMeses</td>
<td class="text-end">
<strong class="text-success">$@tipo.TotalRecaudado.ToString("N2")</strong>
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="table-active">
<td colspan="2" class="text-end"><strong>TOTAL:</strong></td>
<td class="text-end">
<h5 class="mb-0 text-success">$@Model.TotalRecaudado.ToString("N2")</h5>
</td>
</tr>
</tfoot>
</table>
</div>
}
else
{
<div class="alert alert-info">
No hay datos para el período seleccionado.
</div>
}
</div>
<!-- Detalle de Movimientos -->
<div class="card-custom">
<h6 class="text-primary border-bottom pb-2 mb-3">Detalle de Movimientos</h6>
@if (Model.Movimientos.Any())
{
<div class="table-responsive">
<table class="table-custom">
<thead>
<tr>
<th>Fecha</th>
<th>Miembro</th>
<th>Tipos</th>
<th>Período Cubierto</th>
<th class="text-end">Monto</th>
</tr>
</thead>
<tbody>
@foreach (var movimiento in Model.Movimientos.OrderByDescending(m => m.Fecha))
{
<tr>
<td>@movimiento.Fecha.ToString("dd/MM/yyyy HH:mm")</td>
<td><strong>@movimiento.NombreMiembro</strong></td>
<td>
@foreach (var tipo in movimiento.TiposColaboracion.Split(", "))
{
<span class="badge bg-primary me-1">@tipo</span>
}
</td>
<td>@movimiento.PeriodoCubierto</td>
<td class="text-end">
<strong>$@movimiento.Monto.ToString("N2")</strong>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="alert alert-info">
No hay movimientos registrados en este período.
</div>
}
</div>
<style>
@@media print {
.btn, .no-print {
display: none !important;
}
.card-custom {
border: 1px solid #dee2e6;
page-break-inside: avoid;
}
}
</style>

View File

@@ -0,0 +1,118 @@
@{
ViewData["Title"] = "Reportes de Colaboraciones";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Reportes de Colaboraciones</h4>
<p class="text-muted mb-0">Generar reportes por rango de fechas</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="card-custom">
<form asp-action="GenerarReporte" method="post">
<div class="row mb-4">
<div class="col-md-4">
<label for="fechaInicio" class="form-label">Fecha Inicio</label>
<input type="date"
class="form-control"
id="fechaInicio"
name="fechaInicio"
value="@ViewBag.FechaInicio?.ToString("yyyy-MM-dd")"
required />
</div>
<div class="col-md-4">
<label for="fechaFin" class="form-label">Fecha Fin</label>
<input type="date"
class="form-control"
id="fechaFin"
name="fechaFin"
value="@ViewBag.FechaFin?.ToString("yyyy-MM-dd")"
required />
</div>
<div class="col-md-4">
<label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-primary-custom w-100">
<i class="bi bi-search me-1"></i> Generar Reporte
</button>
</div>
</div>
</form>
<!-- Acceso rápido -->
<div class="row">
<div class="col-12">
<h6 class="text-muted mb-3">Accesos Rápidos</h6>
</div>
<div class="col-md-3 mb-2">
<button class="btn btn-outline-primary w-100" onclick="reporteHoy()">
<i class="bi bi-calendar-day me-1"></i> Hoy
</button>
</div>
<div class="col-md-3 mb-2">
<button class="btn btn-outline-primary w-100" onclick="reporteSemana()">
<i class="bi bi-calendar-week me-1"></i> Esta Semana
</button>
</div>
<div class="col-md-3 mb-2">
<button class="btn btn-outline-primary w-100" onclick="reporteMes()">
<i class="bi bi-calendar-month me-1"></i> Este Mes
</button>
</div>
<div class="col-md-3 mb-2">
<button class="btn btn-outline-primary w-100" onclick="reporteAnio()">
<i class="bi bi-calendar3 me-1"></i> Este Año
</button>
</div>
</div>
</div>
@section Scripts {
<script>
function setFechas(inicio, fin) {
document.getElementById('fechaInicio').value = inicio;
document.getElementById('fechaFin').value = fin;
}
function reporteHoy() {
const hoy = new Date().toISOString().split('T')[0];
setFechas(hoy, hoy);
}
function reporteSemana() {
const hoy = new Date();
const inicioSemana = new Date(hoy);
inicioSemana.setDate(hoy.getDate() - hoy.getDay());
const finSemana = new Date(inicioSemana);
finSemana.setDate(inicioSemana.getDate() + 6);
setFechas(inicioSemana.toISOString().split('T')[0], finSemana.toISOString().split('T')[0]);
}
function reporteMes() {
const hoy = new Date();
const inicio = new Date(hoy.getFullYear(), hoy.getMonth(), 1);
const fin = new Date(hoy.getFullYear(), hoy.getMonth() + 1, 0);
setFechas(inicio.toISOString().split('T')[0], fin.toISOString().split('T')[0]);
}
function reporteAnio() {
const hoy = new Date();
const inicio = new Date(hoy.getFullYear(), 0, 1);
const fin = new Date(hoy.getFullYear(), 11, 31);
setFechas(inicio.toISOString().split('T')[0], fin.toISOString().split('T')[0]);
}
@if (TempData["Error"] != null)
{
<text>
toastr.error('@TempData["Error"]');
</text>
}
</script>
}

View File

@@ -0,0 +1,64 @@
@model Rs_system.Models.ContabilidadRegistro
@{
ViewData["Title"] = "Nuevo Registro Contable";
}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card card-custom">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h5 class="card-title mb-0">Nuevo Movimiento</h5>
</div>
<div class="card-body">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-3">
<label asp-for="GrupoTrabajoId" class="form-label">Grupo de Trabajo</label>
<select asp-for="GrupoTrabajoId" class="form-select" asp-items="ViewBag.Grupos">
<option value="">Seleccione...</option>
</select>
<span asp-validation-for="GrupoTrabajoId" class="text-danger"></span>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label asp-for="Tipo" class="form-label">Tipo de Movimiento</label>
<select asp-for="Tipo" class="form-select" asp-items="Html.GetEnumSelectList<Rs_system.Models.TipoMovimientoContable>()"></select>
<span asp-validation-for="Tipo" class="text-danger"></span>
</div>
<div class="col-md-6 mb-3">
<label asp-for="Fecha" class="form-label">Fecha</label>
<input asp-for="Fecha" type="date" class="form-control" value="@DateTime.Now.ToString("yyyy-MM-dd")" />
<span asp-validation-for="Fecha" class="text-danger"></span>
</div>
</div>
<div class="mb-3">
<label asp-for="Monto" class="form-label">Monto</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="Monto" class="form-control" type="number" step="0.01" />
</div>
<span asp-validation-for="Monto" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="3"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary">Guardar</button>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,140 @@
@using Rs_system.Models
@model List<ReporteMensualContable>
@{
ViewData["Title"] = "Contabilidad Mensual";
var grupoId = ViewBag.GrupoId as long?;
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Contabilidad Mensual</h4>
<p class="text-muted mb-0">Gestión de reportes financieros por grupo</p>
</div>
</div>
<div class="card card-custom mb-4">
<div class="card-body">
<form method="get" class="row g-3 align-items-end">
<div class="col-md-8">
<label class="form-label">Grupo de Trabajo</label>
<select name="grupoId" class="form-select" asp-items="ViewBag.Grupos" onchange="this.form.submit()">
<option value="">Seleccione un grupo...</option>
</select>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-primary-custom w-100" data-bs-toggle="modal" data-bs-target="#abrirMesModal" @(grupoId.HasValue ? "" : "disabled")>
<i class="bi bi-plus-lg me-1"></i> Abrir Nuevo Mes
</button>
</div>
</form>
</div>
</div>
@if (grupoId.HasValue)
{
@if (Model != null && Model.Any())
{
<div class="card card-custom">
<div class="table-responsive">
<table class="table table-custom">
<thead>
<tr>
<th>Mes / Año</th>
<th>Saldo Inicial</th>
<th>Estado</th>
<th>Fecha Creación</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td class="fw-bold">@item.NombreMes @item.Anio</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>@item.FechaCreacion.ToLocalTime().ToString("dd/MM/yyyy HH:mm")</td>
<td class="text-end">
<a asp-action="RegistroMensual" asp-route-id="@item.Id" class="btn btn-sm btn-outline-success">
<i class="bi bi-table me-1"></i> @(item.Cerrado ? "Ver Detalles" : "Gestionar Registros")
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
else
{
<div class="alert alert-info py-4 text-center">
<i class="bi bi-info-circle fs-3 d-block mb-3"></i>
<h5>No hay reportes mensuales para este grupo</h5>
<p class="mb-0">Haga clic en "Abrir Nuevo Mes" para comenzar el registro contable de este mes.</p>
</div>
}
}
else
{
<div class="alert alert-light border text-center py-5">
<i class="bi bi-people fs-2 d-block mb-3 text-muted"></i>
<h5 class="text-muted">Seleccione un grupo para ver sus reportes mensuales</h5>
</div>
}
<!-- Modal Abrir Mes -->
<div class="modal fade" id="abrirMesModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form asp-action="AbrirMes" method="post">
<input type="hidden" name="grupoId" value="@grupoId" />
<div class="modal-header">
<h5 class="modal-title">Abrir Nuevo Mes Contable</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Mes</label>
<select name="mes" class="form-select">
@for (int i = 1; i <= 12; i++)
{
bool isCurrent = i == DateTime.Now.Month;
<!option value="@i" @(isCurrent ? "selected" : "")>@(new DateTime(2000, i, 1).ToString("MMMM", new System.Globalization.CultureInfo("es-ES")))</!option>
}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Año</label>
<select name="anio" class="form-select">
@for (int i = DateTime.Now.Year - 1; i <= DateTime.Now.Year + 1; i++)
{
bool isCurrent = i == DateTime.Now.Year;
<!option value="@i" @(isCurrent ? "selected" : "")>@i</!option>
}
</select>
</div>
</div>
<div class="alert alert-info mt-3 mb-0 py-2 small">
<i class="bi bi-lightbulb me-2"></i>El saldo inicial se calculará automáticamente a partir del cierre del mes anterior si existe.
</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-custom">Abrir Mes</button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,261 @@
@model Rs_system.Models.ReporteMensualContable
@{
ViewData["Title"] = $"Reporte {Model.NombreMes} {Model.Anio}";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">@Model.GrupoTrabajo.Nombre - @Model.NombreMes @Model.Anio</h4>
<div class="d-flex align-items-center gap-2">
@if (Model.Cerrado)
{
<span class="badge bg-secondary"><i class="bi bi-lock-fill me-1"></i> REPORTE CERRADO</span>
}
else
{
<span class="badge bg-success"><i class="bi bi-unlock-fill me-1"></i> REPORTE ABIERTO</span>
}
<span class="text-muted small">Creado el @Model.FechaCreacion.ToLocalTime().ToString("dd/MM/yyyy")</span>
</div>
</div>
<div class="d-flex gap-2">
<a asp-action="Index" asp-route-grupoId="@Model.GrupoTrabajoId" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Volver a Planilla
</a>
@if (!Model.Cerrado)
{
<form asp-action="CerrarMes" asp-route-id="@Model.Id" method="post" onsubmit="return confirm('¿Está seguro de cerrar este mes? Ya no se podrán realizar más cambios.')">
<button type="submit" class="btn btn-danger">
<i class="bi bi-lock me-1"></i> Cerrar Mes
</button>
</form>
}
</div>
</div>
<div class="row mb-4">
<div class="col-md-3">
<div class="card card-custom p-3 text-center bg-light">
<span class="text-muted small">Saldo Inicial</span>
<h4 class="mb-0">@Model.SaldoInicial.ToString("N2")</h4>
</div>
</div>
<div class="col-md-3">
<div class="card card-custom p-3 text-center bg-light">
<span class="text-muted small">Ingresos (+)</span>
<h4 class="mb-0 text-success" id="totalIngresos">0.00</h4>
</div>
</div>
<div class="col-md-3">
<div class="card card-custom p-3 text-center bg-light">
<span class="text-muted small">Egresos (-)</span>
<h4 class="mb-0 text-danger" id="totalEgresos">0.00</h4>
</div>
</div>
<div class="col-md-3">
<div class="card card-custom p-3 text-center border-primary shadow-sm">
<span class="text-muted small">Saldo Final (=)</span>
<h4 class="mb-0 text-primary" id="saldoFinal">0.00</h4>
</div>
</div>
</div>
<div class="card card-custom">
<div class="card-header d-flex justify-content-between align-items-center bg-white border-bottom py-3">
<h6 class="mb-0"><i class="bi bi-grid-3x3 me-2"></i>Registros de Movimientos</h6>
@if (!Model.Cerrado)
{
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addRow()">
<i class="bi bi-plus-lg me-1"></i> Agregar Fila
</button>
<button type="button" class="btn btn-sm btn-primary-custom" onclick="saveAll()">
<i class="bi bi-cloud-arrow-up me-1"></i> Guardar Cambios
</button>
</div>
}
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="excelTable">
<thead class="bg-light">
<tr>
<th style="width: 50px;" class="text-center">#</th>
<th style="width: 180px;">Fecha</th>
<th style="width: 200px;">Tipo</th>
<th>Descripción</th>
<th style="width: 200px;" class="text-end">Monto</th>
@if(!Model.Cerrado) { <th style="width: 60px;"></th> }
</tr>
</thead>
<tbody id="tableBody">
@if (Model.Registros != null && Model.Registros.Any())
{
int count = 1;
foreach (var item in Model.Registros.OrderBy(r => r.Fecha))
{
<tr data-id="@item.Id">
<td class="text-center text-muted">@count</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>
<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="0" @(item.Tipo == TipoMovimientoContable.Egreso ? "selected" : "")>Egreso (-)</!option>
</select>
</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="number" step="0.01" class="form-control form-control-sm text-end row-monto" value="@item.Monto.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)" @(Model.Cerrado ? "disabled" : "") onchange="updateTotals()" /></td>
@if(!Model.Cerrado) {
<td class="text-center">
<button class="btn btn-sm text-danger" onclick="deleteRow(this)"><i class="bi bi-trash"></i></button>
</td>
}
</tr>
count++;
}
}
</tbody>
@if(!Model.Cerrado) {
<tfoot>
<tr>
<td colspan="6" class="p-0">
<button type="button" class="btn btn-link w-100 text-decoration-none py-3 text-muted" onclick="addRow()">
<i class="bi bi-plus-circle me-1"></i> Haga clic aquí para agregar una nueva fila
</button>
</td>
</tr>
</tfoot>
}
</table>
</div>
</div>
</div>
@section Scripts {
<script>
const reporteId = @Model.Id;
const saldoInicial = @Model.SaldoInicial.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture);
const isCerrado = @Model.Cerrado.ToString().ToLower();
function addRow() {
if (isCerrado) return;
const tbody = document.getElementById('tableBody');
const rowCount = tbody.children.length + 1;
const today = new Date().toISOString().split('T')[0];
const tr = document.createElement('tr');
tr.setAttribute('data-id', '0');
tr.innerHTML = `
<td class="text-center text-muted">${rowCount}</td>
<td><input type="date" class="form-control form-control-sm row-fecha" value="${today}" /></td>
<td>
<select class="form-select form-select-sm row-tipo" onchange="updateTotals()">
<option value="1">Ingreso (+)</option>
<option value="0">Egreso (-)</option>
</select>
</td>
<td><input type="text" class="form-control form-control-sm row-descripcion" value="" placeholder="Motivo del movimiento..." /></td>
<td><input type="number" step="0.01" class="form-control form-control-sm text-end row-monto" value="0.00" onchange="updateTotals()" /></td>
<td class="text-center">
<button class="btn btn-sm text-danger" onclick="deleteRow(this)"><i class="bi bi-trash"></i></button>
</td>
`;
tbody.appendChild(tr);
updateTotals();
}
function deleteRow(btn) {
if (isCerrado) return;
const row = btn.closest('tr');
row.remove();
// Renumber rows
const rows = document.querySelectorAll('#tableBody tr');
rows.forEach((r, idx) => {
r.cells[0].innerText = idx + 1;
});
updateTotals();
}
function updateTotals() {
let ingresos = 0;
let egresos = 0;
const rows = document.querySelectorAll('#tableBody tr');
rows.forEach(row => {
const tipo = parseInt(row.querySelector('.row-tipo').value);
const monto = parseFloat(row.querySelector('.row-monto').value) || 0;
if (tipo === 1) ingresos += monto;
else egresos += monto;
});
document.getElementById('totalIngresos').innerText = ingresos.toFixed(2);
document.getElementById('totalEgresos').innerText = egresos.toFixed(2);
document.getElementById('saldoFinal').innerText = (saldoInicial + ingresos - egresos).toFixed(2);
}
async function saveAll() {
if (isCerrado) return;
const registros = [];
const rows = document.querySelectorAll('#tableBody tr');
rows.forEach(row => {
registros.push({
id: parseInt(row.getAttribute('data-id')),
fecha: row.querySelector('.row-fecha').value,
tipo: parseInt(row.querySelector('.row-tipo').value),
monto: parseFloat(row.querySelector('.row-monto').value) || 0,
descripcion: row.querySelector('.row-descripcion').value
});
});
const btn = event.target.closest('button');
const originalContent = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Guardando...';
btn.disabled = true;
try {
const response = await fetch('/Contabilidad/GuardarBulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reporteId: reporteId, registros: registros })
});
const result = await response.json();
if (result.success) {
Swal.fire({
icon: 'success',
title: '¡Guardado!',
text: 'Los registros se han guardado exitosamente.',
timer: 1500,
showConfirmButton: false
});
// Refresh view results would be better, but for now we trust the saldo returned
document.getElementById('saldoFinal').innerText = result.saldo.toFixed(2);
window.location.reload(); // Reload to refresh IDs and state
} else {
Swal.fire({
icon: 'error',
title: 'Error',
text: result.message || 'Error al guardar los registros.'
});
}
} catch (error) {
console.error(error);
Swal.fire({
icon: 'error',
title: 'Error de Red',
text: 'No se pudo conectar con el servidor.'
});
} finally {
btn.innerHTML = originalContent;
btn.disabled = false;
}
}
// Initialize totals on load
document.addEventListener('DOMContentLoaded', updateTotals);
</script>
}

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

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

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

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

View File

@@ -0,0 +1,67 @@
@model Rs_system.Models.EstadoArticulo
@{
ViewData["Title"] = "Nuevo Estado";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Nuevo Estado</h4>
<p class="text-muted mb-0">Registrar un nuevo estado para clasificación</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="card-custom" style="max-width: 800px; margin: 0 auto;">
<div class="card-body">
<form asp-action="Create" method="post">
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="mb-3">
<label asp-for="Nombre" class="form-label">Nombre <span class="text-danger">*</span></label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Bueno, Malo, Regular" autofocus />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="2" placeholder="Breve descripción del estado (opcional)"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Color" class="form-label">Color de Etiqueta</label>
<select asp-for="Color" class="form-select">
<option value="secondary">Gris (Por defecto)</option>
<option value="success">Verde (Bueno/Completo)</option>
<option value="warning">Amarillo (Advertencia/Reparación)</option>
<option value="danger">Rojo (Malo/Crítico)</option>
<option value="info">Azul (Informativo)</option>
<option value="primary">Azul Oscuro (Primario)</option>
</select>
<div class="form-text">Color que se usará para mostrar el estado en las listas.</div>
<span asp-validation-for="Color" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input asp-for="Activo" class="form-check-input" type="checkbox" role="switch" checked />
<label asp-for="Activo" class="form-check-label">Estado Activo</label>
</div>
<div class="form-text">Si está inactivo, no aparecerá en las selecciones.</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Guardar
</button>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,68 @@
@model Rs_system.Models.EstadoArticulo
@{
ViewData["Title"] = "Editar Estado";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Editar Estado</h4>
<p class="text-muted mb-0">Modificar información del estado</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="card-custom" style="max-width: 800px; margin: 0 auto;">
<div class="card-body">
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="Id" />
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="mb-3">
<label asp-for="Nombre" class="form-label">Nombre <span class="text-danger">*</span></label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Bueno, Malo, Regular" />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="2" placeholder="Breve descripción del estado (opcional)"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Color" class="form-label">Color de Etiqueta</label>
<select asp-for="Color" class="form-select">
<option value="secondary">Gris (Por defecto)</option>
<option value="success">Verde (Bueno/Completo)</option>
<option value="warning">Amarillo (Advertencia/Reparación)</option>
<option value="danger">Rojo (Malo/Crítico)</option>
<option value="info">Azul (Informativo)</option>
<option value="primary">Azul Oscuro (Primario)</option>
</select>
<div class="form-text">Color que se usará para mostrar el estado en las listas.</div>
<span asp-validation-for="Color" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input asp-for="Activo" class="form-check-input" type="checkbox" role="switch" />
<label asp-for="Activo" class="form-check-label">Estado Activo</label>
</div>
<div class="form-text">Si está inactivo, no aparecerá en las selecciones.</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Guardar Cambios
</button>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,121 @@
@model IEnumerable<Rs_system.Models.EstadoArticulo>
@{
ViewData["Title"] = "Estados de Artículos";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Estados de Artículos</h4>
<p class="text-muted mb-0">Gestión de estados para bienes e inventario</p>
</div>
<a asp-action="Create" class="btn btn-primary-custom">
<i class="bi bi-plus-lg me-1"></i> Nuevo Estado
</a>
</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">Etiqueta Visual</th>
<th class="text-center">Estado</th>
<th class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
@if (!Model.Any())
{
<tr>
<td colspan="5" class="text-center text-muted py-4">
<i class="bi bi-palette fs-1 d-block mb-2"></i>
No hay estados registrados
</td>
</tr>
}
@foreach (var item in Model)
{
<tr>
<td>
<strong>@item.Nombre</strong>
</td>
<td>
@if (!string.IsNullOrEmpty(item.Descripcion))
{
<span>@item.Descripcion</span>
}
else
{
<span class="text-muted">-</span>
}
</td>
<td class="text-center">
<span class="badge bg-@item.Color">@item.Nombre</span>
</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">
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary" title="Editar">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="confirmDelete(@item.Id)" title="Eliminar">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Delete Form -->
<form id="deleteForm" asp-action="Delete" method="post" style="display: none;">
<input type="hidden" name="id" id="deleteId" />
</form>
@section Scripts {
<script>
function confirmDelete(id) {
Swal.fire({
title: '¿Eliminar estado?',
text: 'Esta acción moverá el estado a la papelera.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Sí, eliminar',
cancelButtonText: 'Cancelar'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
});
}
// Show success/error messages
@if (TempData["SuccessMessage"] != null)
{
<text>
toastr.success('@TempData["SuccessMessage"]');
</text>
}
@if (TempData["ErrorMessage"] != null)
{
<text>
toastr.error('@TempData["ErrorMessage"]');
</text>
}
</script>
}

View File

@@ -0,0 +1,298 @@
@model dynamic
@{
ViewData["Title"] = "Nuevo Movimiento";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Registrar Movimiento</h4>
<p class="text-muted mb-0">Traslados, bajas o cambios de estado</p>
</div>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Cancelar
</a>
</div>
<div class="row">
<!-- Step 1: Select Article -->
<div class="col-md-4 mb-4">
<div class="card-custom h-100">
<div class="card-header bg-transparent py-3">
<h5 class="card-title mb-0">1. Seleccionar Artículo</h5>
</div>
<div class="card-body">
<form method="get" asp-action="Create">
<div class="mb-3">
<label class="form-label">Buscar Artículo</label>
<select name="articuloId" class="form-select" asp-items="ViewBag.Articulos" onchange="this.form.submit()">
<option value="">-- Seleccione un artículo --</option>
</select>
<div class="form-text">Seleccione para cargar datos actuales.</div>
</div>
</form>
@if (ViewBag.ArticuloId != null)
{
<div class="alert alert-light border mt-3">
<h6 class="fw-bold mb-2">Estado Actual</h6>
<div class="mb-1"><span class="text-muted">Ubicación:</span> <strong>@ViewBag.UbicacionActual</strong></div>
<div class="mb-1"><span class="text-muted">Estado:</span> <strong>@ViewBag.EstadoActual</strong></div>
</div>
}
</div>
</div>
</div>
<!-- Step 2: Select Action -->
<div class="col-md-8">
@if (ViewBag.ArticuloId != null)
{
<div class="card-custom">
<div class="card-header bg-transparent py-3">
<ul class="nav nav-pills card-header-pills" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-entrada-tab" data-bs-toggle="pill" data-bs-target="#pills-entrada" type="button" role="tab">
<i class="bi bi-plus-circle me-1"></i> Entrada
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-traslado-tab" data-bs-toggle="pill" data-bs-target="#pills-traslado" type="button" role="tab">
<i class="bi bi-arrow-left-right me-1"></i> Traslado
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-estado-tab" data-bs-toggle="pill" data-bs-target="#pills-estado" type="button" role="tab">
<i class="bi bi-cone-striped me-1"></i> Cambio de Estado
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-prestamo-tab" data-bs-toggle="pill" data-bs-target="#pills-prestamo" type="button" role="tab">
<i class="bi bi-box-arrow-right me-1"></i> Préstamo
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link text-danger" id="pills-baja-tab" data-bs-toggle="pill" data-bs-target="#pills-baja" type="button" role="tab">
<i class="bi bi-trash me-1"></i> Dar de Baja
</button>
</li>
</ul>
</div>
<div class="card-body pt-4">
<div class="tab-content" id="pills-tabContent">
<!-- ENTRADA FORM -->
<div class="tab-pane fade show active" id="pills-entrada" role="tabpanel">
<form asp-action="RegistrarEntrada" method="post">
<input type="hidden" name="articuloId" value="@ViewBag.ArticuloId" />
<h5 class="mb-3">Registrar Entrada de Inventario (Compra/Reingreso)</h5>
<div class="alert alert-success py-2">
<i class="bi bi-info-circle me-2"></i>
Esta acción aumentará el stock actual del artículo.
</div>
<div class="mb-3">
<label class="form-label fw-bold">Cantidad a Ingresar</label>
@if (ViewBag.TipoControl == "LOTE")
{
<input type="number" name="cantidad" class="form-control" min="1" value="1" required />
}
else
{
<input type="number" name="cantidad" class="form-control" value="1" readonly />
<div class="form-text">Para artículos unitarios, la cantidad es siempre 1.</div>
}
</div>
<div class="mb-3">
<label class="form-label">Observación / Referencia</label>
<textarea name="observacion" class="form-control" rows="2" placeholder="Ej: Compra según factura #123, Donación recibida..."></textarea>
</div>
<button type="submit" class="btn btn-success">
<i class="bi bi-plus-lg me-1"></i> Confirmar Entrada
</button>
</form>
</div>
<!-- TRASLADO FORM -->
<div class="tab-pane fade" id="pills-traslado" role="tabpanel">
<form asp-action="RegistrarTraslado" method="post">
<input type="hidden" name="articuloId" value="@ViewBag.ArticuloId" />
<h5 class="mb-3">Registrar Traslado de Ubicación</h5>
@if (ViewBag.TipoControl == "LOTE")
{
<div class="alert alert-info py-2">
<div class="d-flex justify-content-between">
<span><strong>Control por Lote:</strong> Especifique la cantidad a mover.</span>
<span class="badge bg-light text-dark border">Global: @ViewBag.CantidadGlobal</span>
</div>
</div>
<div class="mb-3">
<label class="form-label text-primary fw-bold">Cantidad a Mover</label>
<input type="number" name="cantidad" class="form-control" min="1" max="@ViewBag.CantidadGlobal" value="1" required />
</div>
}
<div class="mb-3">
<label class="form-label">Nueva Ubicación</label>
<select name="nuevaUbicacionId" class="form-select" asp-items="ViewBag.Ubicaciones" required>
<option value="">-- Seleccionar Destino --</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Observación</label>
<textarea name="observacion" class="form-control" rows="2" placeholder="Motivo del traslado..."></textarea>
</div>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Confirmar Traslado
</button>
</form>
</div>
<!-- CAMBIO ESTADO FORM -->
<div class="tab-pane fade" id="pills-estado" role="tabpanel">
<form asp-action="RegistrarCambioEstado" method="post">
<input type="hidden" name="articuloId" value="@ViewBag.ArticuloId" />
<h5 class="mb-3">Registrar Cambio de Condición</h5>
<div class="alert alert-light border">
<i class="bi bi-info-circle me-1"></i>
@if(ViewBag.TipoControl == "LOTE") {
<span>El cambio de estado aplicará a <strong>todo el lote</strong> (@ViewBag.CantidadGlobal unidades).</span>
} else {
<span>El cambio de estado aplica a la unidad única.</span>
}
</div>
<div class="mb-3">
<label class="form-label">Nuevo Estado</label>
<select name="nuevoEstadoId" class="form-select" asp-items="ViewBag.Estados" required>
<option value="">-- Seleccionar Nuevo Estado --</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Observación</label>
<textarea name="observacion" class="form-control" rows="2" placeholder="Detalles del daño o reparación..."></textarea>
</div>
<button type="submit" class="btn btn-warning text-white">
<i class="bi bi-save me-1"></i> Confirmar Cambio
</button>
</form>
</div>
<!-- PRESTAMO FORM -->
<div class="tab-pane fade" id="pills-prestamo" role="tabpanel">
<form asp-action="RegistrarPrestamo" method="post">
<input type="hidden" name="articuloId" value="@ViewBag.ArticuloId" />
<h5 class="mb-3">Registrar Préstamo a Persona</h5>
@if (ViewBag.TipoControl == "LOTE")
{
<div class="alert alert-info py-2">
<div class="d-flex justify-content-between">
<span><strong>Control por Lote:</strong> Especifique la cantidad a prestar.</span>
<span class="badge bg-light text-dark border">Disponible: @ViewBag.CantidadGlobal</span>
</div>
</div>
<div class="mb-3">
<label class="form-label text-primary fw-bold">Cantidad a Prestar</label>
<input type="number" name="cantidad" class="form-control" min="1" max="@ViewBag.CantidadGlobal" value="1" required />
</div>
}
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Nombre de la Persona <span class="text-danger">*</span></label>
<input type="text" name="personaNombre" class="form-control" placeholder="Nombre completo" required />
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Identificación</label>
<input type="text" name="personaIdentificacion" class="form-control" placeholder="Cédula, DNI, etc." />
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Fecha Devolución Estimada</label>
<input type="date" name="fechaDevolucionEstimada" class="form-control" />
<div class="form-text">Fecha aproximada de devolución del artículo.</div>
</div>
<div class="mb-3">
<label class="form-label">Observación</label>
<textarea name="observacion" class="form-control" rows="2" placeholder="Detalles del préstamo..."></textarea>
</div>
<div class="alert alert-light border">
<i class="bi bi-info-circle me-1"></i>
<span>Se generarán códigos individuales para cada artículo prestado (ej: sp-b20-001, sp-b20-002, ...)</span>
</div>
<button type="submit" class="btn btn-info text-white">
<i class="bi bi-box-arrow-right me-1"></i> Confirmar Préstamo
</button>
</form>
</div>
<!-- BAJA FORM -->
<div class="tab-pane fade" id="pills-baja" role="tabpanel">
<form asp-action="RegistrarBaja" method="post" onsubmit="return confirm('¿Está seguro de realizar esta baja?');">
<input type="hidden" name="articuloId" value="@ViewBag.ArticuloId" />
<h5 class="mb-3 text-danger">Registrar Baja de Activo</h5>
@if (ViewBag.TipoControl == "LOTE")
{
<div class="mb-3">
<label class="form-label fw-bold">Cantidad a dar de Baja</label>
<input type="number" name="cantidad" class="form-control" min="1" max="@ViewBag.CantidadGlobal" value="1" required />
<div class="form-text">Esto restará del stock global.</div>
</div>
}
else
{
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Atención:</strong> Esta acción marcará el artículo como inactivo.
</div>
}
<div class="mb-3">
<label class="form-label">Motivo de Baja <span class="text-danger">*</span></label>
<textarea name="motivo" class="form-control" rows="3" placeholder="Ej: Robo, Pérdida total, Venta, Donación..." required></textarea>
</div>
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash me-1"></i> Confirmar Baja
</button>
</form>
</div>
</div>
</div>
</div>
}
else
{
<div class="card-custom h-100 d-flex align-items-center justify-content-center bg-light border-0">
<div class="text-center text-muted py-5">
<i class="bi bi-arrow-left-circle fs-1 mb-3 d-block"></i>
<h5>Seleccione un artículo para comenzar</h5>
<p>Use el panel de la izquierda para buscar el activo.</p>
</div>
</div>
}
</div>
</div>
@section Scripts {
<script>
@if (TempData["ErrorMessage"] != null)
{
<text>toastr.error('@TempData["ErrorMessage"]');</text>
}
</script>
}

View File

@@ -0,0 +1,128 @@
@model IEnumerable<Rs_system.Models.MovimientoInventario>
@{
ViewData["Title"] = "Historial de Movimientos";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Historial de Movimientos</h4>
<p class="text-muted mb-0">Registro de traslados, bajas y cambios de estado</p>
</div>
<a asp-action="Create" class="btn btn-primary-custom">
<i class="bi bi-arrow-left-right me-1"></i> Nuevo Movimiento
</a>
</div>
<div class="card-custom">
<div class="table-responsive">
<table class="table-custom align-middle">
<thead>
<tr>
<th>Fecha</th>
<th>Artículo</th>
<th>Tipo</th>
<th>Detalles</th>
<th>Usuario</th>
</tr>
</thead>
<tbody>
@if (!Model.Any())
{
<tr>
<td colspan="5" class="text-center text-muted py-5">
<i class="bi bi-clock-history fs-1 d-block mb-2"></i>
No hay movimientos registrados
</td>
</tr>
}
@foreach (var item in Model)
{
<tr>
<td style="width: 150px;">
<div class="fw-bold">@item.Fecha.ToLocalTime().ToString("dd/MM/yyyy")</div>
<small class="text-muted">@item.Fecha.ToLocalTime().ToString("HH:mm")</small>
</td>
<td>
<div class="fw-bold">@item.Articulo?.Codigo</div>
<small class="text-muted">@item.Articulo?.Nombre</small>
</td>
<td>
@switch (item.TipoMovimiento)
{
case "ENTRADA":
<span class="badge bg-success">ENTRADA</span>
break;
case "TRASLADO":
<span class="badge bg-info text-dark">TRASLADO</span>
break;
case "BAJA":
<span class="badge bg-danger">BAJA</span>
break;
case "PRESTAMO":
<span class="badge bg-primary">PRESTAMO</span>
break;
case "DEVOLUCION":
<span class="badge bg-teal" style="background-color: #20c997; color: white;">DEVOLUCIÓN</span>
break;
case "CAMBIO_ESTADO":
<span class="badge bg-warning text-dark">ESTADO</span>
break;
default:
<span class="badge bg-secondary">@item.TipoMovimiento</span>
break;
}
</td>
<td>
@if (item.TipoMovimiento == "TRASLADO")
{
<div class="d-flex align-items-center">
<span class="text-muted me-2">@item.UbicacionOrigen?.Nombre</span>
<i class="bi bi-arrow-right text-primary me-2"></i>
<span class="fw-bold">@item.UbicacionDestino?.Nombre</span>
</div>
}
else if (item.TipoMovimiento == "CAMBIO_ESTADO")
{
<div class="d-flex align-items-center">
<span class="text-muted me-2">@item.EstadoAnterior?.Nombre</span>
<i class="bi bi-arrow-right text-primary me-2"></i>
<span class="fw-bold">@item.EstadoNuevo?.Nombre</span>
</div>
}
@if (item.Cantidad > 1 || item.Articulo?.TipoControl == "LOTE")
{
<div class="mt-1">
<span class="badge bg-light text-dark border">Cantidad: @item.Cantidad</span>
</div>
}
@if (!string.IsNullOrEmpty(item.Observacion))
{
<div class="text-muted fst-italic mt-1 small">
<i class="bi bi-card-text me-1"></i> @item.Observacion
</div>
}
</td>
<td>
<small class="text-muted">@item.UsuarioId</small>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@section Scripts {
<script>
@if (TempData["SuccessMessage"] != null)
{
<text>toastr.success('@TempData["SuccessMessage"]');</text>
}
@if (TempData["ErrorMessage"] != null)
{
<text>toastr.error('@TempData["ErrorMessage"]');</text>
}
</script>
}

View File

@@ -0,0 +1,156 @@
@model IEnumerable<Rs_system.Models.Prestamo>
@{
ViewData["Title"] = "Préstamos Activos";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Préstamos Activos</h4>
<p class="text-muted mb-0">Artículos actualmente prestados</p>
</div>
<div>
<a asp-action="Create" class="btn btn-outline-primary me-2">
<i class="bi bi-plus-circle me-1"></i> Nuevo Préstamo
</a>
<a asp-action="Index" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Historial
</a>
</div>
</div>
<div class="card-custom">
<div class="card-body">
@if (!Model.Any())
{
<div class="text-center py-5">
<i class="bi bi-box-arrow-in-right fs-1 text-muted mb-3 d-block"></i>
<h5 class="text-muted">No hay préstamos activos</h5>
<p class="text-muted">Todos los artículos han sido devueltos.</p>
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Artículo</th>
<th>Persona</th>
<th>Cantidad</th>
<th>Fecha Préstamo</th>
<th>Devolución Estimada</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
@foreach (var prestamo in Model)
{
<tr>
<td>
<div class="fw-bold">@prestamo.Articulo?.Codigo</div>
<div class="text-muted small">@prestamo.Articulo?.Nombre</div>
</td>
<td>
<div class="fw-bold">@prestamo.PersonaNombre</div>
@if (!string.IsNullOrEmpty(prestamo.PersonaIdentificacion))
{
<div class="text-muted small">@prestamo.PersonaIdentificacion</div>
}
</td>
<td>
<span class="badge bg-light text-dark">@prestamo.Cantidad</span>
</td>
<td>@prestamo.FechaPrestamo.ToString("dd/MM/yyyy")</td>
<td>
@if (prestamo.FechaDevolucionEstimada.HasValue)
{
var diasRestantes = (prestamo.FechaDevolucionEstimada.Value - DateTime.Today).Days;
var claseCss = diasRestantes < 0 ? "text-danger" : diasRestantes <= 3 ? "text-warning" : "text-success";
<span class="@claseCss">@prestamo.FechaDevolucionEstimada.Value.ToString("dd/MM/yyyy")</span>
}
else
{
<span class="text-muted">No definida</span>
}
</td>
<td>
@switch (prestamo.Estado)
{
case "ACTIVO":
<span class="badge bg-info">Activo</span>
break;
case "ATRASADO":
<span class="badge bg-danger">Atrasado</span>
break;
default:
<span class="badge bg-secondary">@prestamo.Estado</span>
break;
}
</td>
<td>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-info" onclick="verDetalles(@prestamo.Id)">
<i class="bi bi-eye"></i>
</button>
<form asp-action="RegistrarDevolucion" method="post" style="display: inline;">
<input type="hidden" name="prestamoId" value="@prestamo.Id" />
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('¿Confirmar devolución de este préstamo?');">
<i class="bi bi-check-circle"></i> Devolver
</button>
</form>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
<!-- Modal Detalles -->
<div class="modal fade" id="detallesModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Detalles del Préstamo</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="detallesContent">
<!-- Content loaded via AJAX -->
</div>
</div>
</div>
</div>
@section Scripts {
<script>
function verDetalles(prestamoId) {
// Aquí podrías implementar una llamada AJAX para cargar los detalles
// Por ahora mostramos un mensaje simple
$('#detallesContent').html('<div class="text-center py-3"><i class="bi bi-hourglass-split"></i> Cargando detalles...</div>');
$('#detallesModal').modal('show');
// Simulación de carga
setTimeout(() => {
$('#detallesContent').html(`
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
Los códigos individuales de los artículos prestados se mostrarán aquí.
</div>
`);
}, 500);
}
@if (TempData["SuccessMessage"] != null)
{
<text>toastr.success('@TempData["SuccessMessage"]');</text>
}
@if (TempData["ErrorMessage"] != null)
{
<text>toastr.error('@TempData["ErrorMessage"]');</text>
}
</script>
}

View File

@@ -0,0 +1,63 @@
@model Rs_system.Models.TipoColaboracion
@{
ViewData["Title"] = "Nuevo Tipo de Colaboración";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Nuevo Tipo de Colaboración</h4>
<p class="text-muted mb-0">Crear un nuevo tipo de colaboración</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="card-custom">
<form asp-action="Create" method="post">
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="mb-3">
<label asp-for="Nombre" class="form-label"></label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Mantenimiento" required />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label"></label>
<textarea asp-for="Descripcion" class="form-control" rows="2" placeholder="Descripción del tipo de colaboración"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="MontoSugerido" class="form-label"></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="MontoSugerido" type="number" step="0.01" class="form-control" placeholder="1.00" required />
</div>
<span asp-validation-for="MontoSugerido" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Orden" class="form-label"></label>
<input asp-for="Orden" type="number" class="form-control" placeholder="1" required />
<span asp-validation-for="Orden" class="text-danger"></span>
<small class="form-text text-muted">Orden de aparición en las listas</small>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input asp-for="Activo" class="form-check-input" type="checkbox" checked />
<label asp-for="Activo" class="form-check-label"></label>
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Guardar
</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,65 @@
@model Rs_system.Models.TipoColaboracion
@{
ViewData["Title"] = "Editar Tipo de Colaboración";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Editar Tipo de Colaboración</h4>
<p class="text-muted mb-0">Modificar tipo de colaboración existente</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="card-custom">
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="CreadoEn" />
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="mb-3">
<label asp-for="Nombre" class="form-label"></label>
<input asp-for="Nombre" class="form-control" required />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label"></label>
<textarea asp-for="Descripcion" class="form-control" rows="2"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="MontoSugerido" class="form-label"></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input asp-for="MontoSugerido" type="number" step="0.01" class="form-control" required />
</div>
<span asp-validation-for="MontoSugerido" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Orden" class="form-label"></label>
<input asp-for="Orden" type="number" class="form-control" required />
<span asp-validation-for="Orden" class="text-danger"></span>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input asp-for="Activo" class="form-check-input" type="checkbox" />
<label asp-for="Activo" class="form-check-label"></label>
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Actualizar
</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,112 @@
@model IEnumerable<Rs_system.Models.TipoColaboracion>
@{
ViewData["Title"] = "Tipos de Colaboración";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Tipos de Colaboración</h4>
<p class="text-muted mb-0">Gestión de tipos de colaboración económica</p>
</div>
<a asp-action="Create" class="btn btn-primary-custom">
<i class="bi bi-plus-lg me-1"></i> Nuevo Tipo
</a>
</div>
<div class="card-custom">
<div class="table-responsive">
<table class="table-custom">
<thead>
<tr>
<th>Orden</th>
<th>Nombre</th>
<th>Descripción</th>
<th>Monto Sugerido</th>
<th class="text-center">Estado</th>
<th class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
@if (!Model.Any())
{
<tr>
<td colspan="6" class="text-center text-muted py-4">
<i class="bi bi-list-ul fs-1 d-block mb-2"></i>
No hay tipos de colaboración registrados
</td>
</tr>
}
@foreach (var tipo in Model)
{
<tr>
<td><strong>@tipo.Orden</strong></td>
<td><strong>@tipo.Nombre</strong></td>
<td>@(tipo.Descripcion ?? "-")</td>
<td><span class="text-success">$@tipo.MontoSugerido.ToString("N2")</span></td>
<td class="text-center">
@if (tipo.Activo)
{
<span class="badge bg-success">Activo</span>
}
else
{
<span class="badge bg-secondary">Inactivo</span>
}
</td>
<td class="text-center">
<a asp-action="Edit" asp-route-id="@tipo.Id" class="btn btn-sm btn-outline-secondary" title="Editar">
<i class="bi bi-pencil"></i>
</a>
@if (tipo.Activo)
{
<button type="button" class="btn btn-sm btn-outline-danger" onclick="confirmDelete(@tipo.Id)" title="Desactivar">
<i class="bi bi-x-circle"></i>
</button>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Delete Form -->
<form id="deleteForm" asp-action="Delete" method="post" style="display: none;">
<input type="hidden" name="id" id="deleteId" />
</form>
@section Scripts {
<script>
function confirmDelete(id) {
Swal.fire({
title: '¿Desactivar tipo?',
text: 'Este tipo de colaboración será desactivado.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Sí, desactivar',
cancelButtonText: 'Cancelar'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
});
}
@if (TempData["Success"] != null)
{
<text>
toastr.success('@TempData["Success"]');
</text>
}
@if (TempData["Error"] != null)
{
<text>
toastr.error('@TempData["Error"]');
</text>
}
</script>
}

View File

@@ -0,0 +1,63 @@
@model Rs_system.Models.Ubicacion
@{
ViewData["Title"] = "Nueva Ubicación";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Nueva Ubicación</h4>
<p class="text-muted mb-0">Registrar un nuevo lugar de almacenamiento</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="card-custom" style="max-width: 800px; margin: 0 auto;">
<div class="card-body">
<form asp-action="Create" method="post">
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="mb-3">
<label asp-for="Nombre" class="form-label">Nombre <span class="text-danger">*</span></label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Bodega Central, Auditorio, Oficina Pastoral" autofocus />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="2" placeholder="Detalles sobre la ubicación (opcional)"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Responsable" class="form-label">Responsable</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<input asp-for="Responsable" class="form-control" placeholder="Nombre de la persona encargada" />
</div>
<div class="form-text">Persona a cargo de esta ubicación (opcional).</div>
<span asp-validation-for="Responsable" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input asp-for="Activo" class="form-check-input" type="checkbox" role="switch" checked />
<label asp-for="Activo" class="form-check-label">Ubicación Activa</label>
</div>
<div class="form-text">Si está inactiva, no se podrá asignar a nuevos artículos.</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Guardar
</button>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,64 @@
@model Rs_system.Models.Ubicacion
@{
ViewData["Title"] = "Editar Ubicación";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Editar Ubicación</h4>
<p class="text-muted mb-0">Modificar información del lugar</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="card-custom" style="max-width: 800px; margin: 0 auto;">
<div class="card-body">
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="Id" />
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<div class="mb-3">
<label asp-for="Nombre" class="form-label">Nombre <span class="text-danger">*</span></label>
<input asp-for="Nombre" class="form-control" placeholder="Ej: Bodega Central, Auditorio" />
<span asp-validation-for="Nombre" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Descripcion" class="form-label">Descripción</label>
<textarea asp-for="Descripcion" class="form-control" rows="2" placeholder="Detalles sobre la ubicación (opcional)"></textarea>
<span asp-validation-for="Descripcion" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Responsable" class="form-label">Responsable</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<input asp-for="Responsable" class="form-control" placeholder="Nombre de la persona encargada" />
</div>
<div class="form-text">Persona a cargo de esta ubicación (opcional).</div>
<span asp-validation-for="Responsable" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input asp-for="Activo" class="form-check-input" type="checkbox" role="switch" />
<label asp-for="Activo" class="form-check-label">Ubicación Activa</label>
</div>
<div class="form-text">Si está inactiva, no se podrá asignar a nuevos artículos.</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<a asp-action="Index" class="btn btn-secondary">Cancelar</a>
<button type="submit" class="btn btn-primary-custom">
<i class="bi bi-save me-1"></i> Guardar Cambios
</button>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@@ -0,0 +1,131 @@
@model IEnumerable<Rs_system.Models.Ubicacion>
@{
ViewData["Title"] = "Ubicaciones de Inventario";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h4 class="mb-1">Ubicaciones de Inventario</h4>
<p class="text-muted mb-0">Gestión de lugares físicos de almacenamiento</p>
</div>
<a asp-action="Create" class="btn btn-primary-custom">
<i class="bi bi-plus-lg me-1"></i> Nueva Ubicación
</a>
</div>
<div class="card-custom">
<div class="table-responsive">
<table class="table-custom">
<thead>
<tr>
<th>Nombre</th>
<th>Descripción</th>
<th>Responsable</th>
<th class="text-center">Estado</th>
<th class="text-center">Acciones</th>
</tr>
</thead>
<tbody>
@if (!Model.Any())
{
<tr>
<td colspan="5" class="text-center text-muted py-4">
<i class="bi bi-geo-alt fs-1 d-block mb-2"></i>
No hay ubicaciones registradas
</td>
</tr>
}
@foreach (var item in Model)
{
<tr>
<td>
<strong>@item.Nombre</strong>
</td>
<td>
@if (!string.IsNullOrEmpty(item.Descripcion))
{
<span>@item.Descripcion</span>
}
else
{
<span class="text-muted">-</span>
}
</td>
<td>
@if (!string.IsNullOrEmpty(item.Responsable))
{
<div class="d-flex align-items-center">
<i class="bi bi-person-circle me-2 text-muted"></i>
<span>@item.Responsable</span>
</div>
}
else
{
<span class="text-muted">-</span>
}
</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">
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-secondary" title="Editar">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="confirmDelete(@item.Id)" title="Eliminar">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Delete Form -->
<form id="deleteForm" asp-action="Delete" method="post" style="display: none;">
<input type="hidden" name="id" id="deleteId" />
</form>
@section Scripts {
<script>
function confirmDelete(id) {
Swal.fire({
title: '¿Eliminar ubicación?',
text: 'Esta acción archivará la ubicación.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Sí, eliminar',
cancelButtonText: 'Cancelar'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
});
}
// Show success/error messages
@if (TempData["SuccessMessage"] != null)
{
<text>
toastr.success('@TempData["SuccessMessage"]');
</text>
}
@if (TempData["ErrorMessage"] != null)
{
<text>
toastr.error('@TempData["ErrorMessage"]');
</text>
}
</script>
}