add new
This commit is contained in:
161
RS_system/Controllers/ArticulosController.cs
Normal file
161
RS_system/Controllers/ArticulosController.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Rs_system.Models.ViewModels;
|
||||
using Rs_system.Services;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class ArticulosController : Controller
|
||||
{
|
||||
private readonly IArticuloService _service;
|
||||
|
||||
public ArticulosController(IArticuloService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
// GET: Articulos
|
||||
public async Task<IActionResult> Index(string? search, int? categoriaId, int? ubicacionId, int? estadoId)
|
||||
{
|
||||
// Load filter lists
|
||||
var categorias = await _service.GetCategoriasAsync();
|
||||
ViewBag.Categorias = new SelectList(categorias.Select(c => new { c.Id, c.Nombre }), "Id", "Nombre", categoriaId);
|
||||
|
||||
var ubicaciones = await _service.GetUbicacionesAsync();
|
||||
ViewBag.Ubicaciones = new SelectList(ubicaciones.Select(u => new { u.Id, u.Nombre }), "Id", "Nombre", ubicacionId);
|
||||
|
||||
// Custom Estado SelectList
|
||||
var estados = await _service.GetEstadosAsync();
|
||||
ViewBag.Estados = new SelectList(estados.Select(e => new { e.Id, e.Nombre }), "Id", "Nombre", estadoId);
|
||||
|
||||
// Keep Search params
|
||||
ViewBag.CurrentSearch = search ?? "";
|
||||
ViewBag.CurrentCategoria = categoriaId;
|
||||
ViewBag.CurrentUbicacion = ubicacionId;
|
||||
ViewBag.CurrentEstado = estadoId;
|
||||
|
||||
var list = await _service.GetAllAsync(search, categoriaId, ubicacionId, estadoId);
|
||||
return View(list);
|
||||
}
|
||||
|
||||
// GET: Articulos/Details/5
|
||||
public async Task<IActionResult> Details(int? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var articulo = await _service.GetByIdAsync(id.Value);
|
||||
if (articulo == null) return NotFound();
|
||||
|
||||
return View(articulo);
|
||||
}
|
||||
|
||||
// GET: Articulos/Create
|
||||
public async Task<IActionResult> Create()
|
||||
{
|
||||
await LoadDropdownsAsync();
|
||||
return View(new ArticuloViewModel());
|
||||
}
|
||||
|
||||
// POST: Articulos/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(ArticuloViewModel viewModel)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
if (await _service.ExistsCodigoAsync(viewModel.Codigo))
|
||||
{
|
||||
ModelState.AddModelError("Codigo", "Ya existe un artículo con este código.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var createdBy = User.Identity?.Name ?? "Sistema";
|
||||
var result = await _service.CreateAsync(viewModel, createdBy);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Artículo registrado exitosamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
ModelState.AddModelError("", "Ocurrió un error al guardar el artículo.");
|
||||
}
|
||||
}
|
||||
|
||||
await LoadDropdownsAsync();
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
// GET: Articulos/Edit/5
|
||||
public async Task<IActionResult> Edit(int? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var articulo = await _service.GetByIdAsync(id.Value);
|
||||
if (articulo == null) return NotFound();
|
||||
|
||||
await LoadDropdownsAsync();
|
||||
return View(articulo);
|
||||
}
|
||||
|
||||
// POST: Articulos/Edit/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(int id, ArticuloViewModel viewModel)
|
||||
{
|
||||
if (id != viewModel.Id) return NotFound();
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
if (await _service.ExistsCodigoAsync(viewModel.Codigo, id))
|
||||
{
|
||||
ModelState.AddModelError("Codigo", "Ya existe otro artículo con este código.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = await _service.UpdateAsync(viewModel);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Artículo actualizado exitosamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
ModelState.AddModelError("", "No se pudo actualizar el artículo.");
|
||||
}
|
||||
}
|
||||
|
||||
await LoadDropdownsAsync();
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
// POST: Articulos/Delete/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var result = await _service.DeleteAsync(id);
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Artículo eliminado exitosamente.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "No se pudo eliminado el artículo.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
private async Task LoadDropdownsAsync()
|
||||
{
|
||||
var categorias = await _service.GetCategoriasAsync();
|
||||
ViewBag.Categorias = new SelectList(categorias.Select(c => new { c.Id, c.Nombre }), "Id", "Nombre");
|
||||
|
||||
var ubicaciones = await _service.GetUbicacionesAsync();
|
||||
ViewBag.Ubicaciones = new SelectList(ubicaciones.Select(u => new { u.Id, u.Nombre }), "Id", "Nombre");
|
||||
|
||||
var estados = await _service.GetEstadosAsync();
|
||||
ViewBag.Estados = new SelectList(estados.Select(e => new { e.Id, e.Nombre }), "Id", "Nombre");
|
||||
}
|
||||
}
|
||||
125
RS_system/Controllers/CategoriasController.cs
Normal file
125
RS_system/Controllers/CategoriasController.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Services;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class CategoriasController : Controller
|
||||
{
|
||||
private readonly ICategoriaService _service;
|
||||
|
||||
public CategoriasController(ICategoriaService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
// GET: Categorias
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var list = await _service.GetAllAsync();
|
||||
return View(list);
|
||||
}
|
||||
|
||||
// GET: Categorias/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: Categorias/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create([Bind("Nombre,Descripcion,Activo")] Categoria categoria)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(categoria.Nombre))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "El nombre es obligatorio.");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
if (await _service.ExistsAsync(categoria.Nombre))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "Ya existe una categoría con ese nombre.");
|
||||
return View(categoria);
|
||||
}
|
||||
|
||||
categoria.CreadoPor = User.Identity?.Name ?? "Sistema";
|
||||
var result = await _service.CreateAsync(categoria);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Categoría creada exitosamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
ModelState.AddModelError("", "Ocurrió un error al guardar los datos.");
|
||||
}
|
||||
return View(categoria);
|
||||
}
|
||||
|
||||
// GET: Categorias/Edit/5
|
||||
public async Task<IActionResult> Edit(int? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var categoria = await _service.GetByIdAsync(id.Value);
|
||||
if (categoria == null) return NotFound();
|
||||
|
||||
return View(categoria);
|
||||
}
|
||||
|
||||
// POST: Categorias/Edit/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(int id, [Bind("Id,Nombre,Descripcion,Activo")] Categoria categoria)
|
||||
{
|
||||
if (id != categoria.Id) return NotFound();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(categoria.Nombre))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "El nombre es obligatorio.");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
if (await _service.ExistsAsync(categoria.Nombre, id))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "Ya existe otra categoría con ese nombre.");
|
||||
return View(categoria);
|
||||
}
|
||||
|
||||
var result = await _service.UpdateAsync(categoria);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Categoría actualizada exitosamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
ModelState.AddModelError("", "No se pudo actualizar la categoría o no fue encontrada.");
|
||||
}
|
||||
|
||||
return View(categoria);
|
||||
}
|
||||
|
||||
// POST: Categorias/Delete/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var result = await _service.DeleteAsync(id);
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Categoría eliminada exitosamente.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "No se pudo eliminar la categoría.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
221
RS_system/Controllers/ColaboracionController.cs
Normal file
221
RS_system/Controllers/ColaboracionController.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Rs_system.Models.ViewModels;
|
||||
using Rs_system.Services;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class ColaboracionController : Controller
|
||||
{
|
||||
private readonly IColaboracionService _colaboracionService;
|
||||
private readonly IMiembroService _miembroService;
|
||||
|
||||
public ColaboracionController(
|
||||
IColaboracionService colaboracionService,
|
||||
IMiembroService miembroService)
|
||||
{
|
||||
_colaboracionService = colaboracionService;
|
||||
_miembroService = miembroService;
|
||||
}
|
||||
|
||||
// GET: Colaboracion
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
try
|
||||
{
|
||||
var colaboraciones = await _colaboracionService.GetColaboracionesRecientesAsync();
|
||||
return View(colaboraciones);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = $"Error al cargar colaboraciones: {ex.Message}";
|
||||
return View(new List<Models.Colaboracion>());
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Colaboracion/Create
|
||||
public async Task<IActionResult> Create()
|
||||
{
|
||||
try
|
||||
{
|
||||
var viewModel = new RegistrarColaboracionViewModel
|
||||
{
|
||||
MesInicial = DateTime.Now.Month,
|
||||
AnioInicial = DateTime.Now.Year,
|
||||
MesFinal = DateTime.Now.Month,
|
||||
AnioFinal = DateTime.Now.Year,
|
||||
MontoTotal = 0,
|
||||
TiposDisponibles = await _colaboracionService.GetTiposActivosAsync()
|
||||
};
|
||||
|
||||
await CargarMiembrosAsync();
|
||||
return View(viewModel);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = $"Error al cargar formulario: {ex.Message}";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
// POST: Colaboracion/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(RegistrarColaboracionViewModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
try
|
||||
{
|
||||
var registradoPor = User.Identity?.Name ?? "Sistema";
|
||||
await _colaboracionService.RegistrarColaboracionAsync(model, registradoPor);
|
||||
|
||||
TempData["Success"] = "Colaboración registrada exitosamente";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError("", $"Error al registrar: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Recargar datos para la vista
|
||||
model.TiposDisponibles = await _colaboracionService.GetTiposActivosAsync();
|
||||
await CargarMiembrosAsync();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// GET: Colaboracion/Details/5
|
||||
public async Task<IActionResult> Details(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var colaboracion = await _colaboracionService.GetColaboracionByIdAsync(id);
|
||||
if (colaboracion == null)
|
||||
{
|
||||
TempData["Error"] = "Colaboración no encontrada";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
return View(colaboracion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = $"Error al cargar detalle: {ex.Message}";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Colaboracion/Reportes
|
||||
public IActionResult Reportes()
|
||||
{
|
||||
ViewBag.FechaInicio = DateTime.Now.Date;
|
||||
ViewBag.FechaFin = DateTime.Now.Date;
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: Colaboracion/GenerarReporte
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> GenerarReporte(DateTime fechaInicio, DateTime fechaFin)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ajustar fecha fin para incluir todo el día
|
||||
var fechaFinAjustada = fechaFin.Date.AddDays(1).AddSeconds(-1);
|
||||
|
||||
var reporte = await _colaboracionService.GenerarReportePorFechasAsync(
|
||||
fechaInicio.Date,
|
||||
fechaFinAjustada);
|
||||
|
||||
return View("Reporte", reporte);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = $"Error al generar reporte: {ex.Message}";
|
||||
return RedirectToAction(nameof(Reportes));
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Colaboracion/EstadoCuenta/5
|
||||
public async Task<IActionResult> EstadoCuenta(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var estado = await _colaboracionService.GenerarEstadoCuentaAsync(id);
|
||||
return View(estado);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = $"Error al generar estado de cuenta: {ex.Message}";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Colaboracion/BuscarMiembros?termino=juan
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> BuscarMiembros(string termino)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(termino) || termino.Length < 2)
|
||||
{
|
||||
return Json(new List<object>());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var miembros = await _miembroService.GetAllAsync();
|
||||
|
||||
var resultados = miembros
|
||||
.Where(m =>
|
||||
m.Nombres.Contains(termino, StringComparison.OrdinalIgnoreCase) ||
|
||||
m.Apellidos.Contains(termino, StringComparison.OrdinalIgnoreCase) ||
|
||||
$"{m.Nombres} {m.Apellidos}".Contains(termino, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(10)
|
||||
.Select(m => new
|
||||
{
|
||||
id = m.Id,
|
||||
text = $"{m.Nombres} {m.Apellidos}",
|
||||
telefono = m.Telefono
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Json(resultados);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new List<object>());
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Colaboracion/ObtenerUltimosPagos?miembroId=5
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ObtenerUltimosPagos(long miembroId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ultimosPagos = await _colaboracionService.GetUltimosPagosPorMiembroAsync(miembroId);
|
||||
return Json(ultimosPagos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new List<object>());
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private async Task CargarMiembrosAsync()
|
||||
{
|
||||
var miembros = await _miembroService.GetAllAsync();
|
||||
ViewBag.Miembros = new SelectList(
|
||||
miembros.Select(m => new
|
||||
{
|
||||
Id = m.Id,
|
||||
NombreCompleto = $"{m.Nombres} {m.Apellidos}"
|
||||
}),
|
||||
"Id",
|
||||
"NombreCompleto"
|
||||
);
|
||||
}
|
||||
}
|
||||
127
RS_system/Controllers/ContabilidadController.cs
Normal file
127
RS_system/Controllers/ContabilidadController.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Services;
|
||||
using Rs_system.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class ContabilidadController : Controller
|
||||
{
|
||||
private readonly IContabilidadService _contabilidadService;
|
||||
private readonly IMiembroService _miembroService;
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public ContabilidadController(IContabilidadService contabilidadService, IMiembroService miembroService, ApplicationDbContext context)
|
||||
{
|
||||
_contabilidadService = contabilidadService;
|
||||
_miembroService = miembroService;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index(long? grupoId)
|
||||
{
|
||||
var grupos = await _miembroService.GetGruposTrabajoAsync();
|
||||
ViewBag.Grupos = new SelectList(grupos.Select(g => new { g.Id, g.Nombre }), "Id", "Nombre", grupoId);
|
||||
|
||||
List<ReporteMensualContable> reportes = new();
|
||||
if (grupoId.HasValue)
|
||||
{
|
||||
reportes = await _contabilidadService.ListarReportesPorGrupoAsync(grupoId.Value);
|
||||
}
|
||||
|
||||
ViewBag.GrupoId = grupoId;
|
||||
return View(reportes);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> AbrirMes(long grupoId, int mes, int anio)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reporte = await _contabilidadService.ObtenerOCrearReporteMensualAsync(grupoId, mes, anio);
|
||||
TempData["Success"] = $"Reporte de {reporte.NombreMes} {anio} abierto correctamente.";
|
||||
return RedirectToAction(nameof(RegistroMensual), new { id = reporte.Id });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = "Error al abrir el mes: " + ex.Message;
|
||||
return RedirectToAction(nameof(Index), new { grupoId });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> RegistroMensual(long id)
|
||||
{
|
||||
var reporte = await _context.ReportesMensualesContables
|
||||
.Include(r => r.GrupoTrabajo)
|
||||
.Include(r => r.Registros)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
if (reporte == null) return NotFound();
|
||||
|
||||
ViewBag.SaldoActual = await _contabilidadService.CalcularSaldoActualAsync(id);
|
||||
return View(reporte);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> GuardarBulk([FromBody] BulkSaveRequest request)
|
||||
{
|
||||
if (request == null || request.ReporteId <= 0) return BadRequest("Solicitud inválida.");
|
||||
|
||||
var registros = request.Registros.Select(r => new ContabilidadRegistro
|
||||
{
|
||||
Id = r.Id,
|
||||
Tipo = r.Tipo,
|
||||
Monto = r.Monto,
|
||||
Fecha = DateTime.SpecifyKind(r.Fecha, DateTimeKind.Utc),
|
||||
Descripcion = r.Descripcion ?? ""
|
||||
}).ToList();
|
||||
|
||||
var success = await _contabilidadService.GuardarRegistrosBulkAsync(request.ReporteId, registros);
|
||||
if (success)
|
||||
{
|
||||
var nuevoSaldo = await _contabilidadService.CalcularSaldoActualAsync(request.ReporteId);
|
||||
return Json(new { success = true, saldo = nuevoSaldo });
|
||||
}
|
||||
|
||||
return Json(new { success = false, message = "Error al guardar los registros. Verifique que el mes no esté cerrado." });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CerrarMes(long id)
|
||||
{
|
||||
var success = await _contabilidadService.CerrarReporteAsync(id);
|
||||
if (success)
|
||||
{
|
||||
TempData["Success"] = "El reporte ha sido cerrado. Ya no se pueden realizar cambios.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["Error"] = "No se pudo cerrar el reporte.";
|
||||
}
|
||||
return RedirectToAction(nameof(RegistroMensual), new { id });
|
||||
}
|
||||
|
||||
// Helper classes for AJAX
|
||||
public class BulkSaveRequest
|
||||
{
|
||||
public long ReporteId { get; set; }
|
||||
public List<RegistroInput> Registros { get; set; } = new();
|
||||
}
|
||||
|
||||
public class RegistroInput
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public TipoMovimientoContable Tipo { get; set; }
|
||||
public decimal Monto { get; set; }
|
||||
public DateTime Fecha { get; set; }
|
||||
public string? Descripcion { get; set; }
|
||||
}
|
||||
}
|
||||
319
RS_system/Controllers/ContabilidadGeneralController.cs
Normal file
319
RS_system/Controllers/ContabilidadGeneralController.cs
Normal file
@@ -0,0 +1,319 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Services;
|
||||
using Rs_system.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class ContabilidadGeneralController : Controller
|
||||
{
|
||||
private readonly IContabilidadGeneralService _contabilidadService;
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IFileStorageService _fileStorageService;
|
||||
|
||||
public ContabilidadGeneralController(IContabilidadGeneralService contabilidadService, ApplicationDbContext context, IFileStorageService fileStorageService)
|
||||
{
|
||||
_contabilidadService = contabilidadService;
|
||||
_context = context;
|
||||
_fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
// ==================== Vista Principal ====================
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index(int? anio)
|
||||
{
|
||||
var anioActual = anio ?? DateTime.Now.Year;
|
||||
ViewBag.Anio = anioActual;
|
||||
|
||||
// Generar lista de años disponibles
|
||||
var anios = Enumerable.Range(DateTime.Now.Year - 5, 10).Reverse();
|
||||
ViewBag.Anios = new SelectList(anios);
|
||||
|
||||
var reportes = await _contabilidadService.ListarReportesAsync(anioActual);
|
||||
return View(reportes);
|
||||
}
|
||||
|
||||
// ==================== Abrir/Crear Reporte Mensual ====================
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> AbrirMes(int mes, int anio)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reporte = await _contabilidadService.ObtenerOCrearReporteMensualAsync(mes, anio);
|
||||
TempData["Success"] = $"Reporte de {reporte.NombreMes} {anio} abierto correctamente.";
|
||||
return RedirectToAction(nameof(RegistroMensual), new { id = reporte.Id });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = "Error al abrir el mes: " + ex.Message;
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Registro Mensual (Excel-like) ====================
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> RegistroMensual(long id)
|
||||
{
|
||||
var reporte = await _context.ReportesMensualesGenerales
|
||||
.Include(r => r.Movimientos)
|
||||
.ThenInclude(m => m.CategoriaIngreso)
|
||||
.Include(r => r.Movimientos)
|
||||
.ThenInclude(m => m.CategoriaEgreso)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
if (reporte == null) return NotFound();
|
||||
|
||||
ViewBag.SaldoActual = await _contabilidadService.CalcularSaldoActualAsync(id);
|
||||
ViewBag.CategoriasIngreso = await _contabilidadService.ObtenerCategoriasIngresoAsync();
|
||||
ViewBag.CategoriasEgreso = await _contabilidadService.ObtenerCategoriasEgresoAsync();
|
||||
|
||||
return View(reporte);
|
||||
}
|
||||
|
||||
// ==================== Guardar Movimientos Bulk (AJAX) ====================
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> GuardarBulk([FromBody] BulkSaveRequest request)
|
||||
{
|
||||
if (request == null || request.ReporteId <= 0)
|
||||
return BadRequest("Solicitud inválida.");
|
||||
|
||||
var movimientos = request.Movimientos.Select(m => new MovimientoGeneral
|
||||
{
|
||||
Id = m.Id,
|
||||
Tipo = m.Tipo,
|
||||
CategoriaIngresoId = m.CategoriaIngresoId,
|
||||
CategoriaEgresoId = m.CategoriaEgresoId,
|
||||
Monto = m.Monto,
|
||||
Fecha = DateTime.SpecifyKind(m.Fecha, DateTimeKind.Utc),
|
||||
Descripcion = m.Descripcion ?? "",
|
||||
NumeroComprobante = m.NumeroComprobante
|
||||
}).ToList();
|
||||
|
||||
var success = await _contabilidadService.GuardarMovimientosBulkAsync(request.ReporteId, movimientos);
|
||||
|
||||
if (success)
|
||||
{
|
||||
var nuevoSaldo = await _contabilidadService.CalcularSaldoActualAsync(request.ReporteId);
|
||||
return Json(new { success = true, saldo = nuevoSaldo });
|
||||
}
|
||||
|
||||
return Json(new { success = false, message = "Error al guardar los movimientos. Verifique que el mes no esté cerrado." });
|
||||
}
|
||||
|
||||
// ==================== Cerrar Mes ====================
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CerrarMes(long id)
|
||||
{
|
||||
var success = await _contabilidadService.CerrarReporteAsync(id);
|
||||
if (success)
|
||||
{
|
||||
TempData["Success"] = "El reporte ha sido cerrado. Ya no se pueden realizar cambios.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["Error"] = "No se pudo cerrar el reporte.";
|
||||
}
|
||||
return RedirectToAction(nameof(RegistroMensual), new { id });
|
||||
}
|
||||
|
||||
// ==================== Consolidado Mensual ====================
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Consolidado(long id)
|
||||
{
|
||||
var reporte = await _context.ReportesMensualesGenerales
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
if (reporte == null) return NotFound();
|
||||
|
||||
ViewBag.ConsolidadoIngresos = await _contabilidadService.ObtenerConsolidadoIngresosAsync(id);
|
||||
ViewBag.ConsolidadoEgresos = await _contabilidadService.ObtenerConsolidadoEgresosAsync(id);
|
||||
ViewBag.SaldoActual = await _contabilidadService.CalcularSaldoActualAsync(id);
|
||||
|
||||
return View(reporte);
|
||||
}
|
||||
|
||||
// ==================== Gestión de Categorías ====================
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GestionCategorias()
|
||||
{
|
||||
var categoriasIngreso = await _context.CategoriasIngreso
|
||||
.OrderBy(c => c.Nombre)
|
||||
.ToListAsync();
|
||||
|
||||
var categoriasEgreso = await _context.CategoriasEgreso
|
||||
.OrderBy(c => c.Nombre)
|
||||
.ToListAsync();
|
||||
|
||||
ViewBag.CategoriasIngreso = categoriasIngreso;
|
||||
ViewBag.CategoriasEgreso = categoriasEgreso;
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
// ==================== CRUD Categorías Ingreso ====================
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CrearCategoriaIngreso(CategoriaIngreso categoria)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _contabilidadService.CrearCategoriaIngresoAsync(categoria);
|
||||
TempData["Success"] = "Categoría de ingreso creada exitosamente.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["Error"] = "Error al crear la categoría.";
|
||||
}
|
||||
return RedirectToAction(nameof(GestionCategorias));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EditarCategoriaIngreso(CategoriaIngreso categoria)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var success = await _contabilidadService.ActualizarCategoriaIngresoAsync(categoria);
|
||||
TempData[success ? "Success" : "Error"] = success
|
||||
? "Categoría actualizada exitosamente."
|
||||
: "Error al actualizar la categoría.";
|
||||
}
|
||||
return RedirectToAction(nameof(GestionCategorias));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EliminarCategoriaIngreso(long id)
|
||||
{
|
||||
var success = await _contabilidadService.EliminarCategoriaIngresoAsync(id);
|
||||
TempData[success ? "Success" : "Error"] = success
|
||||
? "Categoría eliminada exitosamente."
|
||||
: "Error al eliminar la categoría.";
|
||||
return RedirectToAction(nameof(GestionCategorias));
|
||||
}
|
||||
|
||||
// ==================== CRUD Categorías Egreso ====================
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CrearCategoriaEgreso(CategoriaEgreso categoria)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
await _contabilidadService.CrearCategoriaEgresoAsync(categoria);
|
||||
TempData["Success"] = "Categoría de egreso creada exitosamente.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["Error"] = "Error al crear la categoría.";
|
||||
}
|
||||
return RedirectToAction(nameof(GestionCategorias));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EditarCategoriaEgreso(CategoriaEgreso categoria)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var success = await _contabilidadService.ActualizarCategoriaEgresoAsync(categoria);
|
||||
TempData[success ? "Success" : "Error"] = success
|
||||
? "Categoría actualizada exitosamente."
|
||||
: "Error al actualizar la categoría.";
|
||||
}
|
||||
return RedirectToAction(nameof(GestionCategorias));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EliminarCategoriaEgreso(long id)
|
||||
{
|
||||
var success = await _contabilidadService.EliminarCategoriaEgresoAsync(id);
|
||||
TempData[success ? "Success" : "Error"] = success
|
||||
? "Categoría eliminada exitosamente."
|
||||
: "Error al eliminar la categoría.";
|
||||
return RedirectToAction(nameof(GestionCategorias));
|
||||
}
|
||||
|
||||
// ==================== Helper Classes for AJAX ====================
|
||||
|
||||
public class BulkSaveRequest
|
||||
{
|
||||
public long ReporteId { get; set; }
|
||||
public List<MovimientoInput> Movimientos { get; set; } = new();
|
||||
}
|
||||
|
||||
public class MovimientoInput
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int Tipo { get; set; }
|
||||
public long? CategoriaIngresoId { get; set; }
|
||||
public long? CategoriaEgresoId { get; set; }
|
||||
public decimal Monto { get; set; }
|
||||
public DateTime Fecha { get; set; }
|
||||
public string? Descripcion { get; set; }
|
||||
public string? NumeroComprobante { get; set; }
|
||||
}
|
||||
|
||||
// ==================== Adjuntos ====================
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ObtenerAdjuntos(long movimientoId)
|
||||
{
|
||||
var adjuntos = await _contabilidadService.ObtenerAdjuntosMovimientoAsync(movimientoId);
|
||||
return Json(adjuntos.Select(a => new {
|
||||
id = a.Id,
|
||||
nombre = a.NombreArchivo,
|
||||
url = _fileStorageService.GetFileUrl(a.RutaArchivo),
|
||||
tipo = a.TipoContenido,
|
||||
fecha = a.FechaSubida.ToLocalTime().ToString("g")
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SubirAdjunto(long movimientoId, List<IFormFile> archivos)
|
||||
{
|
||||
if (movimientoId <= 0 || archivos == null || !archivos.Any())
|
||||
return BadRequest("Datos inválidos.");
|
||||
|
||||
int count = 0;
|
||||
foreach (var archivo in archivos)
|
||||
{
|
||||
if (archivo.Length > 0)
|
||||
{
|
||||
// El usuario solicitó guardar en uploads/miembros
|
||||
var ruta = await _fileStorageService.SaveFileAsync(archivo, "miembros");
|
||||
if (!string.IsNullOrEmpty(ruta))
|
||||
{
|
||||
await _contabilidadService.CrearAdjuntoAsync(movimientoId, archivo.FileName, ruta, archivo.ContentType);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Json(new { success = true, count = count, message = $"{count} archivos subidos correctamente." });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> EliminarAdjunto(long id)
|
||||
{
|
||||
// Primero obtener para borrar el archivo físico si es necesario (opcional, aquí solo borramos registro BD)
|
||||
// O idealmente el servicio se encarga. Por ahora solo borramos BD.
|
||||
var success = await _contabilidadService.EliminarAdjuntoAsync(id);
|
||||
return Json(new { success = success });
|
||||
}
|
||||
}
|
||||
125
RS_system/Controllers/EstadosController.cs
Normal file
125
RS_system/Controllers/EstadosController.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Services;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class EstadosController : Controller
|
||||
{
|
||||
private readonly IEstadoArticuloService _service;
|
||||
|
||||
public EstadosController(IEstadoArticuloService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
// GET: Estados
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var list = await _service.GetAllAsync();
|
||||
return View(list);
|
||||
}
|
||||
|
||||
// GET: Estados/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: Estados/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create([Bind("Nombre,Descripcion,Color,Activo")] EstadoArticulo estado)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(estado.Nombre))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "El nombre es obligatorio.");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
if (await _service.ExistsAsync(estado.Nombre))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "Ya existe un estado con ese nombre.");
|
||||
return View(estado);
|
||||
}
|
||||
|
||||
estado.CreadoPor = User.Identity?.Name ?? "Sistema";
|
||||
var result = await _service.CreateAsync(estado);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Estado creado exitosamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
ModelState.AddModelError("", "Ocurrió un error al guardar los datos.");
|
||||
}
|
||||
return View(estado);
|
||||
}
|
||||
|
||||
// GET: Estados/Edit/5
|
||||
public async Task<IActionResult> Edit(int? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var estado = await _service.GetByIdAsync(id.Value);
|
||||
if (estado == null) return NotFound();
|
||||
|
||||
return View(estado);
|
||||
}
|
||||
|
||||
// POST: Estados/Edit/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(int id, [Bind("Id,Nombre,Descripcion,Color,Activo")] EstadoArticulo estado)
|
||||
{
|
||||
if (id != estado.Id) return NotFound();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(estado.Nombre))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "El nombre es obligatorio.");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
if (await _service.ExistsAsync(estado.Nombre, id))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "Ya existe otro estado con ese nombre.");
|
||||
return View(estado);
|
||||
}
|
||||
|
||||
var result = await _service.UpdateAsync(estado);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Estado actualizado exitosamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
ModelState.AddModelError("", "No se pudo actualizar el estado o no fue encontrado.");
|
||||
}
|
||||
|
||||
return View(estado);
|
||||
}
|
||||
|
||||
// POST: Estados/Delete/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var result = await _service.DeleteAsync(id);
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Estado eliminado exitosamente.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "No se pudo eliminar el estado.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
190
RS_system/Controllers/MovimientosInventarioController.cs
Normal file
190
RS_system/Controllers/MovimientosInventarioController.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Services;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class MovimientosInventarioController : Controller
|
||||
{
|
||||
private readonly IMovimientoService _movimientoService;
|
||||
private readonly IArticuloService _articuloService;
|
||||
private readonly IUbicacionService _ubicacionService;
|
||||
private readonly IEstadoArticuloService _estadoService;
|
||||
private readonly IPrestamoService _prestamoService;
|
||||
|
||||
public MovimientosInventarioController(
|
||||
IMovimientoService movimientoService,
|
||||
IArticuloService articuloService,
|
||||
IUbicacionService ubicacionService,
|
||||
IEstadoArticuloService estadoService,
|
||||
IPrestamoService prestamoService)
|
||||
{
|
||||
_movimientoService = movimientoService;
|
||||
_articuloService = articuloService;
|
||||
_ubicacionService = ubicacionService;
|
||||
_estadoService = estadoService;
|
||||
_prestamoService = prestamoService;
|
||||
}
|
||||
|
||||
// GET: MovimientosInventario
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var historial = await _movimientoService.GetHistorialGeneralAsync(50); // Limit 50 for performance
|
||||
return View(historial);
|
||||
}
|
||||
|
||||
// GET: MovimientosInventario/Create
|
||||
// This is the "Wizard" or "Action Selector"
|
||||
public async Task<IActionResult> Create(int? articuloId)
|
||||
{
|
||||
if (articuloId.HasValue)
|
||||
{
|
||||
var articulo = await _articuloService.GetByIdAsync(articuloId.Value);
|
||||
if (articulo == null) return NotFound();
|
||||
|
||||
ViewBag.ArticuloId = articulo.Id;
|
||||
ViewBag.ArticuloNombre = $"{articulo.Codigo} - {articulo.Nombre}";
|
||||
ViewBag.UbicacionActual = articulo.UbicacionNombre;
|
||||
ViewBag.EstadoActual = articulo.EstadoNombre;
|
||||
ViewBag.TipoControl = articulo.TipoControl; // "UNITARIO" or "LOTE"
|
||||
ViewBag.CantidadGlobal = articulo.CantidadGlobal; // For LOTE validation?
|
||||
}
|
||||
|
||||
ViewBag.Articulos = new SelectList((await _articuloService.GetAllAsync()).Select(x => new { x.Id, Nombre = $"{x.Codigo} - {x.Nombre}" }), "Id", "Nombre", articuloId);
|
||||
ViewBag.Ubicaciones = new SelectList(await _ubicacionService.GetAllAsync(), "Id", "Nombre");
|
||||
ViewBag.Estados = new SelectList(await _estadoService.GetAllAsync(), "Id", "Nombre");
|
||||
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: MovimientosInventario/RegistrarTraslado
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RegistrarTraslado(int articuloId, int nuevaUbicacionId, string observacion, int cantidad = 1)
|
||||
{
|
||||
var usuario = User.Identity?.Name ?? "Sistema";
|
||||
// Use the new Quantity-Aware method
|
||||
var result = await _movimientoService.RegistrarTrasladoCantidadAsync(articuloId, nuevaUbicacionId, cantidad, observacion, usuario);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Traslado registrado correctamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
TempData["ErrorMessage"] = "Error al registrar el traslado. Verifique stock o campos.";
|
||||
return RedirectToAction(nameof(Create), new { articuloId });
|
||||
}
|
||||
|
||||
// POST: MovimientosInventario/RegistrarBaja
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RegistrarBaja(int articuloId, string motivo, int cantidad = 1)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(motivo))
|
||||
{
|
||||
TempData["ErrorMessage"] = "Debe especificar un motivo para la baja.";
|
||||
return RedirectToAction(nameof(Create), new { articuloId });
|
||||
}
|
||||
|
||||
var usuario = User.Identity?.Name ?? "Sistema";
|
||||
var result = await _movimientoService.RegistrarBajaCantidadAsync(articuloId, cantidad, motivo, usuario);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Baja registrada correctamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
TempData["ErrorMessage"] = "Error al registrar la baja.";
|
||||
return RedirectToAction(nameof(Create), new { articuloId });
|
||||
}
|
||||
|
||||
// POST: MovimientosInventario/RegistrarCambioEstado
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RegistrarCambioEstado(int articuloId, int nuevoEstadoId, string observacion)
|
||||
{
|
||||
var usuario = User.Identity?.Name ?? "Sistema";
|
||||
var result = await _movimientoService.RegistrarCambioEstadoAsync(articuloId, nuevoEstadoId, observacion, usuario);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Cambio de estado registrado correctamento.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
TempData["ErrorMessage"] = "Error al registrar el cambio de estado. Verifique que el estado sea diferente al actual.";
|
||||
return RedirectToAction(nameof(Create), new { articuloId });
|
||||
}
|
||||
|
||||
// POST: MovimientosInventario/RegistrarPrestamo
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RegistrarPrestamo(int articuloId, int cantidad, string personaNombre, string personaIdentificacion, DateTime? fechaDevolucionEstimada, string observacion)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(personaNombre))
|
||||
{
|
||||
TempData["ErrorMessage"] = "Debe especificar el nombre de la persona a quien se presta el artículo.";
|
||||
return RedirectToAction(nameof(Create), new { articuloId });
|
||||
}
|
||||
|
||||
var usuario = User.Identity?.Name ?? "Sistema";
|
||||
var result = await _prestamoService.RegistrarPrestamoAsync(articuloId, cantidad, personaNombre, personaIdentificacion, fechaDevolucionEstimada, observacion, usuario);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Préstamo registrado correctamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
TempData["ErrorMessage"] = "Error al registrar el préstamo. Verifique stock disponible.";
|
||||
return RedirectToAction(nameof(Create), new { articuloId });
|
||||
}
|
||||
|
||||
// GET: MovimientosInventario/PrestamosActivos
|
||||
public async Task<IActionResult> PrestamosActivos()
|
||||
{
|
||||
var prestamosActivos = await _prestamoService.GetPrestamosActivosAsync();
|
||||
return View(prestamosActivos);
|
||||
}
|
||||
|
||||
// POST: MovimientosInventario/RegistrarDevolucion
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RegistrarDevolucion(long prestamoId, string observacion)
|
||||
{
|
||||
var usuario = User.Identity?.Name ?? "Sistema";
|
||||
var result = await _prestamoService.RegistrarDevolucionAsync(prestamoId, observacion, usuario);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Devolución registrada correctamente.";
|
||||
return RedirectToAction(nameof(PrestamosActivos));
|
||||
}
|
||||
|
||||
TempData["ErrorMessage"] = "Error al registrar la devolución.";
|
||||
return RedirectToAction(nameof(PrestamosActivos));
|
||||
}
|
||||
|
||||
// POST: MovimientosInventario/RegistrarEntrada
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RegistrarEntrada(int articuloId, int cantidad, string observacion)
|
||||
{
|
||||
var usuario = User.Identity?.Name ?? "Sistema";
|
||||
var result = await _movimientoService.RegistrarEntradaCantidadAsync(articuloId, cantidad, observacion, usuario);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Entrada de inventario registrada correctamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
TempData["ErrorMessage"] = "Error al registrar la entrada de inventario.";
|
||||
return RedirectToAction(nameof(Create), new { articuloId });
|
||||
}
|
||||
}
|
||||
168
RS_system/Controllers/TipoColaboracionController.cs
Normal file
168
RS_system/Controllers/TipoColaboracionController.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Services;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class TipoColaboracionController : Controller
|
||||
{
|
||||
private readonly IColaboracionService _colaboracionService;
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public TipoColaboracionController(IColaboracionService colaboracionService, ApplicationDbContext context)
|
||||
{
|
||||
_colaboracionService = colaboracionService;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// GET: TipoColaboracion
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tipos = await _context.TiposColaboracion
|
||||
.OrderBy(t => t.Orden)
|
||||
.ToListAsync();
|
||||
return View(tipos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = $"Error al cargar tipos: {ex.Message}";
|
||||
return View(new List<TipoColaboracion>());
|
||||
}
|
||||
}
|
||||
|
||||
// GET: TipoColaboracion/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
var model = new TipoColaboracion
|
||||
{
|
||||
MontoSugerido = 1.00m,
|
||||
Activo = true
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// POST: TipoColaboracion/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(TipoColaboracion model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
try
|
||||
{
|
||||
model.CreadoEn = DateTime.UtcNow;
|
||||
model.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.TiposColaboracion.Add(model);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
TempData["Success"] = "Tipo de colaboración creado exitosamente";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError("", $"Error al crear: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// GET: TipoColaboracion/Edit/5
|
||||
public async Task<IActionResult> Edit(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tipo = await _context.TiposColaboracion.FindAsync(id);
|
||||
if (tipo == null)
|
||||
{
|
||||
TempData["Error"] = "Tipo de colaboración no encontrado";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
return View(tipo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = $"Error al cargar tipo: {ex.Message}";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
// POST: TipoColaboracion/Edit/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(long id, TipoColaboracion model)
|
||||
{
|
||||
if (id != model.Id)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tipo = await _context.TiposColaboracion.FindAsync(id);
|
||||
if (tipo == null)
|
||||
{
|
||||
TempData["Error"] = "Tipo de colaboración no encontrado";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
tipo.Nombre = model.Nombre;
|
||||
tipo.Descripcion = model.Descripcion;
|
||||
tipo.MontoSugerido = model.MontoSugerido;
|
||||
tipo.Activo = model.Activo;
|
||||
tipo.Orden = model.Orden;
|
||||
tipo.ActualizadoEn = DateTime.UtcNow;
|
||||
_context.TiposColaboracion.Update(tipo);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
TempData["Success"] = "Tipo de colaboración actualizado exitosamente";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModelState.AddModelError("", $"Error al actualizar: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
// POST: TipoColaboracion/Delete/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tipo = await _context.TiposColaboracion.FindAsync(id);
|
||||
if (tipo == null)
|
||||
{
|
||||
TempData["Error"] = "Tipo de colaboración no encontrado";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
// Soft delete - just deactivate
|
||||
tipo.Activo = false;
|
||||
tipo.ActualizadoEn = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
TempData["Success"] = "Tipo de colaboración desactivado exitosamente";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData["Error"] = $"Error al desactivar: {ex.Message}";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
125
RS_system/Controllers/UbicacionesController.cs
Normal file
125
RS_system/Controllers/UbicacionesController.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Services;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class UbicacionesController : Controller
|
||||
{
|
||||
private readonly IUbicacionService _service;
|
||||
|
||||
public UbicacionesController(IUbicacionService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
// GET: Ubicaciones
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var list = await _service.GetAllAsync();
|
||||
return View(list);
|
||||
}
|
||||
|
||||
// GET: Ubicaciones/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: Ubicaciones/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create([Bind("Nombre,Descripcion,Responsable,Activo")] Ubicacion ubicacion)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ubicacion.Nombre))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "El nombre es obligatorio.");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
if (await _service.ExistsAsync(ubicacion.Nombre))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "Ya existe una ubicación con ese nombre.");
|
||||
return View(ubicacion);
|
||||
}
|
||||
|
||||
ubicacion.CreadoPor = User.Identity?.Name ?? "Sistema";
|
||||
var result = await _service.CreateAsync(ubicacion);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Ubicación creada exitosamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
ModelState.AddModelError("", "Ocurrió un error al guardar los datos.");
|
||||
}
|
||||
return View(ubicacion);
|
||||
}
|
||||
|
||||
// GET: Ubicaciones/Edit/5
|
||||
public async Task<IActionResult> Edit(int? id)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
var ubicacion = await _service.GetByIdAsync(id.Value);
|
||||
if (ubicacion == null) return NotFound();
|
||||
|
||||
return View(ubicacion);
|
||||
}
|
||||
|
||||
// POST: Ubicaciones/Edit/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(int id, [Bind("Id,Nombre,Descripcion,Responsable,Activo")] Ubicacion ubicacion)
|
||||
{
|
||||
if (id != ubicacion.Id) return NotFound();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ubicacion.Nombre))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "El nombre es obligatorio.");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
if (await _service.ExistsAsync(ubicacion.Nombre, id))
|
||||
{
|
||||
ModelState.AddModelError("Nombre", "Ya existe otra ubicación con ese nombre.");
|
||||
return View(ubicacion);
|
||||
}
|
||||
|
||||
var result = await _service.UpdateAsync(ubicacion);
|
||||
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Ubicación actualizada exitosamente.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
ModelState.AddModelError("", "No se pudo actualizar la ubicación o no fue encontrada.");
|
||||
}
|
||||
|
||||
return View(ubicacion);
|
||||
}
|
||||
|
||||
// POST: Ubicaciones/Delete/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var result = await _service.DeleteAsync(id);
|
||||
if (result)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Ubicación eliminada exitosamente.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "No se pudo eliminar la ubicación.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
1382
RS_system/Migrations/20260123043655_AddMonthlyAccounting.Designer.cs
generated
Normal file
1382
RS_system/Migrations/20260123043655_AddMonthlyAccounting.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
96
RS_system/Migrations/20260123043655_AddMonthlyAccounting.cs
Normal file
96
RS_system/Migrations/20260123043655_AddMonthlyAccounting.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RS_system.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMonthlyAccounting : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "reportes_mensuales_contables",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
grupo_trabajo_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
mes = table.Column<int>(type: "integer", nullable: false),
|
||||
anio = table.Column<int>(type: "integer", nullable: false),
|
||||
saldo_inicial = table.Column<decimal>(type: "numeric(18,2)", nullable: false),
|
||||
fecha_creacion = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
cerrado = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_reportes_mensuales_contables", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_reportes_mensuales_contables_grupos_trabajo_grupo_trabajo_id",
|
||||
column: x => x.grupo_trabajo_id,
|
||||
principalTable: "grupos_trabajo",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "contabilidad_registros",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
reporte_mensual_id = table.Column<long>(type: "bigint", nullable: true),
|
||||
grupo_trabajo_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
tipo = table.Column<int>(type: "integer", nullable: false),
|
||||
monto = table.Column<decimal>(type: "numeric(18,2)", nullable: false),
|
||||
fecha = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
descripcion = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_contabilidad_registros", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_contabilidad_registros_grupos_trabajo_grupo_trabajo_id",
|
||||
column: x => x.grupo_trabajo_id,
|
||||
principalTable: "grupos_trabajo",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_contabilidad_registros_reportes_mensuales_contables_reporte_mensual_id",
|
||||
column: x => x.reporte_mensual_id,
|
||||
principalTable: "reportes_mensuales_contables",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_contabilidad_registros_grupo_trabajo_id",
|
||||
table: "contabilidad_registros",
|
||||
column: "grupo_trabajo_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_contabilidad_registros_reporte_mensual_id",
|
||||
table: "contabilidad_registros",
|
||||
column: "reporte_mensual_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_reportes_mensuales_contables_grupo_trabajo_id",
|
||||
table: "reportes_mensuales_contables",
|
||||
column: "grupo_trabajo_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "contabilidad_registros");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "reportes_mensuales_contables");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
143
RS_system/Migrations/AddPrestamosFunctionality.sql
Normal file
143
RS_system/Migrations/AddPrestamosFunctionality.sql
Normal file
@@ -0,0 +1,143 @@
|
||||
-- SQL para agregar funcionalidad de préstamos al sistema de inventario
|
||||
-- PostgreSQL
|
||||
|
||||
-- 1. Agregar el nuevo tipo de movimiento PRESTAMO al enum existente
|
||||
-- Nota: PostgreSQL no tiene ALTER ENUM, así que necesitamos recrear el enum
|
||||
-- Primero creamos el nuevo enum
|
||||
|
||||
CREATE TYPE tipo_movimiento_new AS ENUM (
|
||||
'ENTRADA',
|
||||
'SALIDA',
|
||||
'TRASLADO',
|
||||
'BAJA',
|
||||
'REPARACION',
|
||||
'AJUSTE',
|
||||
'CAMBIO_ESTADO',
|
||||
'PRESTAMO'
|
||||
);
|
||||
|
||||
-- Convertimos los datos existentes al nuevo enum
|
||||
ALTER TABLE movimientos_inventario
|
||||
ALTER COLUMN tipo_movimiento TYPE tipo_movimiento_new
|
||||
USING tipo_movimiento::text::tipo_movimiento_new;
|
||||
|
||||
-- Eliminamos el enum viejo y renombramos el nuevo
|
||||
DROP TYPE tipo_movimiento;
|
||||
ALTER TYPE tipo_movimiento_new RENAME TO tipo_movimiento;
|
||||
|
||||
-- 2. Crear tabla de préstamos
|
||||
CREATE TABLE prestamos (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
articulo_id INTEGER NOT NULL,
|
||||
cantidad INTEGER NOT NULL,
|
||||
persona_nombre VARCHAR(200) NOT NULL,
|
||||
persona_identificacion VARCHAR(50),
|
||||
fecha_prestamo TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
fecha_devolucion_estimada TIMESTAMP WITH TIME ZONE,
|
||||
fecha_devolucion_real TIMESTAMP WITH TIME ZONE,
|
||||
estado VARCHAR(20) NOT NULL DEFAULT 'ACTIVO',
|
||||
observacion VARCHAR(500),
|
||||
usuario_id VARCHAR(100),
|
||||
CONSTRAINT fk_prestamos_articulos FOREIGN KEY (articulo_id) REFERENCES articulos(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 3. Crear tabla de detalles de préstamo (para códigos individuales)
|
||||
CREATE TABLE prestamo_detalles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
prestamo_id BIGINT NOT NULL,
|
||||
codigo_articulo_individual VARCHAR(100) NOT NULL,
|
||||
estado VARCHAR(20) NOT NULL DEFAULT 'PRESTADO',
|
||||
fecha_devolucion TIMESTAMP WITH TIME ZONE,
|
||||
observacion VARCHAR(300),
|
||||
CONSTRAINT fk_prestamo_detalles_prestamo FOREIGN KEY (prestamo_id) REFERENCES prestamos(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 4. Crear índices para mejor rendimiento
|
||||
CREATE INDEX idx_prestamos_articulo_id ON prestamos(articulo_id);
|
||||
CREATE INDEX idx_prestamos_estado ON prestamos(estado);
|
||||
CREATE INDEX idx_prestamos_fecha_prestamo ON prestamos(fecha_prestamo);
|
||||
CREATE INDEX idx_prestamo_detalles_prestamo_id ON prestamo_detalles(prestamo_id);
|
||||
CREATE INDEX idx_prestamo_detalles_codigo ON prestamo_detalles(codigo_articulo_individual);
|
||||
|
||||
-- 5. Crear función para generar códigos individuales automáticamente
|
||||
CREATE OR REPLACE FUNCTION generar_codigo_individual(p_codigo_base VARCHAR, p_secuencia INTEGER)
|
||||
RETURNS VARCHAR AS $$
|
||||
BEGIN
|
||||
RETURN p_codigo_base || '-' || LPAD(p_secuencia::TEXT, 3, '0');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 6. Crear trigger para actualizar estado de préstamo cuando todos los detalles están devueltos
|
||||
CREATE OR REPLACE FUNCTION actualizar_estado_prestamo()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Si se actualiza un detalle a DEVUELTO, verificar si todos están devueltos
|
||||
IF NEW.estado = 'DEVUELTO' THEN
|
||||
UPDATE prestamos
|
||||
SET estado = CASE
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1 FROM prestamo_detalles
|
||||
WHERE prestamo_id = NEW.prestamo_id AND estado != 'DEVUELTO'
|
||||
) THEN 'DEVUELTO'
|
||||
ELSE estado
|
||||
END,
|
||||
fecha_devolucion_real = CASE
|
||||
WHEN NOT EXISTS (
|
||||
SELECT 1 FROM prestamo_detalles
|
||||
WHERE prestamo_id = NEW.prestamo_id AND estado != 'DEVUELTO'
|
||||
) THEN CURRENT_TIMESTAMP
|
||||
ELSE fecha_devolucion_real
|
||||
END
|
||||
WHERE id = NEW.prestamo_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 7. Crear el trigger
|
||||
CREATE TRIGGER trigger_actualizar_estado_prestamo
|
||||
AFTER UPDATE ON prestamo_detalles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION actualizar_estado_prestamo();
|
||||
|
||||
-- 8. Insertar datos de ejemplo (opcional)
|
||||
-- INSERT INTO prestamos (articulo_id, cantidad, persona_nombre, persona_identificacion, fecha_devolucion_estimada, observacion, usuario_id)
|
||||
-- VALUES (1, 2, 'Juan Pérez', '12345678', CURRENT_TIMESTAMP + INTERVAL '7 days', 'Préstamo para evento especial', 'admin');
|
||||
|
||||
-- Generar códigos individuales para el préstamo de ejemplo
|
||||
-- INSERT INTO prestamo_detalles (prestamo_id, codigo_articulo_individual, estado)
|
||||
-- VALUES
|
||||
-- (1, generar_codigo_individual('sp-b20', 1), 'PRESTADO'),
|
||||
-- (1, generar_codigo_individual('sp-b20', 2), 'PRESTADO');
|
||||
|
||||
-- 9. Crear vista para préstamos activos con detalles
|
||||
CREATE OR REPLACE VIEW vista_prestamos_activos AS
|
||||
SELECT
|
||||
p.id,
|
||||
p.articulo_id,
|
||||
a.codigo as articulo_codigo,
|
||||
a.nombre as articulo_nombre,
|
||||
p.cantidad,
|
||||
p.persona_nombre,
|
||||
p.persona_identificacion,
|
||||
p.fecha_prestamo,
|
||||
p.fecha_devolucion_estimada,
|
||||
p.estado,
|
||||
p.observacion,
|
||||
p.usuario_id,
|
||||
COUNT(pd.id) as detalles_devueltos,
|
||||
(p.cantidad - COUNT(pd.id)) as detalles_pendientes
|
||||
FROM prestamos p
|
||||
LEFT JOIN articulos a ON p.articulo_id = a.id
|
||||
LEFT JOIN prestamo_detalles pd ON p.id = pd.prestamo_id AND pd.estado = 'DEVUELTO'
|
||||
WHERE p.estado = 'ACTIVO'
|
||||
GROUP BY p.id, p.articulo_id, a.codigo, a.nombre, p.cantidad, p.persona_nombre, p.persona_identificacion, p.fecha_prestamo, p.fecha_devolucion_estimada, p.estado, p.observacion, p.usuario_id;
|
||||
|
||||
-- 10. Conceder permisos (ajustar según tu usuario de base de datos)
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON prestamos TO tu_usuario;
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON prestamo_detalles TO tu_usuario;
|
||||
-- GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO tu_usuario;
|
||||
-- GRANT EXECUTE ON FUNCTION generar_codigo_individual TO tu_usuario;
|
||||
-- GRANT EXECUTE ON FUNCTION actualizar_estado_prestamo TO tu_usuario;
|
||||
-- GRANT SELECT ON vista_prestamos_activos TO tu_usuario;
|
||||
1379
RS_system/Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
1379
RS_system/Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
File diff suppressed because it is too large
Load Diff
0
RS_system/Migrations/init_sql.sql
Normal file
0
RS_system/Migrations/init_sql.sql
Normal file
67
RS_system/Migrations/sql_articulos.sql
Normal file
67
RS_system/Migrations/sql_articulos.sql
Normal file
@@ -0,0 +1,67 @@
|
||||
-- Table: public.articulos
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.articulos
|
||||
(
|
||||
id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
||||
codigo character varying(50) COLLATE pg_catalog."default" NOT NULL,
|
||||
nombre character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
descripcion character varying(500) COLLATE pg_catalog."default",
|
||||
marca character varying(100) COLLATE pg_catalog."default",
|
||||
modelo character varying(100) COLLATE pg_catalog."default",
|
||||
numero_serie character varying(100) COLLATE pg_catalog."default",
|
||||
precio numeric(10,2) NOT NULL DEFAULT 0,
|
||||
fecha_adquisicion date,
|
||||
imagen_url text COLLATE pg_catalog."default",
|
||||
categoria_id integer NOT NULL,
|
||||
estado_id integer NOT NULL,
|
||||
ubicacion_id integer NOT NULL,
|
||||
activo boolean NOT NULL DEFAULT true,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
creado_en timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
actualizado_en timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
creado_por character varying(100) COLLATE pg_catalog."default",
|
||||
CONSTRAINT articulos_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT fk_articulos_categorias FOREIGN KEY (categoria_id)
|
||||
REFERENCES public.categorias (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_articulos_estados FOREIGN KEY (estado_id)
|
||||
REFERENCES public.estados_articulos (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_articulos_ubicaciones FOREIGN KEY (ubicacion_id)
|
||||
REFERENCES public.ubicaciones (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE RESTRICT
|
||||
)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.articulos
|
||||
OWNER to postgres;
|
||||
|
||||
-- Indexes
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ix_articulos_codigo
|
||||
ON public.articulos USING btree
|
||||
(codigo COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_articulos_nombre
|
||||
ON public.articulos USING btree
|
||||
(nombre COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_articulos_categoria_id
|
||||
ON public.articulos USING btree
|
||||
(categoria_id ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_articulos_estado_id
|
||||
ON public.articulos USING btree
|
||||
(estado_id ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_articulos_ubicacion_id
|
||||
ON public.articulos USING btree
|
||||
(ubicacion_id ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
27
RS_system/Migrations/sql_categorias.sql
Normal file
27
RS_system/Migrations/sql_categorias.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Table: public.categorias
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.categorias
|
||||
(
|
||||
id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
||||
nombre character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
descripcion character varying(500) COLLATE pg_catalog."default",
|
||||
activo boolean NOT NULL DEFAULT true,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
creado_en timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
actualizado_en timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
creado_por character varying(100) COLLATE pg_catalog."default",
|
||||
CONSTRAINT categorias_pkey PRIMARY KEY (id)
|
||||
)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.categorias
|
||||
OWNER to postgres;
|
||||
|
||||
-- Index: ix_categorias_nombre
|
||||
|
||||
-- DROP INDEX IF EXISTS public.ix_categorias_nombre;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_categorias_nombre
|
||||
ON public.categorias USING btree
|
||||
(nombre COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
28
RS_system/Migrations/sql_estados.sql
Normal file
28
RS_system/Migrations/sql_estados.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- Table: public.estados_articulos
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.estados_articulos
|
||||
(
|
||||
id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
||||
nombre character varying(50) COLLATE pg_catalog."default" NOT NULL,
|
||||
descripcion character varying(200) COLLATE pg_catalog."default",
|
||||
color character varying(20) COLLATE pg_catalog."default" DEFAULT 'secondary'::character varying,
|
||||
activo boolean NOT NULL DEFAULT true,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
creado_en timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
actualizado_en timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
creado_por character varying(100) COLLATE pg_catalog."default",
|
||||
CONSTRAINT estados_articulos_pkey PRIMARY KEY (id)
|
||||
)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.estados_articulos
|
||||
OWNER to postgres;
|
||||
|
||||
-- Index: ix_estados_articulos_nombre
|
||||
|
||||
-- DROP INDEX IF EXISTS public.ix_estados_articulos_nombre;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_estados_articulos_nombre
|
||||
ON public.estados_articulos USING btree
|
||||
(nombre COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
52
RS_system/Migrations/sql_miembros.sql
Normal file
52
RS_system/Migrations/sql_miembros.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
-- =====================================================
|
||||
-- Script: Church Members Module Database Schema (Refactored)
|
||||
-- Description: Creates tables for work groups and members linked to personas
|
||||
-- =====================================================
|
||||
|
||||
-- Table: grupos_trabajo
|
||||
CREATE TABLE IF NOT EXISTS public.grupos_trabajo
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
nombre character varying(100) NOT NULL,
|
||||
descripcion text,
|
||||
activo boolean NOT NULL DEFAULT true,
|
||||
creado_en timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
actualizado_en timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT grupos_trabajo_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Table: miembros
|
||||
-- Drop if exists to handle the schema change during development
|
||||
DROP TABLE IF EXISTS public.miembros;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.miembros
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
persona_id bigint NOT NULL,
|
||||
bautizado_espiritu_santo boolean NOT NULL DEFAULT false,
|
||||
fecha_ingreso_congregacion date,
|
||||
telefono_emergencia character varying(20),
|
||||
grupo_trabajo_id bigint,
|
||||
activo boolean NOT NULL DEFAULT true,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
creado_en timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
actualizado_en timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
creado_por character varying(100),
|
||||
CONSTRAINT miembros_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT fk_miembros_persona FOREIGN KEY (persona_id)
|
||||
REFERENCES public.personas (id),
|
||||
CONSTRAINT fk_miembros_grupo_trabajo FOREIGN KEY (grupo_trabajo_id)
|
||||
REFERENCES public.grupos_trabajo (id)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_miembros_persona ON public.miembros(persona_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_miembros_grupo_trabajo ON public.miembros(grupo_trabajo_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_miembros_activo ON public.miembros(activo, eliminado);
|
||||
|
||||
-- Initial work groups data
|
||||
INSERT INTO public.grupos_trabajo (nombre, descripcion) VALUES
|
||||
('Concilio Misionero Femenil', 'Grupo de trabajo de mujeres misioneras'),
|
||||
('Fraternidad de Varones', 'Grupo de trabajo de varones de la iglesia'),
|
||||
('Embajadores de Cristo', 'Grupo de jóvenes embajadores')
|
||||
ON CONFLICT DO NOTHING;
|
||||
52
RS_system/Migrations/sql_movimientos.sql
Normal file
52
RS_system/Migrations/sql_movimientos.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
-- Table: public.movimientos_inventario
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.movimientos_inventario
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
|
||||
articulo_id integer NOT NULL,
|
||||
tipo_movimiento character varying(50) COLLATE pg_catalog."default" NOT NULL,
|
||||
fecha timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
ubicacion_origen_id integer,
|
||||
ubicacion_destino_id integer,
|
||||
estado_anterior_id integer,
|
||||
estado_nuevo_id integer,
|
||||
observacion character varying(500) COLLATE pg_catalog."default",
|
||||
usuario_id character varying(100) COLLATE pg_catalog."default",
|
||||
CONSTRAINT movimientos_inventario_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT fk_movimientos_articulos FOREIGN KEY (articulo_id)
|
||||
REFERENCES public.articulos (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_movimientos_origen FOREIGN KEY (ubicacion_origen_id)
|
||||
REFERENCES public.ubicaciones (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE SET NULL,
|
||||
CONSTRAINT fk_movimientos_destino FOREIGN KEY (ubicacion_destino_id)
|
||||
REFERENCES public.ubicaciones (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE SET NULL,
|
||||
CONSTRAINT fk_movimientos_estado_ant FOREIGN KEY (estado_anterior_id)
|
||||
REFERENCES public.estados_articulos (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE SET NULL,
|
||||
CONSTRAINT fk_movimientos_estado_new FOREIGN KEY (estado_nuevo_id)
|
||||
REFERENCES public.estados_articulos (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE SET NULL
|
||||
)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.movimientos_inventario
|
||||
OWNER to postgres;
|
||||
|
||||
-- Indexes
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_movimientos_articulo_id
|
||||
ON public.movimientos_inventario USING btree
|
||||
(articulo_id ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_movimientos_fecha
|
||||
ON public.movimientos_inventario USING btree
|
||||
(fecha DESC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
106
RS_system/Migrations/sql_ofrendas.sql
Normal file
106
RS_system/Migrations/sql_ofrendas.sql
Normal file
@@ -0,0 +1,106 @@
|
||||
-- =============================================
|
||||
-- Módulo de Ofrendas - Script SQL
|
||||
-- =============================================
|
||||
|
||||
-- Tabla: registros_culto
|
||||
-- Almacena los registros de ofrendas por culto
|
||||
CREATE TABLE IF NOT EXISTS public.registros_culto
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
|
||||
fecha date NOT NULL,
|
||||
observaciones character varying(500) COLLATE pg_catalog."default",
|
||||
creado_por character varying(100) COLLATE pg_catalog."default",
|
||||
creado_en timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
actualizado_en timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT registros_culto_pkey PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.registros_culto IS 'Registros de ofrendas por culto';
|
||||
|
||||
-- Índice para búsqueda por fecha
|
||||
CREATE INDEX IF NOT EXISTS idx_registros_culto_fecha ON public.registros_culto(fecha);
|
||||
CREATE INDEX IF NOT EXISTS idx_registros_culto_eliminado ON public.registros_culto(eliminado);
|
||||
|
||||
-- =============================================
|
||||
|
||||
-- Tabla: ofrendas
|
||||
-- Almacena las ofrendas individuales de cada registro
|
||||
CREATE TABLE IF NOT EXISTS public.ofrendas
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
|
||||
registro_culto_id bigint NOT NULL,
|
||||
monto numeric(10,2) NOT NULL CHECK (monto > 0),
|
||||
concepto character varying(200) COLLATE pg_catalog."default" NOT NULL,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT ofrendas_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT fk_ofrendas_registro_culto FOREIGN KEY (registro_culto_id)
|
||||
REFERENCES public.registros_culto (id) MATCH SIMPLE
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.ofrendas IS 'Ofrendas individuales de cada registro de culto';
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX IF NOT EXISTS idx_ofrendas_registro_culto_id ON public.ofrendas(registro_culto_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ofrendas_eliminado ON public.ofrendas(eliminado);
|
||||
|
||||
-- =============================================
|
||||
|
||||
-- Tabla: descuentos_ofrenda
|
||||
-- Almacena los descuentos aplicados a cada ofrenda
|
||||
CREATE TABLE IF NOT EXISTS public.descuentos_ofrenda
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
|
||||
ofrenda_id bigint NOT NULL,
|
||||
monto numeric(10,2) NOT NULL CHECK (monto > 0),
|
||||
concepto character varying(200) COLLATE pg_catalog."default" NOT NULL,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT descuentos_ofrenda_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT fk_descuentos_ofrenda_ofrenda FOREIGN KEY (ofrenda_id)
|
||||
REFERENCES public.ofrendas (id) MATCH SIMPLE
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.descuentos_ofrenda IS 'Descuentos aplicados a las ofrendas (diezmo, asignaciones, etc.)';
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX IF NOT EXISTS idx_descuentos_ofrenda_ofrenda_id ON public.descuentos_ofrenda(ofrenda_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_descuentos_ofrenda_eliminado ON public.descuentos_ofrenda(eliminado);
|
||||
|
||||
-- =============================================
|
||||
-- Permisos para el módulo
|
||||
-- =============================================
|
||||
|
||||
-- Primero crear el módulo si no existe
|
||||
INSERT INTO public.modulos (nombre, descripcion, icono, orden, activo)
|
||||
SELECT 'Secretaría', 'Módulo de secretaría de la iglesia', 'bi bi-journal-bookmark', 40, true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.modulos WHERE nombre = 'Secretaría');
|
||||
|
||||
-- Crear permisos para el controlador Ofrenda
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Ofrenda.Index', 'Ver Ofrendas', 'Permite ver el listado de registros de ofrendas',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Secretaría'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Ofrenda.Index');
|
||||
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Ofrenda.Create', 'Crear Ofrenda', 'Permite crear nuevos registros de ofrendas',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Secretaría'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Ofrenda.Create');
|
||||
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Ofrenda.Edit', 'Editar Ofrenda', 'Permite editar registros de ofrendas',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Secretaría'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Ofrenda.Edit');
|
||||
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Ofrenda.Delete', 'Eliminar Ofrenda', 'Permite eliminar registros de ofrendas',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Secretaría'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Ofrenda.Delete');
|
||||
|
||||
INSERT INTO public.permisos (codigo, nombre, descripcion, modulo_id, activo)
|
||||
SELECT 'Ofrenda.Details', 'Ver Detalle Ofrenda', 'Permite ver el detalle de un registro de ofrendas',
|
||||
(SELECT id FROM public.modulos WHERE nombre = 'Secretaría'), true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Ofrenda.Details');
|
||||
28
RS_system/Migrations/sql_ubicaciones.sql
Normal file
28
RS_system/Migrations/sql_ubicaciones.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- Table: public.ubicaciones
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.ubicaciones
|
||||
(
|
||||
id integer NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
||||
nombre character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
descripcion character varying(200) COLLATE pg_catalog."default",
|
||||
responsable character varying(100) COLLATE pg_catalog."default",
|
||||
activo boolean NOT NULL DEFAULT true,
|
||||
eliminado boolean NOT NULL DEFAULT false,
|
||||
creado_en timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
actualizado_en timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
creado_por character varying(100) COLLATE pg_catalog."default",
|
||||
CONSTRAINT ubicaciones_pkey PRIMARY KEY (id)
|
||||
)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.ubicaciones
|
||||
OWNER to postgres;
|
||||
|
||||
-- Index: ix_ubicaciones_nombre
|
||||
|
||||
-- DROP INDEX IF EXISTS public.ix_ubicaciones_nombre;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_ubicaciones_nombre
|
||||
ON public.ubicaciones USING btree
|
||||
(nombre COLLATE pg_catalog."default" ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
45
RS_system/Migrations/sql_upgrade_lotes.sql
Normal file
45
RS_system/Migrations/sql_upgrade_lotes.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- Add columns to articulos
|
||||
ALTER TABLE public.articulos
|
||||
ADD COLUMN IF NOT EXISTS tipo_control character varying(50) NOT NULL DEFAULT 'UNITARIO';
|
||||
|
||||
ALTER TABLE public.articulos
|
||||
ADD COLUMN IF NOT EXISTS cantidad_global integer NOT NULL DEFAULT 1;
|
||||
|
||||
-- Add columns to movimientos_inventario
|
||||
ALTER TABLE public.movimientos_inventario
|
||||
ADD COLUMN IF NOT EXISTS cantidad integer NOT NULL DEFAULT 1;
|
||||
|
||||
-- Create existencias table
|
||||
CREATE TABLE IF NOT EXISTS public.existencias
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1 ),
|
||||
articulo_id integer NOT NULL,
|
||||
ubicacion_id integer NOT NULL,
|
||||
cantidad integer NOT NULL DEFAULT 0,
|
||||
actualizado_en timestamp without time zone NOT NULL DEFAULT (now() AT TIME ZONE 'utc'::text),
|
||||
CONSTRAINT existencias_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT fk_existencias_articulo FOREIGN KEY (articulo_id)
|
||||
REFERENCES public.articulos (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_existencias_ubicacion FOREIGN KEY (ubicacion_id)
|
||||
REFERENCES public.ubicaciones (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT uq_existencias_articulo_ubicacion UNIQUE (articulo_id, ubicacion_id)
|
||||
)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.existencias
|
||||
OWNER to postgres;
|
||||
|
||||
-- Index for existencias
|
||||
CREATE INDEX IF NOT EXISTS ix_existencias_articulo_id
|
||||
ON public.existencias USING btree
|
||||
(articulo_id ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS ix_existencias_ubicacion_id
|
||||
ON public.existencias USING btree
|
||||
(ubicacion_id ASC NULLS LAST)
|
||||
TABLESPACE pg_default;
|
||||
101
RS_system/Models/Articulo.cs
Normal file
101
RS_system/Models/Articulo.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("articulos")]
|
||||
public class Articulo
|
||||
{
|
||||
public enum TipoControlInventario
|
||||
{
|
||||
UNITARIO, // 1 record = 1 physical item (Laptop, Projector)
|
||||
LOTE // 1 record = N items (Chairs, Cables)
|
||||
}
|
||||
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("tipo_control")]
|
||||
[Required]
|
||||
public string TipoControl { get; set; } = nameof(TipoControlInventario.UNITARIO);
|
||||
|
||||
[Column("cantidad_global")]
|
||||
public int CantidadGlobal { get; set; } = 1; // Cache/Total for LOTE. Always 1 for UNITARIO.
|
||||
|
||||
[Column("codigo")]
|
||||
[Required(ErrorMessage = "El código es obligatorio")]
|
||||
[StringLength(50, ErrorMessage = "El código no puede exceder los 50 caracteres")]
|
||||
public string Codigo { get; set; } = string.Empty;
|
||||
|
||||
[Column("nombre")]
|
||||
[Required(ErrorMessage = "El nombre es obligatorio")]
|
||||
[StringLength(100, ErrorMessage = "El nombre no puede exceder los 100 caracteres")]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(500, ErrorMessage = "La descripción no puede exceder los 500 caracteres")]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("marca")]
|
||||
[StringLength(100)]
|
||||
public string? Marca { get; set; }
|
||||
|
||||
[Column("modelo")]
|
||||
[StringLength(100)]
|
||||
public string? Modelo { get; set; }
|
||||
|
||||
[Column("numero_serie")]
|
||||
[StringLength(100)]
|
||||
public string? NumeroSerie { get; set; }
|
||||
|
||||
[Column("precio")]
|
||||
[Range(0, 99999999.99)]
|
||||
public decimal Precio { get; set; } = 0;
|
||||
|
||||
[Column("fecha_adquisicion")]
|
||||
public DateOnly? FechaAdquisicion { get; set; }
|
||||
|
||||
[Column("imagen_url")]
|
||||
public string? ImagenUrl { get; set; }
|
||||
|
||||
// Foreign Keys
|
||||
|
||||
[Column("categoria_id")]
|
||||
public int CategoriaId { get; set; }
|
||||
|
||||
[Column("estado_id")]
|
||||
public int EstadoId { get; set; }
|
||||
|
||||
[Column("ubicacion_id")]
|
||||
public int UbicacionId { get; set; }
|
||||
|
||||
// Audit & Control
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("eliminado")]
|
||||
public bool Eliminado { get; set; } = false;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("creado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CreadoPor { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
|
||||
[ForeignKey("CategoriaId")]
|
||||
public virtual Categoria? Categoria { get; set; }
|
||||
|
||||
[ForeignKey("EstadoId")]
|
||||
public virtual EstadoArticulo? Estado { get; set; }
|
||||
|
||||
[ForeignKey("UbicacionId")]
|
||||
public virtual Ubicacion? Ubicacion { get; set; }
|
||||
}
|
||||
37
RS_system/Models/Categoria.cs
Normal file
37
RS_system/Models/Categoria.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("categorias")]
|
||||
public class Categoria
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("nombre")]
|
||||
[Required(ErrorMessage = "El nombre es obligatorio")]
|
||||
[StringLength(100, ErrorMessage = "El nombre no puede exceder los 100 caracteres")]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(500, ErrorMessage = "La descripción no puede exceder los 500 caracteres")]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("eliminado")]
|
||||
public bool Eliminado { get; set; } = false;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("creado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CreadoPor { get; set; }
|
||||
}
|
||||
30
RS_system/Models/CategoriaEgreso.cs
Normal file
30
RS_system/Models/CategoriaEgreso.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("categorias_egreso")]
|
||||
public class CategoriaEgreso
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("nombre")]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(255)]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("activa")]
|
||||
public bool Activa { get; set; } = true;
|
||||
|
||||
[Column("fecha_creacion")]
|
||||
public DateTime FechaCreacion { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation property
|
||||
public virtual ICollection<MovimientoGeneral> Movimientos { get; set; } = new List<MovimientoGeneral>();
|
||||
}
|
||||
30
RS_system/Models/CategoriaIngreso.cs
Normal file
30
RS_system/Models/CategoriaIngreso.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("categorias_ingreso")]
|
||||
public class CategoriaIngreso
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("nombre")]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(255)]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("activa")]
|
||||
public bool Activa { get; set; } = true;
|
||||
|
||||
[Column("fecha_creacion")]
|
||||
public DateTime FechaCreacion { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation property
|
||||
public virtual ICollection<MovimientoGeneral> Movimientos { get; set; } = new List<MovimientoGeneral>();
|
||||
}
|
||||
42
RS_system/Models/Colaboracion.cs
Normal file
42
RS_system/Models/Colaboracion.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("colaboraciones", Schema = "public")]
|
||||
public class Colaboracion
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("miembro_id")]
|
||||
public long MiembroId { get; set; }
|
||||
|
||||
[Column("fecha_registro")]
|
||||
public DateTime FechaRegistro { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Required]
|
||||
[Column("monto_total")]
|
||||
public decimal MontoTotal { get; set; }
|
||||
|
||||
[Column("observaciones")]
|
||||
public string? Observaciones { get; set; }
|
||||
|
||||
[MaxLength(100)]
|
||||
[Column("registrado_por")]
|
||||
public string? RegistradoPor { get; set; }
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation properties
|
||||
[ForeignKey("MiembroId")]
|
||||
public Miembro Miembro { get; set; } = null!;
|
||||
|
||||
public ICollection<DetalleColaboracion> Detalles { get; set; } = new List<DetalleColaboracion>();
|
||||
}
|
||||
49
RS_system/Models/ContabilidadRegistro.cs
Normal file
49
RS_system/Models/ContabilidadRegistro.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
public enum TipoMovimientoContable
|
||||
{
|
||||
Ingreso,
|
||||
Egreso
|
||||
}
|
||||
|
||||
[Table("contabilidad_registros")]
|
||||
public class ContabilidadRegistro
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("reporte_mensual_id")]
|
||||
public long? ReporteMensualId { get; set; }
|
||||
|
||||
[ForeignKey("ReporteMensualId")]
|
||||
public virtual ReporteMensualContable? ReporteMensual { get; set; }
|
||||
|
||||
[Column("grupo_trabajo_id")]
|
||||
[Required]
|
||||
public long GrupoTrabajoId { get; set; }
|
||||
|
||||
[ForeignKey("GrupoTrabajoId")]
|
||||
public virtual GrupoTrabajo GrupoTrabajo { get; set; }
|
||||
|
||||
[Column("tipo")]
|
||||
[Required]
|
||||
public TipoMovimientoContable Tipo { get; set; }
|
||||
|
||||
[Column("monto", TypeName = "decimal(18,2)")]
|
||||
[Required]
|
||||
public decimal Monto { get; set; }
|
||||
|
||||
|
||||
[Column("fecha")]
|
||||
[Required]
|
||||
public DateTime Fecha { get; set; }
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(200)]
|
||||
public string Descripcion { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
44
RS_system/Models/DetalleColaboracion.cs
Normal file
44
RS_system/Models/DetalleColaboracion.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("detalle_colaboraciones", Schema = "public")]
|
||||
public class DetalleColaboracion
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("colaboracion_id")]
|
||||
public long ColaboracionId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("tipo_colaboracion_id")]
|
||||
public long TipoColaboracionId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Range(1, 12)]
|
||||
[Column("mes")]
|
||||
public int Mes { get; set; }
|
||||
|
||||
[Required]
|
||||
[Range(2000, 2100)]
|
||||
[Column("anio")]
|
||||
public int Anio { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("monto")]
|
||||
public decimal Monto { get; set; }
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation properties
|
||||
[ForeignKey("ColaboracionId")]
|
||||
public Colaboracion Colaboracion { get; set; } = null!;
|
||||
|
||||
[ForeignKey("TipoColaboracionId")]
|
||||
public TipoColaboracion TipoColaboracion { get; set; } = null!;
|
||||
}
|
||||
41
RS_system/Models/EstadoArticulo.cs
Normal file
41
RS_system/Models/EstadoArticulo.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("estados_articulos")]
|
||||
public class EstadoArticulo
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("nombre")]
|
||||
[Required(ErrorMessage = "El nombre es obligatorio")]
|
||||
[StringLength(50, ErrorMessage = "El nombre no puede exceder los 50 caracteres")]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(200, ErrorMessage = "La descripción no puede exceder los 200 caracteres")]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("color")]
|
||||
[StringLength(20)]
|
||||
public string? Color { get; set; } = "secondary"; // success, warning, danger, info, primary, secondary
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("eliminado")]
|
||||
public bool Eliminado { get; set; } = false;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("creado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CreadoPor { get; set; }
|
||||
}
|
||||
31
RS_system/Models/Existencia.cs
Normal file
31
RS_system/Models/Existencia.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("existencias")]
|
||||
public class Existencia
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("articulo_id")]
|
||||
public int ArticuloId { get; set; }
|
||||
|
||||
[Column("ubicacion_id")]
|
||||
public int UbicacionId { get; set; }
|
||||
|
||||
[Column("cantidad")]
|
||||
public int Cantidad { get; set; } = 0;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
[ForeignKey("ArticuloId")]
|
||||
public virtual Articulo? Articulo { get; set; }
|
||||
|
||||
[ForeignKey("UbicacionId")]
|
||||
public virtual Ubicacion? Ubicacion { get; set; }
|
||||
}
|
||||
59
RS_system/Models/MovimientoGeneral.cs
Normal file
59
RS_system/Models/MovimientoGeneral.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
public enum TipoMovimientoGeneral
|
||||
{
|
||||
Ingreso = 1,
|
||||
Egreso = 2
|
||||
}
|
||||
|
||||
[Table("movimientos_generales")]
|
||||
public class MovimientoGeneral
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("reporte_mensual_general_id")]
|
||||
public long? ReporteMensualGeneralId { get; set; }
|
||||
|
||||
[ForeignKey("ReporteMensualGeneralId")]
|
||||
public virtual ReporteMensualGeneral? ReporteMensualGeneral { get; set; }
|
||||
|
||||
[Column("tipo")]
|
||||
[Required]
|
||||
public int Tipo { get; set; }
|
||||
|
||||
[Column("categoria_ingreso_id")]
|
||||
public long? CategoriaIngresoId { get; set; }
|
||||
|
||||
[ForeignKey("CategoriaIngresoId")]
|
||||
public virtual CategoriaIngreso? CategoriaIngreso { get; set; }
|
||||
|
||||
[Column("categoria_egreso_id")]
|
||||
public long? CategoriaEgresoId { get; set; }
|
||||
|
||||
[ForeignKey("CategoriaEgresoId")]
|
||||
public virtual CategoriaEgreso? CategoriaEgreso { get; set; }
|
||||
|
||||
[Column("monto", TypeName = "decimal(18,2)")]
|
||||
[Required]
|
||||
public decimal Monto { get; set; }
|
||||
|
||||
[Column("fecha")]
|
||||
[Required]
|
||||
public DateTime Fecha { get; set; }
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(200)]
|
||||
public string Descripcion { get; set; } = string.Empty;
|
||||
|
||||
[Column("numero_comprobante")]
|
||||
[StringLength(50)]
|
||||
public string? NumeroComprobante { get; set; }
|
||||
|
||||
// Navigation property
|
||||
public virtual ICollection<MovimientoGeneralAdjunto> Adjuntos { get; set; } = new List<MovimientoGeneralAdjunto>();
|
||||
}
|
||||
36
RS_system/Models/MovimientoGeneralAdjunto.cs
Normal file
36
RS_system/Models/MovimientoGeneralAdjunto.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("movimientos_generales_adjuntos")]
|
||||
public class MovimientoGeneralAdjunto
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("movimiento_general_id")]
|
||||
[Required]
|
||||
public long MovimientoGeneralId { get; set; }
|
||||
|
||||
[ForeignKey("MovimientoGeneralId")]
|
||||
public virtual MovimientoGeneral MovimientoGeneral { get; set; }
|
||||
|
||||
[Column("nombre_archivo")]
|
||||
[Required]
|
||||
[StringLength(255)]
|
||||
public string NombreArchivo { get; set; } = string.Empty;
|
||||
|
||||
[Column("ruta_archivo")]
|
||||
[Required]
|
||||
[StringLength(500)]
|
||||
public string RutaArchivo { get; set; } = string.Empty;
|
||||
|
||||
[Column("tipo_contenido")]
|
||||
[StringLength(100)]
|
||||
public string? TipoContenido { get; set; }
|
||||
|
||||
[Column("fecha_subida")]
|
||||
public DateTime FechaSubida { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
80
RS_system/Models/MovimientoInventario.cs
Normal file
80
RS_system/Models/MovimientoInventario.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
public enum TipoMovimiento
|
||||
{
|
||||
ENTRADA, // Nueva adquisición (aunque se crea al crear art, podría usarse para reingresos)
|
||||
SALIDA, // Salida temporal
|
||||
TRASLADO, // Cambio de ubicación
|
||||
BAJA, // Retiro permanente (daño, robo, venta)
|
||||
REPARACION, // Envío a taller
|
||||
AJUSTE, // Corrección de inventario
|
||||
CAMBIO_ESTADO, // Cambio de condición física
|
||||
PRESTAMO, // Préstamo a persona
|
||||
DEVOLUCION
|
||||
}
|
||||
|
||||
[Table("movimientos_inventario")]
|
||||
public class MovimientoInventario
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("articulo_id")]
|
||||
[Required]
|
||||
public int ArticuloId { get; set; }
|
||||
|
||||
[Column("tipo_movimiento")]
|
||||
[Required]
|
||||
public string TipoMovimiento { get; set; } = string.Empty;
|
||||
|
||||
[Column("cantidad")]
|
||||
public int Cantidad { get; set; } = 1; // Default 1 for UNITARIO
|
||||
|
||||
[Column("fecha")]
|
||||
public DateTime Fecha { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Ubicaciones
|
||||
[Column("ubicacion_origen_id")]
|
||||
public int? UbicacionOrigenId { get; set; }
|
||||
|
||||
[Column("ubicacion_destino_id")]
|
||||
public int? UbicacionDestinoId { get; set; }
|
||||
|
||||
// Estados
|
||||
[Column("estado_anterior_id")]
|
||||
public int? EstadoAnteriorId { get; set; }
|
||||
|
||||
[Column("estado_nuevo_id")]
|
||||
public int? EstadoNuevoId { get; set; }
|
||||
|
||||
[Column("TipMov")]
|
||||
public int? TipMov { get; set; }
|
||||
|
||||
[Column("observacion")]
|
||||
[StringLength(500)]
|
||||
public string? Observacion { get; set; }
|
||||
|
||||
[Column("usuario_id")]
|
||||
[StringLength(100)]
|
||||
public string? UsuarioId { get; set; } // Username or User ID
|
||||
|
||||
// Navigation Properties
|
||||
[ForeignKey("ArticuloId")]
|
||||
public virtual Articulo? Articulo { get; set; }
|
||||
|
||||
[ForeignKey("UbicacionOrigenId")]
|
||||
public virtual Ubicacion? UbicacionOrigen { get; set; }
|
||||
|
||||
[ForeignKey("UbicacionDestinoId")]
|
||||
public virtual Ubicacion? UbicacionDestino { get; set; }
|
||||
|
||||
[ForeignKey("EstadoAnteriorId")]
|
||||
public virtual EstadoArticulo? EstadoAnterior { get; set; }
|
||||
|
||||
[ForeignKey("EstadoNuevoId")]
|
||||
public virtual EstadoArticulo? EstadoNuevo { get; set; }
|
||||
}
|
||||
89
RS_system/Models/Prestamo.cs
Normal file
89
RS_system/Models/Prestamo.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("prestamos")]
|
||||
public class Prestamo
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("articulo_id")]
|
||||
[Required]
|
||||
public int ArticuloId { get; set; }
|
||||
|
||||
[Column("cantidad")]
|
||||
[Required]
|
||||
public int Cantidad { get; set; }
|
||||
|
||||
[Column("persona_nombre")]
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string PersonaNombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("persona_identificacion")]
|
||||
[StringLength(50)]
|
||||
public string? PersonaIdentificacion { get; set; }
|
||||
|
||||
[Column("fecha_prestamo")]
|
||||
public DateTime FechaPrestamo { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("fecha_devolucion_estimada")]
|
||||
public DateTime? FechaDevolucionEstimada { get; set; }
|
||||
|
||||
[Column("fecha_devolucion_real")]
|
||||
public DateTime? FechaDevolucionReal { get; set; }
|
||||
|
||||
[Column("estado")]
|
||||
[Required]
|
||||
public string Estado { get; set; } = "ACTIVO"; // ACTIVO, DEVUELTO, ATRASADO
|
||||
|
||||
[Column("observacion")]
|
||||
[StringLength(500)]
|
||||
public string? Observacion { get; set; }
|
||||
|
||||
[Column("usuario_id")]
|
||||
[StringLength(100)]
|
||||
public string? UsuarioId { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
[ForeignKey("ArticuloId")]
|
||||
public virtual Articulo? Articulo { get; set; }
|
||||
|
||||
// Navigation Property for detailed items
|
||||
public virtual ICollection<PrestamoDetalle> Detalles { get; set; } = new List<PrestamoDetalle>();
|
||||
}
|
||||
|
||||
[Table("prestamo_detalles")]
|
||||
public class PrestamoDetalle
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("prestamo_id")]
|
||||
[Required]
|
||||
public long PrestamoId { get; set; }
|
||||
|
||||
[Column("codigo_articulo_individual")]
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string CodigoArticuloIndividual { get; set; } = string.Empty;
|
||||
|
||||
[Column("estado")]
|
||||
[Required]
|
||||
public string Estado { get; set; } = "PRESTADO"; // PRESTADO, DEVUELTO
|
||||
|
||||
[Column("fecha_devolucion")]
|
||||
public DateTime? FechaDevolucion { get; set; }
|
||||
|
||||
[Column("observacion")]
|
||||
[StringLength(300)]
|
||||
public string? Observacion { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
[ForeignKey("PrestamoId")]
|
||||
public virtual Prestamo? Prestamo { get; set; }
|
||||
}
|
||||
44
RS_system/Models/ReporteMensualContable.cs
Normal file
44
RS_system/Models/ReporteMensualContable.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("reportes_mensuales_contables")]
|
||||
public class ReporteMensualContable
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("grupo_trabajo_id")]
|
||||
[Required]
|
||||
public long GrupoTrabajoId { get; set; }
|
||||
|
||||
[ForeignKey("GrupoTrabajoId")]
|
||||
public virtual GrupoTrabajo GrupoTrabajo { get; set; }
|
||||
|
||||
[Column("mes")]
|
||||
[Required]
|
||||
public int Mes { get; set; }
|
||||
|
||||
[Column("anio")]
|
||||
[Required]
|
||||
public int Anio { get; set; }
|
||||
|
||||
[Column("saldo_inicial", TypeName = "decimal(18,2)")]
|
||||
public decimal SaldoInicial { get; set; }
|
||||
|
||||
|
||||
[Column("fecha_creacion")]
|
||||
public DateTime FechaCreacion { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("cerrado")]
|
||||
public bool Cerrado { get; set; } = false;
|
||||
|
||||
// Navigation property for details
|
||||
public virtual ICollection<ContabilidadRegistro> Registros { get; set; } = new List<ContabilidadRegistro>();
|
||||
|
||||
// Helper properties for display
|
||||
[NotMapped]
|
||||
public string NombreMes => new DateTime(Anio, Mes, 1).ToString("MMMM", new System.Globalization.CultureInfo("es-ES"));
|
||||
}
|
||||
36
RS_system/Models/ReporteMensualGeneral.cs
Normal file
36
RS_system/Models/ReporteMensualGeneral.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("reportes_mensuales_generales")]
|
||||
public class ReporteMensualGeneral
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Column("mes")]
|
||||
[Required]
|
||||
public int Mes { get; set; }
|
||||
|
||||
[Column("anio")]
|
||||
[Required]
|
||||
public int Anio { get; set; }
|
||||
|
||||
[Column("saldo_inicial", TypeName = "decimal(18,2)")]
|
||||
public decimal SaldoInicial { get; set; }
|
||||
|
||||
[Column("fecha_creacion")]
|
||||
public DateTime FechaCreacion { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("cerrado")]
|
||||
public bool Cerrado { get; set; } = false;
|
||||
|
||||
// Navigation property for details
|
||||
public virtual ICollection<MovimientoGeneral> Movimientos { get; set; } = new List<MovimientoGeneral>();
|
||||
|
||||
// Helper properties for display
|
||||
[NotMapped]
|
||||
public string NombreMes => new DateTime(Anio, Mes, 1).ToString("MMMM", new System.Globalization.CultureInfo("es-ES"));
|
||||
}
|
||||
39
RS_system/Models/TipoColaboracion.cs
Normal file
39
RS_system/Models/TipoColaboracion.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("tipos_colaboracion", Schema = "public")]
|
||||
public class TipoColaboracion
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
[Column("nombre")]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("monto_sugerido")]
|
||||
[Required]
|
||||
public decimal MontoSugerido { get; set; }
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("orden")]
|
||||
public int Orden { get; set; }
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation properties
|
||||
public ICollection<DetalleColaboracion> Detalles { get; set; } = new List<DetalleColaboracion>();
|
||||
}
|
||||
41
RS_system/Models/Ubicacion.cs
Normal file
41
RS_system/Models/Ubicacion.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Rs_system.Models;
|
||||
|
||||
[Table("ubicaciones")]
|
||||
public class Ubicacion
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("nombre")]
|
||||
[Required(ErrorMessage = "El nombre es obligatorio")]
|
||||
[StringLength(100, ErrorMessage = "El nombre no puede exceder los 100 caracteres")]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[Column("descripcion")]
|
||||
[StringLength(200, ErrorMessage = "La descripción no puede exceder los 200 caracteres")]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[Column("responsable")]
|
||||
[StringLength(100, ErrorMessage = "El nombre del responsable no puede exceder los 100 caracteres")]
|
||||
public string? Responsable { get; set; }
|
||||
|
||||
[Column("activo")]
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
[Column("eliminado")]
|
||||
public bool Eliminado { get; set; } = false;
|
||||
|
||||
[Column("creado_en")]
|
||||
public DateTime CreadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("actualizado_en")]
|
||||
public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[Column("creado_por")]
|
||||
[StringLength(100)]
|
||||
public string? CreadoPor { get; set; }
|
||||
}
|
||||
68
RS_system/Models/ViewModels/ArticuloViewModel.cs
Normal file
68
RS_system/Models/ViewModels/ArticuloViewModel.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
public class ArticuloViewModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El código es obligatorio")]
|
||||
[StringLength(50, ErrorMessage = "El código no puede exceder los 50 caracteres")]
|
||||
public string Codigo { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "El nombre es obligatorio")]
|
||||
[StringLength(100, ErrorMessage = "El nombre no puede exceder los 100 caracteres")]
|
||||
public string Nombre { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500, ErrorMessage = "La descripción no puede exceder los 500 caracteres")]
|
||||
public string? Descripcion { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? Marca { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? Modelo { get; set; }
|
||||
|
||||
[Display(Name = "Número de Serie")]
|
||||
[StringLength(100)]
|
||||
public string? NumeroSerie { get; set; }
|
||||
|
||||
[Range(0, 99999999.99)]
|
||||
public decimal Precio { get; set; } = 0;
|
||||
|
||||
[Display(Name = "Fecha de Adquisición")]
|
||||
public DateOnly? FechaAdquisicion { get; set; }
|
||||
|
||||
public string? ImagenUrl { get; set; }
|
||||
|
||||
[Display(Name = "Imagen")]
|
||||
public IFormFile? ImagenFile { get; set; }
|
||||
|
||||
[Display(Name = "Tipo de Control")]
|
||||
public string? TipoControl { get; set; } = "UNITARIO"; // Default for View
|
||||
|
||||
[Display(Name = "Cantidad Inicial")]
|
||||
[Range(1, 100000)]
|
||||
public int CantidadInicial { get; set; } = 1;
|
||||
|
||||
public int CategoriaId { get; set; }
|
||||
|
||||
[Display(Name = "Estado")]
|
||||
[Required(ErrorMessage = "El estado es obligatorio")]
|
||||
public int EstadoId { get; set; }
|
||||
|
||||
[Display(Name = "Ubicación")]
|
||||
[Required(ErrorMessage = "La ubicación es obligatoria")]
|
||||
public int UbicacionId { get; set; }
|
||||
|
||||
public bool Activo { get; set; } = true;
|
||||
|
||||
// Display properties for lists/details
|
||||
public string? CategoriaNombre { get; set; }
|
||||
public string? EstadoNombre { get; set; }
|
||||
public string? EstadoColor { get; set; }
|
||||
public string? UbicacionNombre { get; set; }
|
||||
public int CantidadGlobal { get; set; }
|
||||
}
|
||||
41
RS_system/Models/ViewModels/EstadoCuentaViewModel.cs
Normal file
41
RS_system/Models/ViewModels/EstadoCuentaViewModel.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
public class EstadoCuentaViewModel
|
||||
{
|
||||
public long MiembroId { get; set; }
|
||||
public string NombreMiembro { get; set; } = string.Empty;
|
||||
public DateTime FechaConsulta { get; set; }
|
||||
|
||||
public List<HistorialPorTipo> HistorialPorTipos { get; set; } = new();
|
||||
public decimal TotalAportado { get; set; }
|
||||
}
|
||||
|
||||
public class HistorialPorTipo
|
||||
{
|
||||
public string TipoNombre { get; set; } = string.Empty;
|
||||
public List<RegistroMensual> Registros { get; set; } = new();
|
||||
public decimal TotalTipo { get; set; }
|
||||
}
|
||||
|
||||
public class RegistroMensual
|
||||
{
|
||||
public int Mes { get; set; }
|
||||
public int Anio { get; set; }
|
||||
public decimal Monto { get; set; }
|
||||
public DateTime FechaRegistro { get; set; }
|
||||
|
||||
public string MesTexto => ObtenerMesTexto();
|
||||
|
||||
private string ObtenerMesTexto()
|
||||
{
|
||||
try
|
||||
{
|
||||
var fecha = new DateTime(Anio, Mes, 1);
|
||||
return fecha.ToString("MMMM yyyy", new System.Globalization.CultureInfo("es-ES"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return $"{Mes}/{Anio}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
public class RegistrarColaboracionViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Debe seleccionar un miembro")]
|
||||
public long MiembroId { get; set; }
|
||||
|
||||
[Display(Name = "Mes Inicial")]
|
||||
[Required(ErrorMessage = "Debe seleccionar el mes inicial")]
|
||||
[Range(1, 12, ErrorMessage = "Mes debe estar entre 1 y 12")]
|
||||
public int MesInicial { get; set; }
|
||||
|
||||
[Display(Name = "Año Inicial")]
|
||||
[Required(ErrorMessage = "Debe seleccionar el año inicial")]
|
||||
[Range(2000, 2100, ErrorMessage = "Año debe estar entre 2000 y 2100")]
|
||||
public int AnioInicial { get; set; }
|
||||
|
||||
[Display(Name = "Mes Final")]
|
||||
[Required(ErrorMessage = "Debe seleccionar el mes final")]
|
||||
[Range(1, 12, ErrorMessage = "Mes debe estar entre 1 y 12")]
|
||||
public int MesFinal { get; set; }
|
||||
|
||||
[Display(Name = "Año Final")]
|
||||
[Required(ErrorMessage = "Debe seleccionar el año final")]
|
||||
[Range(2000, 2100, ErrorMessage = "Año debe estar entre 2000 y 2100")]
|
||||
public int AnioFinal { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Debe seleccionar al menos un tipo de colaboración")]
|
||||
public List<long> TiposSeleccionados { get; set; } = new();
|
||||
|
||||
[Required(ErrorMessage = "Debe ingresar el monto total")]
|
||||
[Range(0.01, 999999.99, ErrorMessage = "El monto total debe ser mayor a 0")]
|
||||
[Display(Name = "Monto Total Entregado")]
|
||||
public decimal MontoTotal { get; set; }
|
||||
|
||||
[Display(Name = "Tipo de Colaboración Prioritaria")]
|
||||
public long? TipoPrioritario { get; set; }
|
||||
|
||||
[MaxLength(500, ErrorMessage = "Las observaciones no pueden exceder 500 caracteres")]
|
||||
[Display(Name = "Observaciones")]
|
||||
public string? Observaciones { get; set; }
|
||||
|
||||
// Para cargar en el formulario
|
||||
public List<TipoColaboracion> TiposDisponibles { get; set; } = new();
|
||||
|
||||
// Propiedad calculada: Total de meses
|
||||
public int TotalMeses
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
var fechaInicial = new DateTime(AnioInicial, MesInicial, 1);
|
||||
var fechaFinal = new DateTime(AnioFinal, MesFinal, 1);
|
||||
|
||||
if (fechaFinal < fechaInicial)
|
||||
return 0;
|
||||
|
||||
return ((AnioFinal - AnioInicial) * 12) + (MesFinal - MesInicial) + 1;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Propiedad calculada: Monto sugerido total basado en los tipos seleccionados
|
||||
public decimal MontoSugeridoTotal
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TiposDisponibles == null || !TiposSeleccionados.Any())
|
||||
return 0;
|
||||
|
||||
var tiposSeleccionadosData = TiposDisponibles
|
||||
.Where(t => TiposSeleccionados.Contains(t.Id))
|
||||
.ToList();
|
||||
|
||||
var montoSugeridoPorMes = tiposSeleccionadosData.Sum(t => t.MontoSugerido);
|
||||
return montoSugeridoPorMes * TotalMeses;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
public class ReporteColaboracionesViewModel
|
||||
{
|
||||
public DateTime FechaInicio { get; set; }
|
||||
public DateTime FechaFin { get; set; }
|
||||
public decimal TotalRecaudado { get; set; }
|
||||
|
||||
public List<DesglosePorTipo> DesglosePorTipos { get; set; } = new();
|
||||
public List<DetalleMovimiento> Movimientos { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DesglosePorTipo
|
||||
{
|
||||
public string TipoNombre { get; set; } = string.Empty;
|
||||
public int CantidadMeses { get; set; }
|
||||
public decimal TotalRecaudado { get; set; }
|
||||
}
|
||||
|
||||
public class DetalleMovimiento
|
||||
{
|
||||
public long ColaboracionId { get; set; }
|
||||
public DateTime Fecha { get; set; }
|
||||
public string NombreMiembro { get; set; } = string.Empty;
|
||||
public string TiposColaboracion { get; set; } = string.Empty;
|
||||
public string PeriodoCubierto { get; set; } = string.Empty;
|
||||
public decimal Monto { get; set; }
|
||||
}
|
||||
39
RS_system/Models/ViewModels/UltimoPagoViewModel.cs
Normal file
39
RS_system/Models/ViewModels/UltimoPagoViewModel.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace Rs_system.Models.ViewModels;
|
||||
|
||||
public class UltimoPagoViewModel
|
||||
{
|
||||
public long TipoId { get; set; }
|
||||
public string NombreTipo { get; set; }
|
||||
public int UltimoMes { get; set; }
|
||||
public int UltimoAnio { get; set; }
|
||||
public DateTime FechaUltimoPago { get; set; }
|
||||
|
||||
public string UltimoPeriodoTexto
|
||||
{
|
||||
get
|
||||
{
|
||||
if (UltimoMes == 0 || UltimoAnio == 0) return "Sin pagos registrados";
|
||||
return $"{ObtenerNombreMes(UltimoMes)} {UltimoAnio}";
|
||||
}
|
||||
}
|
||||
|
||||
private string ObtenerNombreMes(int mes)
|
||||
{
|
||||
return mes switch
|
||||
{
|
||||
1 => "Enero",
|
||||
2 => "Febrero",
|
||||
3 => "Marzo",
|
||||
4 => "Abril",
|
||||
5 => "Mayo",
|
||||
6 => "Junio",
|
||||
7 => "Julio",
|
||||
8 => "Agosto",
|
||||
9 => "Septiembre",
|
||||
10 => "Octubre",
|
||||
11 => "Noviembre",
|
||||
12 => "Diciembre",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
}
|
||||
276
RS_system/Services/ArticuloService.cs
Normal file
276
RS_system/Services/ArticuloService.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Models.ViewModels;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class ArticuloService : IArticuloService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IFileStorageService _fileStorageService;
|
||||
|
||||
public ArticuloService(ApplicationDbContext context, IFileStorageService fileStorageService)
|
||||
{
|
||||
_context = context;
|
||||
_fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ArticuloViewModel>> GetAllAsync(string? search = null, int? categoriaId = null, int? ubicacionId = null, int? estadoId = null)
|
||||
{
|
||||
var query = _context.Articulos
|
||||
.Include(a => a.Categoria)
|
||||
.Include(a => a.Estado)
|
||||
.Include(a => a.Ubicacion)
|
||||
.Where(a => !a.Eliminado)
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var term = search.ToLower();
|
||||
query = query.Where(a =>
|
||||
a.Nombre.ToLower().Contains(term) ||
|
||||
a.Codigo.ToLower().Contains(term) ||
|
||||
a.Modelo.ToLower().Contains(term) ||
|
||||
a.Marca.ToLower().Contains(term) ||
|
||||
a.NumeroSerie.ToLower().Contains(term));
|
||||
}
|
||||
|
||||
if (categoriaId.HasValue)
|
||||
query = query.Where(a => a.CategoriaId == categoriaId);
|
||||
|
||||
if (ubicacionId.HasValue)
|
||||
query = query.Where(a => a.UbicacionId == ubicacionId);
|
||||
|
||||
if (estadoId.HasValue)
|
||||
query = query.Where(a => a.EstadoId == estadoId);
|
||||
|
||||
return await query
|
||||
.OrderByDescending(a => a.CreadoEn)
|
||||
.Select(a => new ArticuloViewModel
|
||||
{
|
||||
Id = a.Id,
|
||||
Codigo = a.Codigo,
|
||||
Nombre = a.Nombre,
|
||||
Descripcion = a.Descripcion,
|
||||
Marca = a.Marca,
|
||||
Modelo = a.Modelo,
|
||||
NumeroSerie = a.NumeroSerie,
|
||||
Precio = a.Precio,
|
||||
FechaAdquisicion = a.FechaAdquisicion,
|
||||
ImagenUrl = a.ImagenUrl,
|
||||
CategoriaId = a.CategoriaId,
|
||||
CategoriaNombre = a.Categoria.Nombre,
|
||||
EstadoId = a.EstadoId,
|
||||
EstadoNombre = a.Estado.Nombre,
|
||||
EstadoColor = a.Estado.Color,
|
||||
UbicacionId = a.UbicacionId,
|
||||
UbicacionNombre = a.Ubicacion.Nombre,
|
||||
Activo = a.Activo
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ArticuloViewModel?> GetByIdAsync(int id)
|
||||
{
|
||||
var a = await _context.Articulos
|
||||
.Include(a => a.Categoria)
|
||||
.Include(a => a.Estado)
|
||||
.Include(a => a.Ubicacion)
|
||||
.FirstOrDefaultAsync(x => x.Id == id && !x.Eliminado);
|
||||
|
||||
if (a == null) return null;
|
||||
|
||||
return new ArticuloViewModel
|
||||
{
|
||||
Id = a.Id,
|
||||
Codigo = a.Codigo,
|
||||
Nombre = a.Nombre,
|
||||
Descripcion = a.Descripcion,
|
||||
Marca = a.Marca,
|
||||
Modelo = a.Modelo,
|
||||
NumeroSerie = a.NumeroSerie,
|
||||
Precio = a.Precio,
|
||||
FechaAdquisicion = a.FechaAdquisicion,
|
||||
ImagenUrl = a.ImagenUrl,
|
||||
CategoriaId = a.CategoriaId,
|
||||
CategoriaNombre = a.Categoria.Nombre,
|
||||
EstadoId = a.EstadoId,
|
||||
EstadoNombre = a.Estado.Nombre,
|
||||
EstadoColor = a.Estado.Color,
|
||||
UbicacionId = a.UbicacionId,
|
||||
UbicacionNombre = a.Ubicacion.Nombre,
|
||||
Activo = a.Activo,
|
||||
CantidadGlobal = a.CantidadGlobal,
|
||||
// New Fields
|
||||
TipoControl = a.TipoControl,
|
||||
CantidadInicial = a.CantidadGlobal // Map Global Qty to CantidadInicial for Display
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> CreateAsync(ArticuloViewModel viewModel, string createdBy)
|
||||
{
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
try
|
||||
{
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
string? imagenUrl = null;
|
||||
if (viewModel.ImagenFile != null)
|
||||
{
|
||||
imagenUrl = await _fileStorageService.SaveFileAsync(viewModel.ImagenFile, "articulos");
|
||||
}
|
||||
|
||||
var articulo = new Articulo
|
||||
{
|
||||
Codigo = viewModel.Codigo,
|
||||
Nombre = viewModel.Nombre,
|
||||
Descripcion = viewModel.Descripcion,
|
||||
Marca = viewModel.Marca,
|
||||
Modelo = viewModel.Modelo,
|
||||
NumeroSerie = viewModel.NumeroSerie,
|
||||
Precio = viewModel.Precio,
|
||||
FechaAdquisicion = viewModel.FechaAdquisicion,
|
||||
ImagenUrl = imagenUrl,
|
||||
CategoriaId = viewModel.CategoriaId,
|
||||
EstadoId = viewModel.EstadoId,
|
||||
UbicacionId = viewModel.UbicacionId,
|
||||
Activo = viewModel.Activo,
|
||||
Eliminado = false,
|
||||
CreadoPor = createdBy,
|
||||
CreadoEn = DateTime.UtcNow,
|
||||
ActualizadoEn = DateTime.UtcNow,
|
||||
// New Fields
|
||||
TipoControl = viewModel.TipoControl ?? nameof(Articulo.TipoControlInventario.UNITARIO),
|
||||
CantidadGlobal = (viewModel.TipoControl == nameof(Articulo.TipoControlInventario.LOTE)) ? viewModel.CantidadInicial : 1
|
||||
};
|
||||
|
||||
_context.Articulos.Add(articulo);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// If LOTE, initialize Existencia
|
||||
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
|
||||
{
|
||||
var existencia = new Existencia
|
||||
{
|
||||
ArticuloId = articulo.Id,
|
||||
UbicacionId = articulo.UbicacionId,
|
||||
Cantidad = articulo.CantidadGlobal,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
_context.Existencias.Add(existencia);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(ArticuloViewModel viewModel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var articulo = await _context.Articulos.FindAsync(viewModel.Id);
|
||||
if (articulo == null || articulo.Eliminado) return false;
|
||||
|
||||
if (viewModel.ImagenFile != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(articulo.ImagenUrl))
|
||||
{
|
||||
await _fileStorageService.DeleteFileAsync(articulo.ImagenUrl);
|
||||
}
|
||||
articulo.ImagenUrl = await _fileStorageService.SaveFileAsync(viewModel.ImagenFile, "articulos");
|
||||
}
|
||||
|
||||
articulo.Codigo = viewModel.Codigo;
|
||||
articulo.Nombre = viewModel.Nombre;
|
||||
articulo.Descripcion = viewModel.Descripcion;
|
||||
articulo.Marca = viewModel.Marca;
|
||||
articulo.Modelo = viewModel.Modelo;
|
||||
articulo.NumeroSerie = viewModel.NumeroSerie;
|
||||
articulo.Precio = viewModel.Precio;
|
||||
articulo.FechaAdquisicion = viewModel.FechaAdquisicion;
|
||||
articulo.CategoriaId = viewModel.CategoriaId;
|
||||
articulo.EstadoId = viewModel.EstadoId;
|
||||
articulo.UbicacionId = viewModel.UbicacionId;
|
||||
articulo.Activo = viewModel.Activo;
|
||||
articulo.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.Articulos.Update(articulo);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var articulo = await _context.Articulos.FindAsync(id);
|
||||
if (articulo == null || articulo.Eliminado) return false;
|
||||
|
||||
articulo.Eliminado = true;
|
||||
articulo.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.Articulos.Update(articulo);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsCodigoAsync(string codigo, int? excludeId = null)
|
||||
{
|
||||
var query = _context.Articulos.AsQueryable();
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.Id != excludeId.Value);
|
||||
}
|
||||
return await query.AnyAsync(a => a.Codigo.ToLower() == codigo.ToLower() && !a.Eliminado);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(int Id, string Nombre)>> GetCategoriasAsync()
|
||||
{
|
||||
return await _context.Categorias
|
||||
.Where(x => x.Activo && !x.Eliminado)
|
||||
.OrderBy(x => x.Nombre)
|
||||
.Select(x => new ValueTuple<int, string>(x.Id, x.Nombre))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(int Id, string Nombre, string Color)>> GetEstadosAsync()
|
||||
{
|
||||
return await _context.EstadosArticulos
|
||||
.Where(x => x.Activo && !x.Eliminado)
|
||||
.OrderBy(x => x.Nombre)
|
||||
.Select(x => new ValueTuple<int, string, string>(x.Id, x.Nombre, x.Color ?? "secondary"))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<(int Id, string Nombre)>> GetUbicacionesAsync()
|
||||
{
|
||||
return await _context.Ubicaciones
|
||||
.Where(x => x.Activo && !x.Eliminado)
|
||||
.OrderBy(x => x.Nombre)
|
||||
.Select(x => new ValueTuple<int, string>(x.Id, x.Nombre))
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
103
RS_system/Services/CategoriaService.cs
Normal file
103
RS_system/Services/CategoriaService.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class CategoriaService : ICategoriaService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public CategoriaService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Categoria>> GetAllAsync()
|
||||
{
|
||||
return await _context.Categorias
|
||||
.Where(c => !c.Eliminado)
|
||||
.OrderBy(c => c.Nombre)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Categoria?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Categorias
|
||||
.FirstOrDefaultAsync(c => c.Id == id && !c.Eliminado);
|
||||
}
|
||||
|
||||
public async Task<bool> CreateAsync(Categoria categoria)
|
||||
{
|
||||
try
|
||||
{
|
||||
categoria.CreadoEn = DateTime.UtcNow;
|
||||
categoria.ActualizadoEn = DateTime.UtcNow;
|
||||
// Eliminado and Activo defaults are set in the model/DB, ensuring here just in case
|
||||
categoria.Eliminado = false;
|
||||
|
||||
_context.Categorias.Add(categoria);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(Categoria categoria)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await _context.Categorias.FindAsync(categoria.Id);
|
||||
if (existing == null || existing.Eliminado) return false;
|
||||
|
||||
existing.Nombre = categoria.Nombre;
|
||||
existing.Descripcion = categoria.Descripcion;
|
||||
existing.Activo = categoria.Activo;
|
||||
existing.ActualizadoEn = DateTime.UtcNow;
|
||||
// CreadoPor and CreadoEn should not change
|
||||
|
||||
_context.Categorias.Update(existing);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var categoria = await _context.Categorias.FindAsync(id);
|
||||
if (categoria == null || categoria.Eliminado) return false;
|
||||
|
||||
categoria.Eliminado = true;
|
||||
categoria.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.Categorias.Update(categoria);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string nombre, int? excludeId = null)
|
||||
{
|
||||
var query = _context.Categorias.AsQueryable();
|
||||
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(c => c.Id != excludeId.Value);
|
||||
}
|
||||
|
||||
return await query.AnyAsync(c => c.Nombre.ToLower() == nombre.ToLower() && !c.Eliminado);
|
||||
}
|
||||
}
|
||||
373
RS_system/Services/ColaboracionService.cs
Normal file
373
RS_system/Services/ColaboracionService.cs
Normal file
@@ -0,0 +1,373 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Models.ViewModels;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class ColaboracionService : IColaboracionService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public ColaboracionService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<TipoColaboracion>> GetTiposActivosAsync()
|
||||
{
|
||||
return await _context.TiposColaboracion
|
||||
.Where(t => t.Activo)
|
||||
.OrderBy(t => t.Orden)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<TipoColaboracion?> GetTipoByIdAsync(long id)
|
||||
{
|
||||
return await _context.TiposColaboracion
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(t => t.Id == id);
|
||||
}
|
||||
|
||||
public async Task<Colaboracion> RegistrarColaboracionAsync(
|
||||
RegistrarColaboracionViewModel model,
|
||||
string registradoPor)
|
||||
{
|
||||
// Validar que el rango de fechas sea válido
|
||||
var fechaInicial = new DateTime(model.AnioInicial, model.MesInicial, 1);
|
||||
var fechaFinal = new DateTime(model.AnioFinal, model.MesFinal, 1);
|
||||
|
||||
if (fechaFinal < fechaInicial)
|
||||
{
|
||||
throw new ArgumentException("La fecha final no puede ser anterior a la fecha inicial");
|
||||
}
|
||||
|
||||
// Obtener información de los tipos seleccionados
|
||||
var tiposColaboracion = await _context.TiposColaboracion
|
||||
.Where(t => model.TiposSeleccionados.Contains(t.Id))
|
||||
.ToListAsync();
|
||||
|
||||
// Generar todos los meses en el rango
|
||||
var mesesAPagar = GenerarRangoMeses(
|
||||
model.AnioInicial, model.MesInicial,
|
||||
model.AnioFinal, model.MesFinal);
|
||||
|
||||
// Crear colaboración principal
|
||||
var colaboracion = new Colaboracion
|
||||
{
|
||||
MiembroId = model.MiembroId,
|
||||
FechaRegistro = DateTime.UtcNow,
|
||||
MontoTotal = model.MontoTotal,
|
||||
Observaciones = model.Observaciones,
|
||||
RegistradoPor = registradoPor,
|
||||
CreadoEn = DateTime.UtcNow,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Distribuir el monto total entre los meses y tipos
|
||||
var detalles = DistribuirMonto(
|
||||
model.MontoTotal,
|
||||
tiposColaboracion,
|
||||
mesesAPagar,
|
||||
model.TipoPrioritario);
|
||||
|
||||
foreach (var detalle in detalles)
|
||||
{
|
||||
colaboracion.Detalles.Add(detalle);
|
||||
}
|
||||
|
||||
_context.Colaboraciones.Add(colaboracion);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return colaboracion;
|
||||
}
|
||||
|
||||
private List<DetalleColaboracion> DistribuirMonto(
|
||||
decimal montoTotal,
|
||||
List<TipoColaboracion> tipos,
|
||||
List<(int anio, int mes)> meses,
|
||||
long? tipoPrioritario)
|
||||
{
|
||||
var detalles = new List<DetalleColaboracion>();
|
||||
var montoRestante = montoTotal;
|
||||
|
||||
// Estrategia: Mes a Mes
|
||||
// Para cada mes, intentamos cubrir los tipos (Prioritario primero)
|
||||
|
||||
foreach (var (anio, mes) in meses)
|
||||
{
|
||||
if (montoRestante <= 0) break;
|
||||
|
||||
// Ordenar tipos para este mes: Prioritario al inicio
|
||||
var tiposOrdenados = new List<TipoColaboracion>();
|
||||
|
||||
if (tipoPrioritario.HasValue)
|
||||
{
|
||||
var prio = tipos.FirstOrDefault(t => t.Id == tipoPrioritario.Value);
|
||||
if (prio != null)
|
||||
{
|
||||
tiposOrdenados.Add(prio);
|
||||
tiposOrdenados.AddRange(tipos.Where(t => t.Id != tipoPrioritario.Value));
|
||||
}
|
||||
else
|
||||
{
|
||||
tiposOrdenados.AddRange(tipos);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tiposOrdenados.AddRange(tipos);
|
||||
}
|
||||
|
||||
foreach (var tipo in tiposOrdenados)
|
||||
{
|
||||
if (montoRestante <= 0) break;
|
||||
|
||||
// Determinar cuánto asignar
|
||||
// Intentamos cubrir el monto sugerido completo
|
||||
var montoAAsignar = Math.Min(tipo.MontoSugerido, montoRestante);
|
||||
|
||||
// Si es un monto muy pequeño (ej: residuo), igual lo asignamos para no perderlo,
|
||||
// salvo que queramos reglas estrictas de "solo completos".
|
||||
// Por ahora asignamos lo que haya.
|
||||
|
||||
if (montoAAsignar > 0)
|
||||
{
|
||||
detalles.Add(new DetalleColaboracion
|
||||
{
|
||||
TipoColaboracionId = tipo.Id,
|
||||
Mes = mes,
|
||||
Anio = anio,
|
||||
Monto = montoAAsignar,
|
||||
CreadoEn = DateTime.UtcNow
|
||||
});
|
||||
|
||||
montoRestante -= montoAAsignar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return detalles;
|
||||
}
|
||||
|
||||
public async Task<List<UltimoPagoViewModel>> GetUltimosPagosPorMiembroAsync(long miembroId)
|
||||
{
|
||||
// Obtener todos los detalles agrupados por tipo para encontrar la fecha máxima
|
||||
var detalles = await _context.DetalleColaboraciones
|
||||
.Include(d => d.Colaboracion)
|
||||
.Include(d => d.TipoColaboracion)
|
||||
.Where(d => d.Colaboracion.MiembroId == miembroId)
|
||||
.ToListAsync();
|
||||
|
||||
var resultado = detalles
|
||||
.GroupBy(d => d.TipoColaboracion)
|
||||
.Select(g =>
|
||||
{
|
||||
// Encontrar el registro con el mes/año más reciente
|
||||
var ultimo = g.OrderByDescending(d => d.Anio).ThenByDescending(d => d.Mes).FirstOrDefault();
|
||||
if (ultimo == null) return null;
|
||||
|
||||
return new UltimoPagoViewModel
|
||||
{
|
||||
TipoId = g.Key.Id,
|
||||
NombreTipo = g.Key.Nombre,
|
||||
UltimoMes = ultimo.Mes,
|
||||
UltimoAnio = ultimo.Anio,
|
||||
FechaUltimoPago = ultimo.Colaboracion.FechaRegistro
|
||||
};
|
||||
})
|
||||
.Where(x => x != null)
|
||||
.ToList();
|
||||
|
||||
// Asegurar que retornamos todos los tipos activos, incluso si no tienen pagos
|
||||
var tiposActivos = await GetTiposActivosAsync();
|
||||
var listaFinal = new List<UltimoPagoViewModel>();
|
||||
|
||||
foreach (var tipo in tiposActivos)
|
||||
{
|
||||
var pago = resultado.FirstOrDefault(r => r.TipoId == tipo.Id);
|
||||
if (pago != null)
|
||||
{
|
||||
listaFinal.Add(pago);
|
||||
}
|
||||
else
|
||||
{
|
||||
listaFinal.Add(new UltimoPagoViewModel
|
||||
{
|
||||
TipoId = tipo.Id,
|
||||
NombreTipo = tipo.Nombre,
|
||||
UltimoMes = 0, // No hay pagos
|
||||
UltimoAnio = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return listaFinal;
|
||||
}
|
||||
|
||||
private List<(int anio, int mes)> GenerarRangoMeses(
|
||||
int anioInicial, int mesInicial,
|
||||
int anioFinal, int mesFinal)
|
||||
{
|
||||
var meses = new List<(int, int)>();
|
||||
var fecha = new DateTime(anioInicial, mesInicial, 1);
|
||||
var fechaFin = new DateTime(anioFinal, mesFinal, 1);
|
||||
|
||||
while (fecha <= fechaFin)
|
||||
{
|
||||
meses.Add((fecha.Year, fecha.Month));
|
||||
fecha = fecha.AddMonths(1);
|
||||
}
|
||||
|
||||
return meses;
|
||||
}
|
||||
|
||||
public async Task<List<Colaboracion>> GetColaboracionesRecientesAsync(int cantidad = 50)
|
||||
{
|
||||
return await _context.Colaboraciones
|
||||
.Include(c => c.Miembro)
|
||||
.ThenInclude(m => m.Persona)
|
||||
.Include(c => c.Detalles)
|
||||
.ThenInclude(d => d.TipoColaboracion)
|
||||
.OrderByDescending(c => c.FechaRegistro)
|
||||
.Take(cantidad)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Colaboracion?> GetColaboracionByIdAsync(long id)
|
||||
{
|
||||
return await _context.Colaboraciones
|
||||
.Include(c => c.Miembro)
|
||||
.ThenInclude(m => m.Persona)
|
||||
.Include(c => c.Detalles)
|
||||
.ThenInclude(d => d.TipoColaboracion)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
}
|
||||
|
||||
public async Task<ReporteColaboracionesViewModel> GenerarReportePorFechasAsync(
|
||||
DateTime fechaInicio,
|
||||
DateTime fechaFin)
|
||||
{
|
||||
var colaboraciones = await _context.Colaboraciones
|
||||
.Include(c => c.Miembro)
|
||||
.ThenInclude(m => m.Persona)
|
||||
.Include(c => c.Detalles)
|
||||
.ThenInclude(d => d.TipoColaboracion)
|
||||
.Where(c => c.FechaRegistro >= fechaInicio && c.FechaRegistro <= fechaFin)
|
||||
.OrderByDescending(c => c.FechaRegistro)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
var reporte = new ReporteColaboracionesViewModel
|
||||
{
|
||||
FechaInicio = fechaInicio,
|
||||
FechaFin = fechaFin,
|
||||
TotalRecaudado = colaboraciones.Sum(c => c.MontoTotal)
|
||||
};
|
||||
|
||||
// Desglose por tipo
|
||||
var desglosePorTipo = colaboraciones
|
||||
.SelectMany(c => c.Detalles)
|
||||
.GroupBy(d => d.TipoColaboracion.Nombre)
|
||||
.Select(g => new DesglosePorTipo
|
||||
{
|
||||
TipoNombre = g.Key,
|
||||
CantidadMeses = g.Count(),
|
||||
TotalRecaudado = g.Sum(d => d.Monto)
|
||||
})
|
||||
.OrderBy(d => d.TipoNombre)
|
||||
.ToList();
|
||||
|
||||
reporte.DesglosePorTipos = desglosePorTipo;
|
||||
|
||||
// Detalle de movimientos
|
||||
var movimientos = colaboraciones.Select(c => new DetalleMovimiento
|
||||
{
|
||||
ColaboracionId = c.Id,
|
||||
Fecha = c.FechaRegistro,
|
||||
NombreMiembro = $"{c.Miembro.Persona.Nombres} {c.Miembro.Persona.Apellidos}",
|
||||
TiposColaboracion = string.Join(", ", c.Detalles.Select(d => d.TipoColaboracion.Nombre).Distinct()),
|
||||
PeriodoCubierto = ObtenerPeriodoCubierto(c.Detalles.ToList()),
|
||||
Monto = c.MontoTotal
|
||||
}).ToList();
|
||||
|
||||
reporte.Movimientos = movimientos;
|
||||
|
||||
return reporte;
|
||||
}
|
||||
|
||||
private string ObtenerPeriodoCubierto(List<DetalleColaboracion> detalles)
|
||||
{
|
||||
if (!detalles.Any()) return "";
|
||||
|
||||
var ordenados = detalles.OrderBy(d => d.Anio).ThenBy(d => d.Mes).ToList();
|
||||
var primero = ordenados.First();
|
||||
var ultimo = ordenados.Last();
|
||||
|
||||
var cultura = new CultureInfo("es-ES");
|
||||
|
||||
if (primero.Anio == ultimo.Anio && primero.Mes == ultimo.Mes)
|
||||
{
|
||||
return new DateTime(primero.Anio, primero.Mes, 1).ToString("MMMM yyyy", cultura);
|
||||
}
|
||||
|
||||
return $"{new DateTime(primero.Anio, primero.Mes, 1).ToString("MMM yyyy", cultura)} - " +
|
||||
$"{new DateTime(ultimo.Anio, ultimo.Mes, 1).ToString("MMM yyyy", cultura)}";
|
||||
}
|
||||
|
||||
public async Task<EstadoCuentaViewModel> GenerarEstadoCuentaAsync(long miembroId)
|
||||
{
|
||||
var miembro = await _context.Miembros
|
||||
.Include(m => m.Persona)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(m => m.Id == miembroId);
|
||||
|
||||
if (miembro == null)
|
||||
throw new Exception("Miembro no encontrado");
|
||||
|
||||
var colaboraciones = await _context.Colaboraciones
|
||||
.Include(c => c.Detalles)
|
||||
.ThenInclude(d => d.TipoColaboracion)
|
||||
.Where(c => c.MiembroId == miembroId)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
var estado = new EstadoCuentaViewModel
|
||||
{
|
||||
MiembroId = miembroId,
|
||||
NombreMiembro = $"{miembro.Persona.Nombres} {miembro.Persona.Apellidos}",
|
||||
FechaConsulta = DateTime.Now,
|
||||
TotalAportado = colaboraciones.Sum(c => c.MontoTotal)
|
||||
};
|
||||
|
||||
// Agrupar por tipo
|
||||
var historialPorTipo = colaboraciones
|
||||
.SelectMany(c => c.Detalles.Select(d => new { Detalle = d, FechaRegistro = c.FechaRegistro }))
|
||||
.GroupBy(x => x.Detalle.TipoColaboracion.Nombre)
|
||||
.Select(g => new HistorialPorTipo
|
||||
{
|
||||
TipoNombre = g.Key,
|
||||
TotalTipo = g.Sum(x => x.Detalle.Monto),
|
||||
Registros = g.Select(x => new RegistroMensual
|
||||
{
|
||||
Mes = x.Detalle.Mes,
|
||||
Anio = x.Detalle.Anio,
|
||||
Monto = x.Detalle.Monto,
|
||||
FechaRegistro = x.FechaRegistro
|
||||
})
|
||||
.OrderBy(r => r.Anio)
|
||||
.ThenBy(r => r.Mes)
|
||||
.ToList()
|
||||
})
|
||||
.OrderBy(h => h.TipoNombre)
|
||||
.ToList();
|
||||
|
||||
estado.HistorialPorTipos = historialPorTipo;
|
||||
|
||||
return estado;
|
||||
}
|
||||
}
|
||||
337
RS_system/Services/ContabilidadGeneralService.cs
Normal file
337
RS_system/Services/ContabilidadGeneralService.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class ContabilidadGeneralService : IContabilidadGeneralService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public ContabilidadGeneralService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// ==================== Categorías de Ingreso ====================
|
||||
|
||||
public async Task<List<CategoriaIngreso>> ObtenerCategoriasIngresoAsync()
|
||||
{
|
||||
return await _context.CategoriasIngreso
|
||||
.Where(c => c.Activa)
|
||||
.OrderBy(c => c.Nombre)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CategoriaIngreso?> ObtenerCategoriaIngresoPorIdAsync(long id)
|
||||
{
|
||||
return await _context.CategoriasIngreso.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<CategoriaIngreso> CrearCategoriaIngresoAsync(CategoriaIngreso categoria)
|
||||
{
|
||||
categoria.FechaCreacion = DateTime.UtcNow;
|
||||
_context.CategoriasIngreso.Add(categoria);
|
||||
await _context.SaveChangesAsync();
|
||||
return categoria;
|
||||
}
|
||||
|
||||
public async Task<bool> ActualizarCategoriaIngresoAsync(CategoriaIngreso categoria)
|
||||
{
|
||||
try
|
||||
{
|
||||
_context.CategoriasIngreso.Update(categoria);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> EliminarCategoriaIngresoAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var categoria = await _context.CategoriasIngreso.FindAsync(id);
|
||||
if (categoria == null) return false;
|
||||
|
||||
// Soft delete - marcar como inactiva en lugar de eliminar
|
||||
categoria.Activa = false;
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Categorías de Egreso ====================
|
||||
|
||||
public async Task<List<CategoriaEgreso>> ObtenerCategoriasEgresoAsync()
|
||||
{
|
||||
return await _context.CategoriasEgreso
|
||||
.Where(c => c.Activa)
|
||||
.OrderBy(c => c.Nombre)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CategoriaEgreso?> ObtenerCategoriaEgresoPorIdAsync(long id)
|
||||
{
|
||||
return await _context.CategoriasEgreso.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<CategoriaEgreso> CrearCategoriaEgresoAsync(CategoriaEgreso categoria)
|
||||
{
|
||||
categoria.FechaCreacion = DateTime.UtcNow;
|
||||
_context.CategoriasEgreso.Add(categoria);
|
||||
await _context.SaveChangesAsync();
|
||||
return categoria;
|
||||
}
|
||||
|
||||
public async Task<bool> ActualizarCategoriaEgresoAsync(CategoriaEgreso categoria)
|
||||
{
|
||||
try
|
||||
{
|
||||
_context.CategoriasEgreso.Update(categoria);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> EliminarCategoriaEgresoAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var categoria = await _context.CategoriasEgreso.FindAsync(id);
|
||||
if (categoria == null) return false;
|
||||
|
||||
// Soft delete - marcar como inactiva en lugar de eliminar
|
||||
categoria.Activa = false;
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Reportes Mensuales ====================
|
||||
|
||||
public async Task<ReporteMensualGeneral?> ObtenerReporteMensualAsync(int mes, int anio)
|
||||
{
|
||||
return await _context.ReportesMensualesGenerales
|
||||
.Include(r => r.Movimientos)
|
||||
.ThenInclude(m => m.CategoriaIngreso)
|
||||
.Include(r => r.Movimientos)
|
||||
.ThenInclude(m => m.CategoriaEgreso)
|
||||
.FirstOrDefaultAsync(r => r.Mes == mes && r.Anio == anio);
|
||||
}
|
||||
|
||||
public async Task<ReporteMensualGeneral> ObtenerOCrearReporteMensualAsync(int mes, int anio)
|
||||
{
|
||||
var reporteExistente = await ObtenerReporteMensualAsync(mes, anio);
|
||||
if (reporteExistente != null)
|
||||
return reporteExistente;
|
||||
|
||||
// Obtener el saldo final del mes anterior
|
||||
var mesAnterior = mes == 1 ? 12 : mes - 1;
|
||||
var anioAnterior = mes == 1 ? anio - 1 : anio;
|
||||
|
||||
var reporteAnterior = await ObtenerReporteMensualAsync(mesAnterior, anioAnterior);
|
||||
var saldoInicial = reporteAnterior != null
|
||||
? await CalcularSaldoActualAsync(reporteAnterior.Id)
|
||||
: 0;
|
||||
|
||||
var nuevoReporte = new ReporteMensualGeneral
|
||||
{
|
||||
Mes = mes,
|
||||
Anio = anio,
|
||||
SaldoInicial = saldoInicial,
|
||||
FechaCreacion = DateTime.UtcNow,
|
||||
Cerrado = false
|
||||
};
|
||||
|
||||
_context.ReportesMensualesGenerales.Add(nuevoReporte);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return nuevoReporte;
|
||||
}
|
||||
|
||||
public async Task<List<ReporteMensualGeneral>> ListarReportesAsync(int? anio = null)
|
||||
{
|
||||
var query = _context.ReportesMensualesGenerales.AsQueryable();
|
||||
|
||||
if (anio.HasValue)
|
||||
query = query.Where(r => r.Anio == anio.Value);
|
||||
|
||||
return await query
|
||||
.OrderByDescending(r => r.Anio)
|
||||
.ThenByDescending(r => r.Mes)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> CerrarReporteAsync(long reporteId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reporte = await _context.ReportesMensualesGenerales.FindAsync(reporteId);
|
||||
if (reporte == null) return false;
|
||||
|
||||
_context.Entry(reporte).Property(x => x.Cerrado).CurrentValue = true;
|
||||
_context.Entry(reporte).Property(x => x.Cerrado).IsModified = true;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Movimientos ====================
|
||||
|
||||
public async Task<bool> GuardarMovimientosBulkAsync(long reporteId, List<MovimientoGeneral> movimientos)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reporte = await _context.ReportesMensualesGenerales.FindAsync(reporteId);
|
||||
if (reporte == null || reporte.Cerrado)
|
||||
return false;
|
||||
|
||||
foreach (var movimiento in movimientos)
|
||||
{
|
||||
movimiento.ReporteMensualGeneralId = reporteId;
|
||||
movimiento.Fecha = DateTime.SpecifyKind(movimiento.Fecha, DateTimeKind.Utc);
|
||||
|
||||
if (movimiento.Id > 0)
|
||||
{
|
||||
// Update existing
|
||||
var existente = await _context.MovimientosGenerales.FindAsync(movimiento.Id);
|
||||
if (existente != null)
|
||||
{
|
||||
existente.Tipo = movimiento.Tipo;
|
||||
existente.CategoriaIngresoId = movimiento.CategoriaIngresoId;
|
||||
existente.CategoriaEgresoId = movimiento.CategoriaEgresoId;
|
||||
existente.Monto = movimiento.Monto;
|
||||
existente.Fecha = movimiento.Fecha;
|
||||
existente.Descripcion = movimiento.Descripcion;
|
||||
existente.NumeroComprobante = movimiento.NumeroComprobante;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Insert new
|
||||
_context.MovimientosGenerales.Add(movimiento);
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<decimal> CalcularSaldoActualAsync(long reporteId)
|
||||
{
|
||||
var reporte = await _context.ReportesMensualesGenerales
|
||||
.Include(r => r.Movimientos)
|
||||
.FirstOrDefaultAsync(r => r.Id == reporteId);
|
||||
|
||||
if (reporte == null) return 0;
|
||||
|
||||
var totalIngresos = reporte.Movimientos
|
||||
.Where(m => m.Tipo == (int) TipoMovimientoGeneral.Ingreso)
|
||||
.Sum(m => m.Monto);
|
||||
|
||||
var totalEgresos = reporte.Movimientos
|
||||
.Where(m => m.Tipo == (int)TipoMovimientoGeneral.Egreso)
|
||||
.Sum(m => m.Monto);
|
||||
|
||||
return reporte.SaldoInicial + totalIngresos - totalEgresos;
|
||||
}
|
||||
|
||||
// ==================== Consolidados ====================
|
||||
|
||||
public async Task<Dictionary<string, decimal>> ObtenerConsolidadoIngresosAsync(long reporteId)
|
||||
{
|
||||
var movimientos = await _context.MovimientosGenerales
|
||||
.Include(m => m.CategoriaIngreso)
|
||||
.Where(m => m.ReporteMensualGeneralId == reporteId
|
||||
&& m.Tipo == (int)TipoMovimientoGeneral.Ingreso)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
return movimientos
|
||||
.GroupBy(m => m.CategoriaIngreso?.Nombre ?? "Sin Categoría")
|
||||
.ToDictionary(g => g.Key, g => g.Sum(m => m.Monto));
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, decimal>> ObtenerConsolidadoEgresosAsync(long reporteId)
|
||||
{
|
||||
var movimientos = await _context.MovimientosGenerales
|
||||
.Include(m => m.CategoriaEgreso)
|
||||
.Where(m => m.ReporteMensualGeneralId == reporteId
|
||||
&& m.Tipo == (int)TipoMovimientoGeneral.Egreso)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
return movimientos
|
||||
.GroupBy(m => m.CategoriaEgreso?.Nombre ?? "Sin Categoría")
|
||||
.ToDictionary(g => g.Key, g => g.Sum(m => m.Monto));
|
||||
}
|
||||
|
||||
// ==================== Adjuntos ====================
|
||||
|
||||
public async Task<List<MovimientoGeneralAdjunto>> ObtenerAdjuntosMovimientoAsync(long movimientoId)
|
||||
{
|
||||
return await _context.MovimientosGeneralesAdjuntos
|
||||
.Where(a => a.MovimientoGeneralId == movimientoId)
|
||||
.OrderByDescending(a => a.FechaSubida)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<MovimientoGeneralAdjunto?> CrearAdjuntoAsync(long movimientoId, string nombreArchivo, string rutaArchivo, string tipoContenido)
|
||||
{
|
||||
var movimiento = await _context.MovimientosGenerales.FindAsync(movimientoId);
|
||||
if (movimiento == null) return null;
|
||||
|
||||
var adjunto = new MovimientoGeneralAdjunto
|
||||
{
|
||||
MovimientoGeneralId = movimientoId,
|
||||
NombreArchivo = nombreArchivo,
|
||||
RutaArchivo = rutaArchivo,
|
||||
TipoContenido = tipoContenido,
|
||||
FechaSubida = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.MovimientosGeneralesAdjuntos.Add(adjunto);
|
||||
await _context.SaveChangesAsync();
|
||||
return adjunto;
|
||||
}
|
||||
|
||||
public async Task<bool> EliminarAdjuntoAsync(long adjuntoId)
|
||||
{
|
||||
var adjunto = await _context.MovimientosGeneralesAdjuntos.FindAsync(adjuntoId);
|
||||
if (adjunto == null) return false;
|
||||
|
||||
_context.MovimientosGeneralesAdjuntos.Remove(adjunto);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
169
RS_system/Services/ContabilidadService.cs
Normal file
169
RS_system/Services/ContabilidadService.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class ContabilidadService : IContabilidadService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public ContabilidadService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<ContabilidadRegistro> CrearRegistroAsync(ContabilidadRegistro registro)
|
||||
{
|
||||
// Ensure Group exists
|
||||
var groupExists = await _context.GruposTrabajo.AnyAsync(g => g.Id == registro.GrupoTrabajoId);
|
||||
if (!groupExists)
|
||||
{
|
||||
throw new ArgumentException($"Grupo de trabajo con ID {registro.GrupoTrabajoId} no existe.");
|
||||
}
|
||||
|
||||
_context.ContabilidadRegistros.Add(registro);
|
||||
await _context.SaveChangesAsync();
|
||||
return registro;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ContabilidadRegistro>> ObtenerRegistrosAsync(long grupoId, DateTime desde, DateTime hasta)
|
||||
{
|
||||
return await _context.ContabilidadRegistros
|
||||
.Include(c => c.GrupoTrabajo)
|
||||
.Where(c => c.GrupoTrabajoId == grupoId && c.Fecha.Date >= desde.Date && c.Fecha.Date <= hasta.Date)
|
||||
.OrderByDescending(c => c.Fecha)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<ReporteMensualContable?> ObtenerReporteMensualAsync(long grupoId, int mes, int anio)
|
||||
{
|
||||
return await _context.ReportesMensualesContables
|
||||
.Include(r => r.Registros)
|
||||
.FirstOrDefaultAsync(r => r.GrupoTrabajoId == grupoId && r.Mes == mes && r.Anio == anio);
|
||||
}
|
||||
|
||||
public async Task<ReporteMensualContable> ObtenerOCrearReporteMensualAsync(long grupoId, int mes, int anio)
|
||||
{
|
||||
var reporte = await ObtenerReporteMensualAsync(grupoId, mes, anio);
|
||||
if (reporte != null) return reporte;
|
||||
|
||||
// Calculate Saldo Inicial based on previous month
|
||||
decimal saldoInicial = 0;
|
||||
var prevMes = mes == 1 ? 12 : mes - 1;
|
||||
var prevAnio = mes == 1 ? anio - 1 : anio;
|
||||
|
||||
var reportePrevio = await ObtenerReporteMensualAsync(grupoId, prevMes, prevAnio);
|
||||
if (reportePrevio != null)
|
||||
{
|
||||
saldoInicial = await CalcularSaldoActualAsync(reportePrevio.Id);
|
||||
}
|
||||
|
||||
reporte = new ReporteMensualContable
|
||||
{
|
||||
GrupoTrabajoId = grupoId,
|
||||
Mes = mes,
|
||||
Anio = anio,
|
||||
SaldoInicial = saldoInicial,
|
||||
FechaCreacion = DateTime.UtcNow,
|
||||
Cerrado = false
|
||||
};
|
||||
|
||||
_context.ReportesMensualesContables.Add(reporte);
|
||||
await _context.SaveChangesAsync();
|
||||
return reporte;
|
||||
}
|
||||
|
||||
public async Task<List<ReporteMensualContable>> ListarReportesPorGrupoAsync(long grupoId)
|
||||
{
|
||||
return await _context.ReportesMensualesContables
|
||||
.Where(r => r.GrupoTrabajoId == grupoId)
|
||||
.OrderByDescending(r => r.Anio)
|
||||
.ThenByDescending(r => r.Mes)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> GuardarRegistrosBulkAsync(long reporteId, List<ContabilidadRegistro> registros)
|
||||
{
|
||||
var reporte = await _context.ReportesMensualesContables
|
||||
.Include(r => r.Registros)
|
||||
.FirstOrDefaultAsync(r => r.Id == reporteId);
|
||||
|
||||
if (reporte == null || reporte.Cerrado) return false;
|
||||
try
|
||||
{
|
||||
// Remove existing records for this report (or handle updates carefully)
|
||||
// For a simple bulk entry system, we might replace all or upsert by ID.
|
||||
// Let's go with UPSERT based on ID.
|
||||
|
||||
var existingIds = reporte.Registros.Select(r => r.Id).ToList();
|
||||
var incomingIds = registros.Where(r => r.Id > 0).Select(r => r.Id).ToList();
|
||||
|
||||
// Delete records that are no longer in the list
|
||||
var toDelete = reporte.Registros.Where(r => !incomingIds.Contains(r.Id)).ToList();
|
||||
_context.ContabilidadRegistros.RemoveRange(toDelete);
|
||||
|
||||
foreach (var registro in registros)
|
||||
{
|
||||
if (registro.Id > 0)
|
||||
{
|
||||
// Update
|
||||
var existing = reporte.Registros.FirstOrDefault(r => r.Id == registro.Id);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Tipo = registro.Tipo;
|
||||
existing.Monto = registro.Monto;
|
||||
existing.Fecha = registro.Fecha;
|
||||
existing.Descripcion = registro.Descripcion;
|
||||
_context.Entry(existing).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add
|
||||
registro.ReporteMensualId = reporteId;
|
||||
registro.GrupoTrabajoId = reporte.GrupoTrabajoId;
|
||||
_context.ContabilidadRegistros.Add(registro);
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<decimal> CalcularSaldoActualAsync(long reporteId)
|
||||
{
|
||||
var reporte = await _context.ReportesMensualesContables
|
||||
.Include(r => r.Registros)
|
||||
.FirstOrDefaultAsync(r => r.Id == reporteId);
|
||||
|
||||
if (reporte == null) return 0;
|
||||
|
||||
decimal ingresos = reporte.Registros
|
||||
.Where(r => r.Tipo == TipoMovimientoContable.Ingreso)
|
||||
.Sum(r => r.Monto);
|
||||
|
||||
decimal egresos = reporte.Registros
|
||||
.Where(r => r.Tipo == TipoMovimientoContable.Egreso)
|
||||
.Sum(r => r.Monto);
|
||||
|
||||
return reporte.SaldoInicial + ingresos - egresos;
|
||||
}
|
||||
|
||||
public async Task<bool> CerrarReporteAsync(long reporteId)
|
||||
{
|
||||
var reporte = await _context.ReportesMensualesContables.FindAsync(reporteId);
|
||||
if (reporte == null || reporte.Cerrado) return false;
|
||||
|
||||
reporte.Cerrado = true;
|
||||
_context.ReportesMensualesContables.Update(reporte);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
102
RS_system/Services/EstadoArticuloService.cs
Normal file
102
RS_system/Services/EstadoArticuloService.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class EstadoArticuloService : IEstadoArticuloService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public EstadoArticuloService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<EstadoArticulo>> GetAllAsync()
|
||||
{
|
||||
return await _context.EstadosArticulos
|
||||
.Where(e => !e.Eliminado)
|
||||
.OrderBy(e => e.Nombre)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<EstadoArticulo?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.EstadosArticulos
|
||||
.FirstOrDefaultAsync(e => e.Id == id && !e.Eliminado);
|
||||
}
|
||||
|
||||
public async Task<bool> CreateAsync(EstadoArticulo estado)
|
||||
{
|
||||
try
|
||||
{
|
||||
estado.CreadoEn = DateTime.UtcNow;
|
||||
estado.ActualizadoEn = DateTime.UtcNow;
|
||||
estado.Eliminado = false;
|
||||
|
||||
_context.EstadosArticulos.Add(estado);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(EstadoArticulo estado)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await _context.EstadosArticulos.FindAsync(estado.Id);
|
||||
if (existing == null || existing.Eliminado) return false;
|
||||
|
||||
existing.Nombre = estado.Nombre;
|
||||
existing.Descripcion = estado.Descripcion;
|
||||
existing.Color = estado.Color;
|
||||
existing.Activo = estado.Activo;
|
||||
existing.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.EstadosArticulos.Update(existing);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var estado = await _context.EstadosArticulos.FindAsync(id);
|
||||
if (estado == null || estado.Eliminado) return false;
|
||||
|
||||
estado.Eliminado = true;
|
||||
estado.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.EstadosArticulos.Update(estado);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string nombre, int? excludeId = null)
|
||||
{
|
||||
var query = _context.EstadosArticulos.AsQueryable();
|
||||
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.Id != excludeId.Value);
|
||||
}
|
||||
|
||||
return await query.AnyAsync(e => e.Nombre.ToLower() == nombre.ToLower() && !e.Eliminado);
|
||||
}
|
||||
}
|
||||
18
RS_system/Services/IArticuloService.cs
Normal file
18
RS_system/Services/IArticuloService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Rs_system.Models.ViewModels;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IArticuloService
|
||||
{
|
||||
Task<IEnumerable<ArticuloViewModel>> GetAllAsync(string? search = null, int? categoriaId = null, int? ubicacionId = null, int? estadoId = null);
|
||||
Task<ArticuloViewModel?> GetByIdAsync(int id);
|
||||
Task<bool> CreateAsync(ArticuloViewModel viewModel, string createdBy);
|
||||
Task<bool> UpdateAsync(ArticuloViewModel viewModel);
|
||||
Task<bool> DeleteAsync(int id);
|
||||
Task<bool> ExistsCodigoAsync(string codigo, int? excludeId = null);
|
||||
|
||||
// Dropdown helpers
|
||||
Task<IEnumerable<(int Id, string Nombre)>> GetCategoriasAsync();
|
||||
Task<IEnumerable<(int Id, string Nombre, string Color)>> GetEstadosAsync();
|
||||
Task<IEnumerable<(int Id, string Nombre)>> GetUbicacionesAsync();
|
||||
}
|
||||
13
RS_system/Services/ICategoriaService.cs
Normal file
13
RS_system/Services/ICategoriaService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface ICategoriaService
|
||||
{
|
||||
Task<IEnumerable<Categoria>> GetAllAsync();
|
||||
Task<Categoria?> GetByIdAsync(int id);
|
||||
Task<bool> CreateAsync(Categoria categoria);
|
||||
Task<bool> UpdateAsync(Categoria categoria);
|
||||
Task<bool> DeleteAsync(int id);
|
||||
Task<bool> ExistsAsync(string nombre, int? excludeId = null);
|
||||
}
|
||||
21
RS_system/Services/IColaboracionService.cs
Normal file
21
RS_system/Services/IColaboracionService.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Models.ViewModels;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IColaboracionService
|
||||
{
|
||||
// Tipos de colaboración
|
||||
Task<List<TipoColaboracion>> GetTiposActivosAsync();
|
||||
Task<TipoColaboracion?> GetTipoByIdAsync(long id);
|
||||
|
||||
// Colaboraciones
|
||||
Task<Colaboracion> RegistrarColaboracionAsync(RegistrarColaboracionViewModel model, string registradoPor);
|
||||
Task<List<Colaboracion>> GetColaboracionesRecientesAsync(int cantidad = 50);
|
||||
Task<Colaboracion?> GetColaboracionByIdAsync(long id);
|
||||
|
||||
// Reportes
|
||||
Task<ReporteColaboracionesViewModel> GenerarReportePorFechasAsync(DateTime fechaInicio, DateTime fechaFin);
|
||||
Task<EstadoCuentaViewModel> GenerarEstadoCuentaAsync(long miembroId);
|
||||
Task<List<UltimoPagoViewModel>> GetUltimosPagosPorMiembroAsync(long miembroId);
|
||||
}
|
||||
39
RS_system/Services/IContabilidadGeneralService.cs
Normal file
39
RS_system/Services/IContabilidadGeneralService.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IContabilidadGeneralService
|
||||
{
|
||||
// Categorías de Ingreso
|
||||
Task<List<CategoriaIngreso>> ObtenerCategoriasIngresoAsync();
|
||||
Task<CategoriaIngreso?> ObtenerCategoriaIngresoPorIdAsync(long id);
|
||||
Task<CategoriaIngreso> CrearCategoriaIngresoAsync(CategoriaIngreso categoria);
|
||||
Task<bool> ActualizarCategoriaIngresoAsync(CategoriaIngreso categoria);
|
||||
Task<bool> EliminarCategoriaIngresoAsync(long id);
|
||||
|
||||
// Categorías de Egreso
|
||||
Task<List<CategoriaEgreso>> ObtenerCategoriasEgresoAsync();
|
||||
Task<CategoriaEgreso?> ObtenerCategoriaEgresoPorIdAsync(long id);
|
||||
Task<CategoriaEgreso> CrearCategoriaEgresoAsync(CategoriaEgreso categoria);
|
||||
Task<bool> ActualizarCategoriaEgresoAsync(CategoriaEgreso categoria);
|
||||
Task<bool> EliminarCategoriaEgresoAsync(long id);
|
||||
|
||||
// Reportes Mensuales
|
||||
Task<ReporteMensualGeneral?> ObtenerReporteMensualAsync(int mes, int anio);
|
||||
Task<ReporteMensualGeneral> ObtenerOCrearReporteMensualAsync(int mes, int anio);
|
||||
Task<List<ReporteMensualGeneral>> ListarReportesAsync(int? anio = null);
|
||||
Task<bool> CerrarReporteAsync(long reporteId);
|
||||
|
||||
// Movimientos
|
||||
Task<bool> GuardarMovimientosBulkAsync(long reporteId, List<MovimientoGeneral> movimientos);
|
||||
Task<decimal> CalcularSaldoActualAsync(long reporteId);
|
||||
|
||||
// Consolidados
|
||||
Task<Dictionary<string, decimal>> ObtenerConsolidadoIngresosAsync(long reporteId);
|
||||
Task<Dictionary<string, decimal>> ObtenerConsolidadoEgresosAsync(long reporteId);
|
||||
|
||||
// Adjuntos
|
||||
Task<List<MovimientoGeneralAdjunto>> ObtenerAdjuntosMovimientoAsync(long movimientoId);
|
||||
Task<MovimientoGeneralAdjunto?> CrearAdjuntoAsync(long movimientoId, string nombreArchivo, string rutaArchivo, string tipoContenido);
|
||||
Task<bool> EliminarAdjuntoAsync(long adjuntoId);
|
||||
}
|
||||
18
RS_system/Services/IContabilidadService.cs
Normal file
18
RS_system/Services/IContabilidadService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IContabilidadService
|
||||
{
|
||||
Task<ContabilidadRegistro> CrearRegistroAsync(ContabilidadRegistro registro);
|
||||
Task<IReadOnlyList<ContabilidadRegistro>> ObtenerRegistrosAsync(long grupoId, DateTime desde, DateTime hasta);
|
||||
|
||||
// Monthly Report Methods
|
||||
Task<ReporteMensualContable?> ObtenerReporteMensualAsync(long grupoId, int mes, int anio);
|
||||
Task<ReporteMensualContable> ObtenerOCrearReporteMensualAsync(long grupoId, int mes, int anio);
|
||||
Task<List<ReporteMensualContable>> ListarReportesPorGrupoAsync(long grupoId);
|
||||
Task<bool> GuardarRegistrosBulkAsync(long reporteId, List<ContabilidadRegistro> registros);
|
||||
Task<decimal> CalcularSaldoActualAsync(long reporteId);
|
||||
Task<bool> CerrarReporteAsync(long reporteId);
|
||||
|
||||
}
|
||||
13
RS_system/Services/IEstadoArticuloService.cs
Normal file
13
RS_system/Services/IEstadoArticuloService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IEstadoArticuloService
|
||||
{
|
||||
Task<IEnumerable<EstadoArticulo>> GetAllAsync();
|
||||
Task<EstadoArticulo?> GetByIdAsync(int id);
|
||||
Task<bool> CreateAsync(EstadoArticulo estado);
|
||||
Task<bool> UpdateAsync(EstadoArticulo estado);
|
||||
Task<bool> DeleteAsync(int id);
|
||||
Task<bool> ExistsAsync(string nombre, int? excludeId = null);
|
||||
}
|
||||
21
RS_system/Services/IMovimientoService.cs
Normal file
21
RS_system/Services/IMovimientoService.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IMovimientoService
|
||||
{
|
||||
Task<IEnumerable<MovimientoInventario>> GetHistorialGeneralAsync(int limit = 100);
|
||||
Task<IEnumerable<MovimientoInventario>> GetHistorialPorArticuloAsync(int articuloId);
|
||||
|
||||
// Legacy wrappers (Quantity = 1)
|
||||
Task<bool> RegistrarTrasladoAsync(int articuloId, int nuevaUbicacionId, string observacion, string usuario);
|
||||
Task<bool> RegistrarBajaAsync(int articuloId, string motivo, string usuario);
|
||||
|
||||
// New Quantity-Aware Methods
|
||||
Task<bool> RegistrarTrasladoCantidadAsync(int articuloId, int nuevaUbicacionId, int cantidad, string observacion, string usuario);
|
||||
Task<bool> RegistrarBajaCantidadAsync(int articuloId, int cantidad, string motivo, string usuario);
|
||||
|
||||
Task<bool> RegistrarCambioEstadoAsync(int articuloId, int nuevoEstadoId, string observacion, string usuario);
|
||||
Task<bool> RegistrarPrestamoAsync(int articuloId, int cantidad, string personaNombre, string? personaIdentificacion, DateTime? fechaDevolucionEstimada, string observacion, string usuario);
|
||||
Task<bool> RegistrarEntradaCantidadAsync(int articuloId, int cantidad, string observacion, string usuario);
|
||||
}
|
||||
13
RS_system/Services/IPrestamoService.cs
Normal file
13
RS_system/Services/IPrestamoService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IPrestamoService
|
||||
{
|
||||
Task<IEnumerable<Prestamo>> GetHistorialPrestamosAsync(int limit = 100);
|
||||
Task<IEnumerable<Prestamo>> GetPrestamosActivosAsync();
|
||||
Task<Prestamo?> GetPrestamoByIdAsync(long id);
|
||||
Task<bool> RegistrarPrestamoAsync(int articuloId, int cantidad, string personaNombre, string? personaIdentificacion, DateTime? fechaDevolucionEstimada, string observacion, string usuario);
|
||||
Task<bool> RegistrarDevolucionAsync(long prestamoId, string observacion, string usuario);
|
||||
Task<bool> RegistrarDevolucionParcialAsync(long prestamoId, List<string> codigosDevolucion, string observacion, string usuario);
|
||||
}
|
||||
13
RS_system/Services/IUbicacionService.cs
Normal file
13
RS_system/Services/IUbicacionService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public interface IUbicacionService
|
||||
{
|
||||
Task<IEnumerable<Ubicacion>> GetAllAsync();
|
||||
Task<Ubicacion?> GetByIdAsync(int id);
|
||||
Task<bool> CreateAsync(Ubicacion ubicacion);
|
||||
Task<bool> UpdateAsync(Ubicacion ubicacion);
|
||||
Task<bool> DeleteAsync(int id);
|
||||
Task<bool> ExistsAsync(string nombre, int? excludeId = null);
|
||||
}
|
||||
450
RS_system/Services/MovimientoService.cs
Normal file
450
RS_system/Services/MovimientoService.cs
Normal file
@@ -0,0 +1,450 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class MovimientoService : IMovimientoService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public MovimientoService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MovimientoInventario>> GetHistorialGeneralAsync(int limit = 100)
|
||||
{
|
||||
return await _context.MovimientosInventario
|
||||
.Include(m => m.Articulo)
|
||||
.Include(m => m.UbicacionOrigen)
|
||||
.Include(m => m.UbicacionDestino)
|
||||
.Include(m => m.EstadoAnterior)
|
||||
.Include(m => m.EstadoNuevo)
|
||||
.OrderByDescending(m => m.Fecha)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MovimientoInventario>> GetHistorialPorArticuloAsync(int articuloId)
|
||||
{
|
||||
return await _context.MovimientosInventario
|
||||
.Include(m => m.UbicacionOrigen)
|
||||
.Include(m => m.UbicacionDestino)
|
||||
.Include(m => m.EstadoAnterior)
|
||||
.Include(m => m.EstadoNuevo)
|
||||
.Where(m => m.ArticuloId == articuloId)
|
||||
.OrderByDescending(m => m.Fecha)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> RegistrarTrasladoAsync(int articuloId, int nuevaUbicacionId, string observacion, string usuario)
|
||||
{
|
||||
return await RegistrarTrasladoCantidadAsync(articuloId, nuevaUbicacionId, 1, observacion, usuario);
|
||||
}
|
||||
|
||||
public async Task<bool> RegistrarTrasladoCantidadAsync(
|
||||
int articuloId,
|
||||
int nuevaUbicacionId,
|
||||
int cantidad,
|
||||
string observacion,
|
||||
string usuario)
|
||||
{
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var articulo = await _context.Articulos.FindAsync(articuloId);
|
||||
if (articulo == null) return false;
|
||||
|
||||
var fecha = DateTime.UtcNow;
|
||||
|
||||
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
|
||||
{
|
||||
// ===== LOTE =====
|
||||
var origenExistencia = await _context.Existencias
|
||||
.FirstOrDefaultAsync(e =>
|
||||
e.ArticuloId == articuloId &&
|
||||
e.UbicacionId == articulo.UbicacionId);
|
||||
|
||||
if (origenExistencia == null || origenExistencia.Cantidad < cantidad)
|
||||
return false;
|
||||
|
||||
origenExistencia.Cantidad -= cantidad;
|
||||
origenExistencia.ActualizadoEn = fecha;
|
||||
_context.Existencias.Update(origenExistencia);
|
||||
|
||||
|
||||
var destinoExistencia = await _context.Existencias
|
||||
.FirstOrDefaultAsync(e =>
|
||||
e.ArticuloId == articuloId &&
|
||||
e.UbicacionId == nuevaUbicacionId);
|
||||
|
||||
if (destinoExistencia == null)
|
||||
{
|
||||
destinoExistencia = new Existencia
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
UbicacionId = nuevaUbicacionId,
|
||||
Cantidad = 0,
|
||||
ActualizadoEn = fecha
|
||||
};
|
||||
_context.Existencias.Add(destinoExistencia);
|
||||
}
|
||||
|
||||
destinoExistencia.Cantidad += cantidad;
|
||||
destinoExistencia.ActualizadoEn = fecha;
|
||||
|
||||
// 📉 MOVIMIENTO SALIDA
|
||||
var movSalida = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.TRASLADO),
|
||||
Fecha = fecha,
|
||||
UbicacionOrigenId = articulo.UbicacionId,
|
||||
Cantidad = cantidad,
|
||||
TipMov = 2,
|
||||
Observacion = observacion,
|
||||
UsuarioId = usuario,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
EstadoNuevoId = articulo.EstadoId
|
||||
};
|
||||
|
||||
// 📈 MOVIMIENTO ENTRADA
|
||||
var movEntrada = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.TRASLADO),
|
||||
Fecha = fecha,
|
||||
UbicacionDestinoId = nuevaUbicacionId,
|
||||
Cantidad = cantidad,
|
||||
TipMov = 1,
|
||||
Observacion = observacion,
|
||||
UsuarioId = usuario,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
EstadoNuevoId = articulo.EstadoId
|
||||
};
|
||||
|
||||
_context.MovimientosInventario.AddRange(movSalida, movEntrada);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ===== UNITARIO =====
|
||||
if (articulo.UbicacionId == nuevaUbicacionId)
|
||||
return false;
|
||||
|
||||
// 📉 SALIDA
|
||||
var movSalida = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.TRASLADO),
|
||||
Fecha = fecha,
|
||||
UbicacionOrigenId = articulo.UbicacionId,
|
||||
Cantidad = 1,
|
||||
TipMov = 2,
|
||||
Observacion = observacion,
|
||||
UsuarioId = usuario,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
EstadoNuevoId = articulo.EstadoId
|
||||
};
|
||||
|
||||
// 📈 ENTRADA
|
||||
var movEntrada = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.TRASLADO),
|
||||
Fecha = fecha,
|
||||
UbicacionDestinoId = nuevaUbicacionId,
|
||||
Cantidad = 1,
|
||||
TipMov = 1,
|
||||
Observacion = observacion,
|
||||
UsuarioId = usuario,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
EstadoNuevoId = articulo.EstadoId
|
||||
};
|
||||
|
||||
articulo.UbicacionId = nuevaUbicacionId;
|
||||
articulo.ActualizadoEn = fecha;
|
||||
|
||||
_context.Articulos.Update(articulo);
|
||||
_context.MovimientosInventario.AddRange(movSalida, movEntrada);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> RegistrarCambioEstadoAsync(int articuloId, int nuevoEstadoId, string observacion, string usuario)
|
||||
{
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var articulo = await _context.Articulos.FindAsync(articuloId);
|
||||
if (articulo == null) return false;
|
||||
|
||||
if (articulo.EstadoId == nuevoEstadoId) return false;
|
||||
|
||||
var movimiento = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.CAMBIO_ESTADO),
|
||||
Fecha = DateTime.UtcNow,
|
||||
UbicacionOrigenId = articulo.UbicacionId,
|
||||
UbicacionDestinoId = articulo.UbicacionId,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
EstadoNuevoId = nuevoEstadoId,
|
||||
Cantidad = (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE)) ? articulo.CantidadGlobal : 1,
|
||||
Observacion = observacion,
|
||||
UsuarioId = usuario
|
||||
};
|
||||
|
||||
articulo.EstadoId = nuevoEstadoId;
|
||||
articulo.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.MovimientosInventario.Add(movimiento);
|
||||
_context.Articulos.Update(articulo);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> RegistrarPrestamoAsync(int articuloId, int cantidad, string personaNombre, string? personaIdentificacion,
|
||||
DateTime? fechaDevolucionEstimada, string observacion, string usuario)
|
||||
{
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var articulo = await _context.Articulos.FindAsync(articuloId);
|
||||
if (articulo == null) return false;
|
||||
|
||||
// Validar stock disponible
|
||||
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
|
||||
{
|
||||
var existencia = await _context.Existencias
|
||||
.FirstOrDefaultAsync(e => e.ArticuloId == articuloId && e.UbicacionId == articulo.UbicacionId);
|
||||
|
||||
if (existencia == null || existencia.Cantidad < cantidad) return false;
|
||||
|
||||
// Reducir stock existente
|
||||
existencia.Cantidad -= cantidad;
|
||||
articulo.CantidadGlobal -= cantidad;
|
||||
|
||||
if (articulo.CantidadGlobal < 0) articulo.CantidadGlobal = 0;
|
||||
if (existencia.Cantidad < 0) existencia.Cantidad = 0;
|
||||
|
||||
_context.Existencias.Update(existencia);
|
||||
_context.Articulos.Update(articulo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// UNITARIO - Solo se puede prestar 1
|
||||
if (cantidad != 1) return false;
|
||||
if (!articulo.Activo) return false;
|
||||
}
|
||||
|
||||
// Crear movimiento de inventario
|
||||
var movimiento = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.PRESTAMO),
|
||||
TipMov = 2,
|
||||
Fecha = DateTime.UtcNow,
|
||||
UbicacionOrigenId = articulo.UbicacionId,
|
||||
UbicacionDestinoId = articulo.UbicacionId, // Mismo lugar, solo está prestado
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
EstadoNuevoId = articulo.EstadoId,
|
||||
Cantidad = cantidad,
|
||||
Observacion = $"Préstamo a {personaNombre}. {observacion}",
|
||||
UsuarioId = usuario
|
||||
};
|
||||
|
||||
_context.MovimientosInventario.Add(movimiento);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> RegistrarBajaAsync(int articuloId, string motivo, string usuario)
|
||||
{
|
||||
return await RegistrarBajaCantidadAsync(articuloId, 1, motivo, usuario);
|
||||
}
|
||||
|
||||
public async Task<bool> RegistrarBajaCantidadAsync(int articuloId, int cantidad, string motivo, string usuario)
|
||||
{
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var articulo = await _context.Articulos.FindAsync(articuloId);
|
||||
if (articulo == null) return false;
|
||||
|
||||
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
|
||||
{
|
||||
var existencia = await _context.Existencias
|
||||
.FirstOrDefaultAsync(e => e.ArticuloId == articuloId && e.UbicacionId == articulo.UbicacionId);
|
||||
|
||||
if (existencia == null || existencia.Cantidad < cantidad) return false;
|
||||
|
||||
existencia.Cantidad -= cantidad;
|
||||
articulo.CantidadGlobal -= cantidad;
|
||||
|
||||
if (articulo.CantidadGlobal <= 0) articulo.Activo = false;
|
||||
|
||||
var movimiento = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.BAJA),
|
||||
TipMov = 2,
|
||||
Fecha = DateTime.UtcNow,
|
||||
UbicacionOrigenId = articulo.UbicacionId,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
Cantidad = cantidad,
|
||||
Observacion = motivo,
|
||||
UsuarioId = usuario
|
||||
};
|
||||
|
||||
_context.Existencias.Update(existencia);
|
||||
_context.MovimientosInventario.Add(movimiento);
|
||||
_context.Articulos.Update(articulo);
|
||||
}
|
||||
else
|
||||
{
|
||||
var movimiento = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.BAJA),
|
||||
TipMov = 2,
|
||||
Fecha = DateTime.UtcNow,
|
||||
UbicacionOrigenId = articulo.UbicacionId,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
Cantidad = 1,
|
||||
Observacion = motivo,
|
||||
UsuarioId = usuario
|
||||
};
|
||||
|
||||
articulo.Activo = false;
|
||||
_context.MovimientosInventario.Add(movimiento);
|
||||
_context.Articulos.Update(articulo);
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> RegistrarEntradaCantidadAsync(int articuloId, int cantidad, string observacion, string usuario)
|
||||
{
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var articulo = await _context.Articulos.FindAsync(articuloId);
|
||||
if (articulo == null) return false;
|
||||
|
||||
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
|
||||
{
|
||||
var existencia = await _context.Existencias
|
||||
.FirstOrDefaultAsync(e => e.ArticuloId == articuloId && e.UbicacionId == articulo.UbicacionId);
|
||||
|
||||
if (existencia == null)
|
||||
{
|
||||
existencia = new Existencia
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
UbicacionId = articulo.UbicacionId,
|
||||
Cantidad = 0,
|
||||
ActualizadoEn = DateTime.UtcNow
|
||||
};
|
||||
_context.Existencias.Add(existencia);
|
||||
}
|
||||
|
||||
existencia.Cantidad += cantidad;
|
||||
articulo.CantidadGlobal += cantidad;
|
||||
articulo.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.Existencias.Update(existencia);
|
||||
_context.Articulos.Update(articulo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// UNITARIO - Si está inactivo por baja, lo reactivamos?
|
||||
// En unitario, una entrada suele ser que el objeto "vuelve" a estar disponible.
|
||||
articulo.Activo = true;
|
||||
articulo.ActualizadoEn = DateTime.UtcNow;
|
||||
_context.Articulos.Update(articulo);
|
||||
}
|
||||
|
||||
var movimiento = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.ENTRADA),
|
||||
TipMov = 1,
|
||||
Fecha = DateTime.UtcNow,
|
||||
UbicacionDestinoId = articulo.UbicacionId,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
EstadoNuevoId = articulo.EstadoId,
|
||||
Cantidad = cantidad,
|
||||
Observacion = observacion,
|
||||
UsuarioId = usuario
|
||||
};
|
||||
|
||||
_context.MovimientosInventario.Add(movimiento);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
226
RS_system/Services/PrestamoService.cs
Normal file
226
RS_system/Services/PrestamoService.cs
Normal file
@@ -0,0 +1,226 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class PrestamoService : IPrestamoService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public PrestamoService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Prestamo>> GetHistorialPrestamosAsync(int limit = 100)
|
||||
{
|
||||
return await _context.Prestamos
|
||||
.Include(p => p.Articulo)
|
||||
.OrderByDescending(p => p.FechaPrestamo)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Prestamo>> GetPrestamosActivosAsync()
|
||||
{
|
||||
return await _context.Prestamos
|
||||
.Include(p => p.Articulo)
|
||||
.Where(p => p.Estado == "ACTIVO" || p.Estado == "ATRASADO")
|
||||
.OrderByDescending(p => p.FechaPrestamo)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Prestamo?> GetPrestamoByIdAsync(long id)
|
||||
{
|
||||
return await _context.Prestamos
|
||||
.Include(p => p.Articulo)
|
||||
.Include(p => p.Detalles)
|
||||
.FirstOrDefaultAsync(p => p.Id == id);
|
||||
}
|
||||
|
||||
public async Task<bool> RegistrarPrestamoAsync(int articuloId, int cantidad, string personaNombre, string? personaIdentificacion, DateTime? fechaDevolucionEstimada, string observacion, string usuario)
|
||||
{
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var articulo = await _context.Articulos.FindAsync(articuloId);
|
||||
if (articulo == null) return false;
|
||||
|
||||
// 1. Validar y actualizar stock
|
||||
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
|
||||
{
|
||||
var existencia = await _context.Existencias
|
||||
.FirstOrDefaultAsync(e => e.ArticuloId == articuloId && e.UbicacionId == articulo.UbicacionId);
|
||||
|
||||
if (existencia == null || existencia.Cantidad < cantidad) return false;
|
||||
|
||||
existencia.Cantidad -= cantidad;
|
||||
articulo.CantidadGlobal -= cantidad;
|
||||
_context.Existencias.Update(existencia);
|
||||
_context.Articulos.Update(articulo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unitario
|
||||
if (cantidad != 1 || !articulo.Activo) return false;
|
||||
// En unitario, podrías marcar como inactivo o simplemente registrar el préstamo
|
||||
// Para este sistema, asumiremos que prestado sigue siendo "Activo" pero en una ubicación de préstamo (vía movimiento)
|
||||
}
|
||||
|
||||
// 2. Crear el registro de préstamo
|
||||
var prestamo = new Prestamo
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
Cantidad = cantidad,
|
||||
PersonaNombre = personaNombre,
|
||||
PersonaIdentificacion = personaIdentificacion,
|
||||
FechaPrestamo = DateTime.UtcNow,
|
||||
FechaDevolucionEstimada = fechaDevolucionEstimada,
|
||||
Estado = "ACTIVO",
|
||||
Observacion = observacion,
|
||||
UsuarioId = usuario
|
||||
};
|
||||
|
||||
_context.Prestamos.Add(prestamo);
|
||||
await _context.SaveChangesAsync(); // Guardamos para tener el ID del préstamo
|
||||
|
||||
// 3. Crear movimiento de inventario (auditoría)
|
||||
var movimiento = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articuloId,
|
||||
TipoMovimiento = nameof(TipoMovimiento.PRESTAMO),
|
||||
TipMov = 2,
|
||||
Fecha = DateTime.UtcNow,
|
||||
UbicacionOrigenId = articulo.UbicacionId,
|
||||
UbicacionDestinoId = articulo.UbicacionId,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
EstadoNuevoId = articulo.EstadoId,
|
||||
Cantidad = cantidad,
|
||||
Observacion = $"Préstamo #{prestamo.Id} a {personaNombre}. {observacion}",
|
||||
UsuarioId = usuario
|
||||
};
|
||||
|
||||
_context.MovimientosInventario.Add(movimiento);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> RegistrarDevolucionAsync(
|
||||
long prestamoId,
|
||||
string observacion,
|
||||
string usuario)
|
||||
{
|
||||
var strategy = _context.Database.CreateExecutionStrategy();
|
||||
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var prestamo = await _context.Prestamos.FindAsync(prestamoId);
|
||||
if (prestamo == null)
|
||||
return false;
|
||||
|
||||
if (prestamo.Estado == "DEVUELTO")
|
||||
return false;
|
||||
|
||||
var articulo = await _context.Articulos.FindAsync(prestamo.ArticuloId);
|
||||
if (articulo == null)
|
||||
return false;
|
||||
|
||||
var fechaActual = DateTime.UtcNow;
|
||||
|
||||
if (articulo.TipoControl == nameof(Articulo.TipoControlInventario.LOTE))
|
||||
{
|
||||
// --- Buscar existencia ---
|
||||
var existencia = await _context.Existencias
|
||||
.FirstOrDefaultAsync(e =>
|
||||
e.ArticuloId == articulo.Id &&
|
||||
e.UbicacionId == articulo.UbicacionId);
|
||||
|
||||
// --- Crear existencia si no existe ---
|
||||
if (existencia == null)
|
||||
{
|
||||
existencia = new Existencia
|
||||
{
|
||||
ArticuloId = articulo.Id,
|
||||
UbicacionId = articulo.UbicacionId,
|
||||
Cantidad = 0,
|
||||
ActualizadoEn = fechaActual
|
||||
};
|
||||
_context.Existencias.Add(existencia);
|
||||
}
|
||||
|
||||
// --- Actualizar cantidades ---
|
||||
existencia.Cantidad += prestamo.Cantidad;
|
||||
existencia.ActualizadoEn = fechaActual;
|
||||
|
||||
articulo.CantidadGlobal += prestamo.Cantidad;
|
||||
articulo.ActualizadoEn = fechaActual;
|
||||
|
||||
_context.Existencias.Update(existencia);
|
||||
_context.Articulos.Update(articulo);
|
||||
}
|
||||
else
|
||||
{
|
||||
articulo.Activo = true;
|
||||
articulo.ActualizadoEn = fechaActual;
|
||||
_context.Articulos.Update(articulo);
|
||||
}
|
||||
|
||||
prestamo.Estado = "DEVUELTO";
|
||||
prestamo.FechaDevolucionReal = fechaActual;
|
||||
prestamo.Observacion =
|
||||
$"{prestamo.Observacion}\nDevolución: {observacion}";
|
||||
|
||||
_context.Prestamos.Update(prestamo);
|
||||
|
||||
var movimiento = new MovimientoInventario
|
||||
{
|
||||
ArticuloId = articulo.Id,
|
||||
TipoMovimiento = nameof(TipoMovimiento.DEVOLUCION),
|
||||
TipMov = 1, // ENTRADA
|
||||
Fecha = fechaActual,
|
||||
UbicacionOrigenId = articulo.UbicacionId,
|
||||
UbicacionDestinoId = articulo.UbicacionId,
|
||||
EstadoAnteriorId = articulo.EstadoId,
|
||||
EstadoNuevoId = articulo.EstadoId,
|
||||
Cantidad = prestamo.Cantidad,
|
||||
Observacion = $"Devolución de préstamo #{prestamo.Id}. {observacion}",
|
||||
UsuarioId = usuario
|
||||
};
|
||||
|
||||
_context.MovimientosInventario.Add(movimiento);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> RegistrarDevolucionParcialAsync(long prestamoId, List<string> codigosDevolucion, string observacion, string usuario)
|
||||
{
|
||||
// Implementación básica para seguir la interfaz, aunque el controlador actual no la usa directamente
|
||||
// Esta lógica sería más para artículos unitarios con códigos específicos (PrestamoDetalle)
|
||||
return await RegistrarDevolucionAsync(prestamoId, "Devolución parcial - " + observacion, usuario);
|
||||
}
|
||||
}
|
||||
102
RS_system/Services/UbicacionService.cs
Normal file
102
RS_system/Services/UbicacionService.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Models;
|
||||
|
||||
namespace Rs_system.Services;
|
||||
|
||||
public class UbicacionService : IUbicacionService
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public UbicacionService(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Ubicacion>> GetAllAsync()
|
||||
{
|
||||
return await _context.Ubicaciones
|
||||
.Where(u => !u.Eliminado)
|
||||
.OrderBy(u => u.Nombre)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Ubicacion?> GetByIdAsync(int id)
|
||||
{
|
||||
return await _context.Ubicaciones
|
||||
.FirstOrDefaultAsync(u => u.Id == id && !u.Eliminado);
|
||||
}
|
||||
|
||||
public async Task<bool> CreateAsync(Ubicacion ubicacion)
|
||||
{
|
||||
try
|
||||
{
|
||||
ubicacion.CreadoEn = DateTime.UtcNow;
|
||||
ubicacion.ActualizadoEn = DateTime.UtcNow;
|
||||
ubicacion.Eliminado = false;
|
||||
|
||||
_context.Ubicaciones.Add(ubicacion);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(Ubicacion ubicacion)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await _context.Ubicaciones.FindAsync(ubicacion.Id);
|
||||
if (existing == null || existing.Eliminado) return false;
|
||||
|
||||
existing.Nombre = ubicacion.Nombre;
|
||||
existing.Descripcion = ubicacion.Descripcion;
|
||||
existing.Responsable = ubicacion.Responsable;
|
||||
existing.Activo = ubicacion.Activo;
|
||||
existing.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.Ubicaciones.Update(existing);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ubicacion = await _context.Ubicaciones.FindAsync(id);
|
||||
if (ubicacion == null || ubicacion.Eliminado) return false;
|
||||
|
||||
ubicacion.Eliminado = true;
|
||||
ubicacion.ActualizadoEn = DateTime.UtcNow;
|
||||
|
||||
_context.Ubicaciones.Update(ubicacion);
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsAsync(string nombre, int? excludeId = null)
|
||||
{
|
||||
var query = _context.Ubicaciones.AsQueryable();
|
||||
|
||||
if (excludeId.HasValue)
|
||||
{
|
||||
query = query.Where(u => u.Id != excludeId.Value);
|
||||
}
|
||||
|
||||
return await query.AnyAsync(u => u.Nombre.ToLower() == nombre.ToLower() && !u.Eliminado);
|
||||
}
|
||||
}
|
||||
208
RS_system/Views/Articulos/Create.cshtml
Normal file
208
RS_system/Views/Articulos/Create.cshtml
Normal 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>
|
||||
}
|
||||
128
RS_system/Views/Articulos/Details.cshtml
Normal file
128
RS_system/Views/Articulos/Details.cshtml
Normal 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>
|
||||
215
RS_system/Views/Articulos/Edit.cshtml
Normal file
215
RS_system/Views/Articulos/Edit.cshtml
Normal 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>
|
||||
}
|
||||
141
RS_system/Views/Articulos/Index.cshtml
Normal file
141
RS_system/Views/Articulos/Index.cshtml
Normal 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>
|
||||
}
|
||||
53
RS_system/Views/Categorias/Create.cshtml
Normal file
53
RS_system/Views/Categorias/Create.cshtml
Normal 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");}
|
||||
}
|
||||
54
RS_system/Views/Categorias/Edit.cshtml
Normal file
54
RS_system/Views/Categorias/Edit.cshtml
Normal 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");}
|
||||
}
|
||||
117
RS_system/Views/Categorias/Index.cshtml
Normal file
117
RS_system/Views/Categorias/Index.cshtml
Normal 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>
|
||||
}
|
||||
453
RS_system/Views/Colaboracion/Create.cshtml
Normal file
453
RS_system/Views/Colaboracion/Create.cshtml
Normal 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>
|
||||
}
|
||||
130
RS_system/Views/Colaboracion/Details.cshtml
Normal file
130
RS_system/Views/Colaboracion/Details.cshtml
Normal 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>
|
||||
133
RS_system/Views/Colaboracion/EstadoCuenta.cshtml
Normal file
133
RS_system/Views/Colaboracion/EstadoCuenta.cshtml
Normal 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>
|
||||
}
|
||||
147
RS_system/Views/Colaboracion/Index.cshtml
Normal file
147
RS_system/Views/Colaboracion/Index.cshtml
Normal 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>
|
||||
}
|
||||
150
RS_system/Views/Colaboracion/Reporte.cshtml
Normal file
150
RS_system/Views/Colaboracion/Reporte.cshtml
Normal 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>
|
||||
118
RS_system/Views/Colaboracion/Reportes.cshtml
Normal file
118
RS_system/Views/Colaboracion/Reportes.cshtml
Normal 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"> </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>
|
||||
}
|
||||
64
RS_system/Views/Contabilidad/Create.cshtml
Normal file
64
RS_system/Views/Contabilidad/Create.cshtml
Normal 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");}
|
||||
}
|
||||
140
RS_system/Views/Contabilidad/Index.cshtml
Normal file
140
RS_system/Views/Contabilidad/Index.cshtml
Normal 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>
|
||||
|
||||
261
RS_system/Views/Contabilidad/RegistroMensual.cshtml
Normal file
261
RS_system/Views/Contabilidad/RegistroMensual.cshtml
Normal 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>
|
||||
}
|
||||
164
RS_system/Views/ContabilidadGeneral/Consolidado.cshtml
Normal file
164
RS_system/Views/ContabilidadGeneral/Consolidado.cshtml
Normal file
@@ -0,0 +1,164 @@
|
||||
@model Rs_system.Models.ReporteMensualGeneral
|
||||
@{
|
||||
ViewData["Title"] = $"Consolidado - {Model.NombreMes} {Model.Anio}";
|
||||
var ingresosPorCat = ViewBag.ConsolidadoIngresos as Dictionary<string, decimal> ?? new Dictionary<string, decimal>();
|
||||
var egresosPorCat = ViewBag.ConsolidadoEgresos as Dictionary<string, decimal> ?? new Dictionary<string, decimal>();
|
||||
|
||||
var totalIngresosMes = ingresosPorCat.Values.Sum();
|
||||
var totalEgresosMes = egresosPorCat.Values.Sum();
|
||||
var saldoFinal = Model.SaldoInicial + totalIngresosMes - totalEgresosMes;
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 text-gray-800">Consolidado: @Model.NombreMes @Model.Anio</h1>
|
||||
<div>
|
||||
<a asp-action="RegistroMensual" asp-route-id="@Model.Id" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-edit"></i> Ver Detalle Registros
|
||||
</a>
|
||||
<a asp-action="Index" class="btn btn-secondary btn-sm ml-2">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
<button onclick="window.print()" class="btn btn-info btn-sm ml-2">
|
||||
<i class="fas fa-print"></i> Imprimir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resumen Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Saldo Inicial</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.SaldoInicial.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Total Ingresos</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@totalIngresosMes.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-danger shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Total Egresos</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@totalEgresosMes.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-info shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Saldo Final</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@saldoFinal.ToString("C")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Ingresos Chart/Table -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-success">Desglose de Ingresos</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Categoría</th>
|
||||
<th class="text-right">Monto</th>
|
||||
<th class="text-right">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in ingresosPorCat.OrderByDescending(x => x.Value))
|
||||
{
|
||||
var porcentaje = totalIngresosMes > 0 ? (item.Value / totalIngresosMes) * 100 : 0;
|
||||
<tr>
|
||||
<td>@item.Key</td>
|
||||
<td class="text-right">@item.Value.ToString("C")</td>
|
||||
<td class="text-right">@porcentaje.ToString("F1")%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-weight-bold">
|
||||
<td>Total</td>
|
||||
<td class="text-right">@totalIngresosMes.ToString("C")</td>
|
||||
<td class="text-right">100%</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Egresos Chart/Table -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-danger">Desglose de Egresos</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Categoría</th>
|
||||
<th class="text-right">Monto</th>
|
||||
<th class="text-right">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in egresosPorCat.OrderByDescending(x => x.Value))
|
||||
{
|
||||
var porcentaje = totalEgresosMes > 0 ? (item.Value / totalEgresosMes) * 100 : 0;
|
||||
<tr>
|
||||
<td>@item.Key</td>
|
||||
<td class="text-right">@item.Value.ToString("C")</td>
|
||||
<td class="text-right">@porcentaje.ToString("F1")%</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="font-weight-bold">
|
||||
<td>Total</td>
|
||||
<td class="text-right">@totalEgresosMes.ToString("C")</td>
|
||||
<td class="text-right">100%</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
222
RS_system/Views/ContabilidadGeneral/GestionCategorias.cshtml
Normal file
222
RS_system/Views/ContabilidadGeneral/GestionCategorias.cshtml
Normal file
@@ -0,0 +1,222 @@
|
||||
@{
|
||||
ViewData["Title"] = "Gestión de Categorías";
|
||||
var categoriasIngreso = ViewBag.CategoriasIngreso as List<Rs_system.Models.CategoriaIngreso>;
|
||||
var categoriasEgreso = ViewBag.CategoriasEgreso as List<Rs_system.Models.CategoriaEgreso>;
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 text-gray-800">Gestión de Categorías</h1>
|
||||
<a asp-action="Index" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success">@TempData["Success"]</div>
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<!-- Categorías de Ingreso -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-success">Categorías de Ingreso</h6>
|
||||
<button class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#modalCrearIngreso">
|
||||
<i class="fas fa-plus"></i> Nueva
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Descripción</th>
|
||||
<th style="width: 100px;">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in categoriasIngreso)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Nombre</td>
|
||||
<td>@item.Descripcion</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-primary btn-sm"
|
||||
onclick="editarIngreso(@item.Id, '@item.Nombre', '@item.Descripcion')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form asp-action="EliminarCategoriaIngreso" asp-route-id="@item.Id" method="post" class="d-inline" onsubmit="return confirm('¿Eliminar esta categoría?');">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categorías de Egreso -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-danger">Categorías de Egreso</h6>
|
||||
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#modalCrearEgreso">
|
||||
<i class="fas fa-plus"></i> Nueva
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Descripción</th>
|
||||
<th style="width: 100px;">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in categoriasEgreso)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Nombre</td>
|
||||
<td>@item.Descripcion</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-primary btn-sm"
|
||||
onclick="editarEgreso(@item.Id, '@item.Nombre', '@item.Descripcion')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form asp-action="EliminarCategoriaEgreso" asp-route-id="@item.Id" method="post" class="d-inline" onsubmit="return confirm('¿Eliminar esta categoría?');">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Crear Ingreso -->
|
||||
<div class="modal fade" id="modalCrearIngreso" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Nueva Categoría de Ingreso</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form asp-action="CrearCategoriaIngreso" method="post">
|
||||
<div class="modal-body">
|
||||
<div class="form-group mb-3">
|
||||
<label>Nombre</label>
|
||||
<input name="Nombre" class="form-control" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label>Descripción</label>
|
||||
<input name="Descripcion" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Crear Egreso -->
|
||||
<div class="modal fade" id="modalCrearEgreso" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Nueva Categoría de Egreso</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form asp-action="CrearCategoriaEgreso" method="post">
|
||||
<div class="modal-body">
|
||||
<div class="form-group mb-3">
|
||||
<label>Nombre</label>
|
||||
<input name="Nombre" class="form-control" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label>Descripción</label>
|
||||
<input name="Descripcion" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Editar (Compartido y poblado por JS) -->
|
||||
<div class="modal fade" id="modalEditar" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="tituloEditar">Editar Categoría</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="formEditar" method="post">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="Id" id="editId" />
|
||||
<div class="form-group mb-3">
|
||||
<label>Nombre</label>
|
||||
<input name="Nombre" id="editNombre" class="form-control" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label>Descripción</label>
|
||||
<input name="Descripcion" id="editDescripcion" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Actualizar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function editarIngreso(id, nombre, descripcion) {
|
||||
$('#tituloEditar').text('Editar Categoría de Ingreso');
|
||||
$('#formEditar').attr('action', '@Url.Action("EditarCategoriaIngreso")');
|
||||
llenarModal(id, nombre, descripcion);
|
||||
}
|
||||
|
||||
function editarEgreso(id, nombre, descripcion) {
|
||||
$('#tituloEditar').text('Editar Categoría de Egreso');
|
||||
$('#formEditar').attr('action', '@Url.Action("EditarCategoriaEgreso")');
|
||||
llenarModal(id, nombre, descripcion);
|
||||
}
|
||||
|
||||
function llenarModal(id, nombre, descripcion) {
|
||||
$('#editId').val(id);
|
||||
$('#editNombre').val(nombre);
|
||||
$('#editDescripcion').val(descripcion);
|
||||
|
||||
var modalEl = document.getElementById('modalEditar');
|
||||
var modal = new bootstrap.Modal(modalEl);
|
||||
modal.show();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
124
RS_system/Views/ContabilidadGeneral/Index.cshtml
Normal file
124
RS_system/Views/ContabilidadGeneral/Index.cshtml
Normal file
@@ -0,0 +1,124 @@
|
||||
@model List<Rs_system.Models.ReporteMensualGeneral>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Contabilidad General";
|
||||
var anioActual = ViewBag.Anio;
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 text-gray-800">Contabilidad General</h1>
|
||||
|
||||
<form asp-action="Index" method="get" class="form-inline">
|
||||
<label class="me-2">Año:</label>
|
||||
<select name="anio" class="form-control me-2" asp-items="ViewBag.Anios" onchange="this.form.submit()">
|
||||
<option value="@anioActual" selected>@anioActual</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="alert alert-danger">@TempData["Error"]</div>
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Reportes Mensuales @anioActual</h6>
|
||||
<div>
|
||||
<a asp-action="GestionCategorias" class="btn btn-info btn-sm">
|
||||
<i class="fas fa-tags"></i> Gestionar Categorías
|
||||
</a>
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#abrirMesModal">
|
||||
<i class="fas fa-plus"></i> Abrir Nuevo Mes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mes</th>
|
||||
<th>Saldo Inicial</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.NombreMes</td>
|
||||
<td>@item.SaldoInicial.ToString("C")</td>
|
||||
<td>
|
||||
@if (item.Cerrado)
|
||||
{
|
||||
<span class="badge bg-secondary">Cerrado</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-success">Abierto</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="RegistroMensual" asp-route-id="@item.Id" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-edit"></i> Gestionar
|
||||
</a>
|
||||
<a asp-action="Consolidado" asp-route-id="@item.Id" class="btn btn-info btn-sm">
|
||||
<i class="fas fa-chart-pie"></i> Ver Consolidado
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">No hay reportes para este año.</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Abrir Mes -->
|
||||
<div class="modal fade" id="abrirMesModal" tabindex="-1" role="dialog" aria-labelledby="abrirMesModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="abrirMesModalLabel">Abrir Nuevo Mes Contable</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form asp-action="AbrirMes" method="post">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="anio" value="@anioActual" />
|
||||
<div class="form-group mb-3">
|
||||
<label>Mes</label>
|
||||
<select name="mes" class="form-select" required>
|
||||
@for (int i = 1; i <= 12; i++)
|
||||
{
|
||||
var nMes = new DateTime(2000, i, 1).ToString("MMMM", new System.Globalization.CultureInfo("es-ES"));
|
||||
<option value="@i">@nMes</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-muted small">
|
||||
Nota: Al abrir el mes, se calculará automáticamente el saldo inicial basado en el cierre del mes anterior.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Crear Reporte</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
479
RS_system/Views/ContabilidadGeneral/RegistroMensual.cshtml
Normal file
479
RS_system/Views/ContabilidadGeneral/RegistroMensual.cshtml
Normal file
@@ -0,0 +1,479 @@
|
||||
@model Rs_system.Models.ReporteMensualGeneral
|
||||
@{
|
||||
ViewData["Title"] = $"Registro - {Model.NombreMes} {Model.Anio}";
|
||||
var categoriasIngreso = ViewBag.CategoriasIngreso as List<Rs_system.Models.CategoriaIngreso>;
|
||||
var categoriasEgreso = ViewBag.CategoriasEgreso as List<Rs_system.Models.CategoriaEgreso>;
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h1 class="h3 text-gray-800">Registro Mensual: @Model.NombreMes @Model.Anio</h1>
|
||||
<h5 class="text-secondary">
|
||||
Saldo Actual: <span class="font-weight-bold @(ViewBag.SaldoActual >= 0 ? "text-success" : "text-danger")">@ViewBag.SaldoActual?.ToString("C")</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div>
|
||||
<a asp-action="Index" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
</a>
|
||||
@if (!Model.Cerrado)
|
||||
{
|
||||
<button id="btnGuardar" class="btn btn-primary btn-sm ml-2">
|
||||
<i class="fas fa-save"></i> Guardar Cambios
|
||||
</button>
|
||||
<form asp-action="CerrarMes" asp-route-id="@Model.Id" method="post" class="d-inline ml-2" onsubmit="return confirm('¿Está seguro de cerrar este mes? No podrá realizar más cambios.');">
|
||||
<button type="submit" class="btn btn-warning btn-sm">
|
||||
<i class="fas fa-lock"></i> Cerrar Mes
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge badge-secondary ml-2 p-2">Mes Cerrado</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
@TempData["Success"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
@TempData["Error"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm mb-0" id="tablaRegistros">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th style="width: 50px;">#</th>
|
||||
<th style="width: 120px;">Fecha</th>
|
||||
<th style="width: 120px;">Tipo</th>
|
||||
<th style="width: 200px;">Categoría</th>
|
||||
<th>Descripción</th>
|
||||
<th style="width: 120px;">Comprobante</th>
|
||||
<th style="width: 50px;"></th>
|
||||
<th style="width: 150px;">Monto</th>
|
||||
@if (!Model.Cerrado)
|
||||
{
|
||||
<th style="width: 50px;"></th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbodyRegistros">
|
||||
<!-- Rows rendered by JS -->
|
||||
</tbody>
|
||||
@if (!Model.Cerrado)
|
||||
{
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="9" class="text-center p-2">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="agregarFila()">
|
||||
<i class="fas fa-plus"></i> Agregar Fila
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Adjuntos -->
|
||||
<div class="modal fade" id="modalAdjuntos" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Adjuntos del Movimiento</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="adjuntoMovimientoId" />
|
||||
|
||||
@if (!Model.Cerrado)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subir Archivos (Imágenes o PDF)</label>
|
||||
<!-- Usar 'form' envolvente para resetear fácil -->
|
||||
<form id="formSubirAdjuntos">
|
||||
<div class="input-group">
|
||||
<input type="file" class="form-control" id="inputArchivos" multiple accept="image/*,.pdf">
|
||||
<button class="btn btn-primary" type="button" onclick="subirArchivos()">
|
||||
<i class="fas fa-upload"></i> Subir
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Fecha</th>
|
||||
<th style="width: 150px;">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbodyAdjuntos">
|
||||
<!-- Populated via JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
const esCerrado = @Model.Cerrado.ToString().ToLower();
|
||||
const reporteId = @Model.Id;
|
||||
|
||||
// Data from server
|
||||
let rawRegistros = @Html.Raw(Json.Serialize(Model.Movimientos.Select(m => new {
|
||||
m.Id,
|
||||
m.Tipo, // 1 = Ingreso, 2 = Egreso
|
||||
m.CategoriaIngresoId,
|
||||
m.CategoriaEgresoId,
|
||||
m.Monto,
|
||||
Fecha = m.Fecha.ToString("yyyy-MM-dd"),
|
||||
m.Descripcion,
|
||||
m.NumeroComprobante
|
||||
})));
|
||||
|
||||
// Normalizar datos a mi estructura interna para evitar problemas de mayúsculas/minúsculas
|
||||
let registros = rawRegistros.map(r => ({
|
||||
id: r.Id || r.id || 0,
|
||||
tipo: (r.Tipo !== undefined ? r.Tipo : r.tipo),
|
||||
categoriaIngresoId: r.CategoriaIngresoId || r.categoriaIngresoId,
|
||||
categoriaEgresoId: r.CategoriaEgresoId || r.categoriaEgresoId,
|
||||
monto: r.Monto !== undefined ? r.Monto : r.monto,
|
||||
fecha: r.Fecha || r.fecha,
|
||||
descripcion: r.Descripcion || r.descripcion,
|
||||
numeroComprobante: r.NumeroComprobante || r.numeroComprobante
|
||||
}));
|
||||
|
||||
// Categories helper
|
||||
const rawCatsIngreso = @Html.Raw(Json.Serialize(categoriasIngreso.Select(c => new { c.Id, c.Nombre })));
|
||||
const rawCatsEgreso = @Html.Raw(Json.Serialize(categoriasEgreso.Select(c => new { c.Id, c.Nombre })));
|
||||
|
||||
const catsIngreso = rawCatsIngreso.map(c => ({ id: c.Id || c.id, nombre: c.Nombre || c.nombre }));
|
||||
const catsEgreso = rawCatsEgreso.map(c => ({ id: c.Id || c.id, nombre: c.Nombre || c.nombre }));
|
||||
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('tbodyRegistros');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
registros.forEach((reg, index) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.index = index;
|
||||
|
||||
// Logic for Attachment Button
|
||||
const hasId = reg.id && reg.id > 0;
|
||||
const btnAdjuntoClass = hasId ? "btn-info" : "btn-secondary";
|
||||
const btnAdjuntoTitle = hasId ? "Gestionar Adjuntos" : "Guarde primero para adjuntar";
|
||||
// IMPORTANTE: onclick debe ser cadena vacía si no hay ID, pero el disabled lo controla.
|
||||
const btnAdjuntoAction = hasId ? `abrirAdjuntos(${reg.id})` : "";
|
||||
const btnAdjuntoIcon = '<i class="fas fa-paperclip"></i>';
|
||||
const disabledAttr = hasId ? "" : "disabled";
|
||||
|
||||
if (esCerrado) {
|
||||
// Read-only view
|
||||
const tipoTexto = reg.tipo === 1 ? '<span class="text-success">Ingreso</span>' : '<span class="text-danger">Egreso</span>';
|
||||
const catNombre = reg.tipo === 0
|
||||
? (catsIngreso.find(c => c.id === reg.categoriaIngresoId)?.nombre || '-')
|
||||
: (catsEgreso.find(c => c.id === reg.categoriaEgresoId)?.nombre || '-');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="text-center">${index + 1}</td>
|
||||
<td>${reg.fecha}</td>
|
||||
<td>${tipoTexto}</td>
|
||||
<td>${catNombre}</td>
|
||||
<td>${reg.descripcion || ''}</td>
|
||||
<td>${reg.numeroComprobante || ''}</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm ${btnAdjuntoClass}" onclick="${btnAdjuntoAction}" title="${btnAdjuntoTitle}" ${disabledAttr}>
|
||||
${btnAdjuntoIcon}
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-end">${parseFloat(reg.monto).toFixed(2)}</td>
|
||||
<td></td>
|
||||
`;
|
||||
} else {
|
||||
// Editable view
|
||||
tr.innerHTML = `
|
||||
<td class="text-center align-middle">${index + 1}</td>
|
||||
<td><input type="date" class="form-control form-control-sm" value="${reg.fecha}" onchange="updateReg(${index}, 'fecha', this.value)"></td>
|
||||
<td>
|
||||
<select class="form-control form-control-sm" onchange="cambiarTipo(${index}, this.value)">
|
||||
<option value="1" ${reg.tipo === 1 ? 'selected' : ''}>Ingreso</option>
|
||||
<option value="2" ${reg.tipo === 2 ? 'selected' : ''}>Egreso</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-control form-control-sm" onchange="updateReg(${index}, 'categoria', this.value)">
|
||||
<option value="">Seleccione...</option>
|
||||
${renderCatsOptions(reg.tipo, reg.tipo === 1 ? reg.categoriaIngresoId : reg.categoriaEgresoId)}
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" class="form-control form-control-sm" value="${reg.descripcion || ''}" onchange="updateReg(${index}, 'descripcion', this.value)"></td>
|
||||
<td><input type="text" class="form-control form-control-sm" value="${reg.numeroComprobante || ''}" onchange="updateReg(${index}, 'numeroComprobante', this.value)"></td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-sm ${btnAdjuntoClass}" onclick="${btnAdjuntoAction}" title="${btnAdjuntoTitle}" ${disabledAttr}>
|
||||
${btnAdjuntoIcon}
|
||||
</button>
|
||||
</td>
|
||||
<td><input type="number" step="0.01" class="form-control form-control-sm text-end" value="${reg.monto}" onchange="updateReg(${index}, 'monto', this.value)"></td>
|
||||
<td class="text-center align-middle">
|
||||
<button class="btn btn-danger btn-sm py-0" onclick="eliminarFila(${index})" title="Eliminar"><i class="fas fa-times"></i></button>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function renderCatsOptions(tipo, selectedId) {
|
||||
const list = tipo == 1 ? catsIngreso : catsEgreso; // loose equality for string/number match
|
||||
return list.map(c => `<option value="${c.id}" ${c.id == selectedId ? 'selected' : ''}>${c.nombre}</option>`).join('');
|
||||
}
|
||||
|
||||
function agregarFila() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
registros.push({
|
||||
id: 0,
|
||||
tipo: 1,
|
||||
categoriaIngresoId: null,
|
||||
categoriaEgresoId: null,
|
||||
monto: 0,
|
||||
fecha: today,
|
||||
descripcion: '',
|
||||
numeroComprobante: ''
|
||||
});
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function eliminarFila(index) {
|
||||
registros.splice(index, 1);
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function cambiarTipo(index, nuevoTipo) {
|
||||
registros[index].tipo = parseInt(nuevoTipo);
|
||||
registros[index].categoriaIngresoId = null;
|
||||
registros[index].categoriaEgresoId = null;
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function updateReg(index, field, value) {
|
||||
const reg = registros[index];
|
||||
if (field === 'categoria') {
|
||||
const val = value ? parseInt(value) : null;
|
||||
if (reg.tipo === 1) reg.categoriaIngresoId = val;
|
||||
else reg.categoriaEgresoId = val;
|
||||
} else if (field === 'monto') {
|
||||
reg.monto = parseFloat(value) || 0;
|
||||
} else {
|
||||
reg[field] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: Payload mapping for Server (Server expects PascalCase usually, but standard API accepts camelCase too)
|
||||
document.getElementById('btnGuardar')?.addEventListener('click', async () => {
|
||||
// Validations
|
||||
for (let i = 0; i < registros.length; i++) {
|
||||
const r = registros[i];
|
||||
if (r.monto <= 0) {
|
||||
alert(`Fila ${i+1}: El monto debe ser mayor a 0.`);
|
||||
return;
|
||||
}
|
||||
if (r.tipo === 1 && !r.categoriaIngresoId) {
|
||||
alert(`Fila ${i+1}: Debe seleccionar una categoría de ingreso.`);
|
||||
return;
|
||||
}
|
||||
if (r.tipo === 2 && !r.categoriaEgresoId) {
|
||||
alert(`Fila ${i+1}: Debe seleccionar una categoría de egreso.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnGuardar');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Guardando...';
|
||||
btn.disabled = true;
|
||||
|
||||
// Map back to PascalCase structure just in case server strictly needs it matches DTO
|
||||
const payloadMovimientos = registros.map(r => ({
|
||||
Id: r.id,
|
||||
Tipo: r.tipo,
|
||||
CategoriaIngresoId: r.categoriaIngresoId,
|
||||
CategoriaEgresoId: r.categoriaEgresoId,
|
||||
Monto: r.monto,
|
||||
Fecha: r.fecha,
|
||||
Descripcion: r.descripcion,
|
||||
NumeroComprobante: r.numeroComprobante
|
||||
}));
|
||||
|
||||
try {
|
||||
const response = await fetch('@Url.Action("GuardarBulk")', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ReporteId: reporteId,
|
||||
Movimientos: payloadMovimientos
|
||||
})
|
||||
});
|
||||
// ... rest of logic
|
||||
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// Show toast or alert
|
||||
alert('Guardado exitosamente');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert('Ocurrió un error al guardar.');
|
||||
} finally {
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ================= ADJUNTOS LOGIC =================
|
||||
|
||||
let currentMovId = 0;
|
||||
var myModalAdjuntos;
|
||||
|
||||
function abrirAdjuntos(id) {
|
||||
currentMovId = id;
|
||||
document.getElementById('adjuntoMovimientoId').value = id;
|
||||
|
||||
// Clear previous entries
|
||||
document.getElementById('tbodyAdjuntos').innerHTML = '<tr><td colspan="3" class="text-center">Cargando...</td></tr>';
|
||||
|
||||
if (!myModalAdjuntos) {
|
||||
var el = document.getElementById('modalAdjuntos');
|
||||
myModalAdjuntos = new bootstrap.Modal(el);
|
||||
}
|
||||
myModalAdjuntos.show();
|
||||
|
||||
cargarAdjuntos(id);
|
||||
}
|
||||
|
||||
async function cargarAdjuntos(id) {
|
||||
try {
|
||||
const url = '@Url.Action("ObtenerAdjuntos")?movimientoId=' + id;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
||||
const tbody = document.getElementById('tbodyAdjuntos');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-center">Sin adjuntos</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(adj => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// Si es imagen, mostrar preview pequeño en popover o algo, pero por ahora link simple
|
||||
let link = `<a href="${adj.url}" target="_blank" class="text-decoration-none"><i class="fas fa-external-link-alt"></i> ${adj.nombre}</a>`;
|
||||
|
||||
let deleteBtn = '';
|
||||
if (!esCerrado) {
|
||||
deleteBtn = `<button class="btn btn-danger btn-sm" onclick="eliminarAdjunto(${adj.id})"><i class="fas fa-trash"></i></button>`;
|
||||
}
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${link}</td>
|
||||
<td>${adj.fecha}</td>
|
||||
<td class="text-center">
|
||||
<a href="${adj.url}" download class="btn btn-secondary btn-sm"><i class="fas fa-download"></i></a>
|
||||
${deleteBtn}
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById('tbodyAdjuntos').innerHTML = '<tr><td colspan="3" class="text-danger">Error al cargar adjuntos</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
async function subirArchivos() {
|
||||
const input = document.getElementById('inputArchivos');
|
||||
if (input.files.length === 0) {
|
||||
alert("Seleccione al menos un archivo.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('movimientoId', currentMovId);
|
||||
for (let i = 0; i < input.files.length; i++) {
|
||||
formData.append('archivos', input.files[i]);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('@Url.Action("SubirAdjunto")', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
// Limpiar input y recargar
|
||||
input.value = '';
|
||||
cargarAdjuntos(currentMovId);
|
||||
} else {
|
||||
alert("Error: " + result.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Error al subir archivos.");
|
||||
}
|
||||
}
|
||||
|
||||
async function eliminarAdjunto(id) {
|
||||
if (!confirm('¿Eliminar este adjunto?')) return;
|
||||
try {
|
||||
const res = await fetch('@Url.Action("EliminarAdjunto")?id=' + id, { method: 'POST' });
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
cargarAdjuntos(currentMovId);
|
||||
} else {
|
||||
alert("No se pudo eliminar.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Error al eliminar.");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
if (registros.length === 0 && !esCerrado) {
|
||||
agregarFila();
|
||||
} else {
|
||||
renderTable();
|
||||
}
|
||||
</script>
|
||||
}
|
||||
67
RS_system/Views/Estados/Create.cshtml
Normal file
67
RS_system/Views/Estados/Create.cshtml
Normal 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");}
|
||||
}
|
||||
68
RS_system/Views/Estados/Edit.cshtml
Normal file
68
RS_system/Views/Estados/Edit.cshtml
Normal 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");}
|
||||
}
|
||||
121
RS_system/Views/Estados/Index.cshtml
Normal file
121
RS_system/Views/Estados/Index.cshtml
Normal 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>
|
||||
}
|
||||
298
RS_system/Views/MovimientosInventario/Create.cshtml
Normal file
298
RS_system/Views/MovimientosInventario/Create.cshtml
Normal 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>
|
||||
}
|
||||
128
RS_system/Views/MovimientosInventario/Index.cshtml
Normal file
128
RS_system/Views/MovimientosInventario/Index.cshtml
Normal 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>
|
||||
}
|
||||
156
RS_system/Views/MovimientosInventario/PrestamosActivos.cshtml
Normal file
156
RS_system/Views/MovimientosInventario/PrestamosActivos.cshtml
Normal 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>
|
||||
}
|
||||
63
RS_system/Views/TipoColaboracion/Create.cshtml
Normal file
63
RS_system/Views/TipoColaboracion/Create.cshtml
Normal 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>
|
||||
65
RS_system/Views/TipoColaboracion/Edit.cshtml
Normal file
65
RS_system/Views/TipoColaboracion/Edit.cshtml
Normal 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>
|
||||
112
RS_system/Views/TipoColaboracion/Index.cshtml
Normal file
112
RS_system/Views/TipoColaboracion/Index.cshtml
Normal 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>
|
||||
}
|
||||
63
RS_system/Views/Ubicaciones/Create.cshtml
Normal file
63
RS_system/Views/Ubicaciones/Create.cshtml
Normal 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");}
|
||||
}
|
||||
64
RS_system/Views/Ubicaciones/Edit.cshtml
Normal file
64
RS_system/Views/Ubicaciones/Edit.cshtml
Normal 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");}
|
||||
}
|
||||
131
RS_system/Views/Ubicaciones/Index.cshtml
Normal file
131
RS_system/Views/Ubicaciones/Index.cshtml
Normal 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>
|
||||
}
|
||||
103
RS_system/sql_colaboraciones.sql
Normal file
103
RS_system/sql_colaboraciones.sql
Normal file
@@ -0,0 +1,103 @@
|
||||
-- =============================================
|
||||
-- Script SQL: Módulo de Colaboraciones Económicas
|
||||
-- Descripción: Crea las tablas para gestionar colaboraciones mensuales
|
||||
-- Autor: Sistema
|
||||
-- Fecha: 2026-02-01
|
||||
-- =============================================
|
||||
|
||||
-- Tabla 1: Tipos de Colaboración (configurable)
|
||||
CREATE TABLE IF NOT EXISTS public.tipos_colaboracion
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
nombre character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
descripcion text COLLATE pg_catalog."default",
|
||||
monto_sugerido decimal(10, 2) NOT NULL DEFAULT 0,
|
||||
activo boolean NOT NULL DEFAULT true,
|
||||
orden integer NOT NULL DEFAULT 0,
|
||||
creado_en timestamp with time zone NOT NULL DEFAULT now(),
|
||||
actualizado_en timestamp with time zone NOT NULL DEFAULT now(),
|
||||
CONSTRAINT pk_tipos_colaboracion PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.tipos_colaboracion IS 'Catálogo de tipos de colaboración (Transporte, Limpieza, etc.)';
|
||||
COMMENT ON COLUMN public.tipos_colaboracion.nombre IS 'Nombre del tipo de colaboración';
|
||||
COMMENT ON COLUMN public.tipos_colaboracion.monto_sugerido IS 'Cuota sugerida mensual';
|
||||
COMMENT ON COLUMN public.tipos_colaboracion.orden IS 'Orden de presentación en UI';
|
||||
|
||||
-- Datos iniciales
|
||||
INSERT INTO public.tipos_colaboracion (nombre, descripcion, monto_sugerido, orden)
|
||||
VALUES
|
||||
('Transporte', 'Colaboración mensual para transporte', 1.00, 1),
|
||||
('Limpieza', 'Colaboración mensual para limpieza', 1.00, 2)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- =============================================
|
||||
|
||||
-- Tabla 2: Colaboraciones (cabecera de transacción)
|
||||
CREATE TABLE IF NOT EXISTS public.colaboraciones
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
miembro_id bigint NOT NULL,
|
||||
fecha_registro timestamp with time zone NOT NULL DEFAULT now(),
|
||||
monto_total decimal(10, 2) NOT NULL,
|
||||
observaciones text COLLATE pg_catalog."default",
|
||||
registrado_por character varying(100) COLLATE pg_catalog."default",
|
||||
creado_en timestamp with time zone NOT NULL DEFAULT now(),
|
||||
actualizado_en timestamp with time zone NOT NULL DEFAULT now(),
|
||||
CONSTRAINT pk_colaboraciones PRIMARY KEY (id),
|
||||
CONSTRAINT fk_colaboraciones_miembro FOREIGN KEY (miembro_id)
|
||||
REFERENCES public.miembros (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.colaboraciones IS 'Registro de transacciones de colaboración';
|
||||
COMMENT ON COLUMN public.colaboraciones.miembro_id IS 'Miembro que realizó el pago';
|
||||
COMMENT ON COLUMN public.colaboraciones.monto_total IS 'Monto total de la transacción';
|
||||
COMMENT ON COLUMN public.colaboraciones.registrado_por IS 'Usuario que registró el pago';
|
||||
|
||||
-- Índices para optimizar búsquedas
|
||||
CREATE INDEX IF NOT EXISTS idx_colaboraciones_miembro ON public.colaboraciones(miembro_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_colaboraciones_fecha ON public.colaboraciones(fecha_registro);
|
||||
|
||||
-- =============================================
|
||||
|
||||
-- Tabla 3: Detalle de Colaboraciones (desglose mensual)
|
||||
CREATE TABLE IF NOT EXISTS public.detalle_colaboraciones
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
colaboracion_id bigint NOT NULL,
|
||||
tipo_colaboracion_id bigint NOT NULL,
|
||||
mes integer NOT NULL,
|
||||
anio integer NOT NULL,
|
||||
monto decimal(10, 2) NOT NULL,
|
||||
creado_en timestamp with time zone NOT NULL DEFAULT now(),
|
||||
CONSTRAINT pk_detalle_colaboraciones PRIMARY KEY (id),
|
||||
CONSTRAINT fk_detalle_colaboracion FOREIGN KEY (colaboracion_id)
|
||||
REFERENCES public.colaboraciones (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_detalle_tipo FOREIGN KEY (tipo_colaboracion_id)
|
||||
REFERENCES public.tipos_colaboracion (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT chk_mes_valido CHECK (mes >= 1 AND mes <= 12),
|
||||
CONSTRAINT chk_anio_valido CHECK (anio >= 2000 AND anio <= 2100)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE public.detalle_colaboraciones IS 'Desglose mensual de cada colaboración';
|
||||
COMMENT ON COLUMN public.detalle_colaboraciones.mes IS 'Mes cubierto (1-12)';
|
||||
COMMENT ON COLUMN public.detalle_colaboraciones.anio IS 'Año cubierto';
|
||||
COMMENT ON COLUMN public.detalle_colaboraciones.monto IS 'Monto para este mes/tipo específico';
|
||||
|
||||
-- Índices para reportes y búsquedas
|
||||
CREATE INDEX IF NOT EXISTS idx_detalle_tipo ON public.detalle_colaboraciones(tipo_colaboracion_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_detalle_periodo ON public.detalle_colaboraciones(anio, mes);
|
||||
|
||||
-- Índice único para evitar duplicados (mismo mes/año/tipo en misma colaboración)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_detalle_unico
|
||||
ON public.detalle_colaboraciones(colaboracion_id, tipo_colaboracion_id, mes, anio);
|
||||
|
||||
-- =============================================
|
||||
-- FIN DEL SCRIPT
|
||||
-- =============================================
|
||||
17
RS_system/sql_contabilidad_adjuntos.sql
Normal file
17
RS_system/sql_contabilidad_adjuntos.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Tabla: movimientos_generales_adjuntos
|
||||
CREATE TABLE IF NOT EXISTS public.movimientos_generales_adjuntos
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
movimiento_general_id bigint NOT NULL,
|
||||
nombre_archivo character varying(255) COLLATE pg_catalog."default" NOT NULL,
|
||||
ruta_archivo character varying(500) COLLATE pg_catalog."default" NOT NULL,
|
||||
tipo_contenido character varying(100) COLLATE pg_catalog."default",
|
||||
fecha_subida timestamp without time zone NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
|
||||
CONSTRAINT pk_movimientos_generales_adjuntos PRIMARY KEY (id),
|
||||
CONSTRAINT fk_adjuntos_movimiento FOREIGN KEY (movimiento_general_id)
|
||||
REFERENCES public.movimientos_generales (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_adjuntos_movimiento
|
||||
ON public.movimientos_generales_adjuntos (movimiento_general_id);
|
||||
171
RS_system/sql_contabilidad_general.sql
Normal file
171
RS_system/sql_contabilidad_general.sql
Normal file
@@ -0,0 +1,171 @@
|
||||
-- =====================================================
|
||||
-- Script SQL para Sistema de Contabilidad General
|
||||
-- Base de datos: PostgreSQL
|
||||
-- Fecha: 2026-01-28
|
||||
-- =====================================================
|
||||
|
||||
-- =====================================================
|
||||
-- 1. CREAR TABLAS
|
||||
-- =====================================================
|
||||
|
||||
-- Tabla: categorias_ingreso
|
||||
CREATE TABLE IF NOT EXISTS public.categorias_ingreso
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
nombre character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
descripcion character varying(255) COLLATE pg_catalog."default",
|
||||
activa boolean NOT NULL DEFAULT true,
|
||||
fecha_creacion timestamp without time zone NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
|
||||
CONSTRAINT pk_categorias_ingreso PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Tabla: categorias_egreso
|
||||
CREATE TABLE IF NOT EXISTS public.categorias_egreso
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
nombre character varying(100) COLLATE pg_catalog."default" NOT NULL,
|
||||
descripcion character varying(255) COLLATE pg_catalog."default",
|
||||
activa boolean NOT NULL DEFAULT true,
|
||||
fecha_creacion timestamp without time zone NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
|
||||
CONSTRAINT pk_categorias_egreso PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Tabla: reportes_mensuales_generales
|
||||
CREATE TABLE IF NOT EXISTS public.reportes_mensuales_generales
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
mes integer NOT NULL,
|
||||
anio integer NOT NULL,
|
||||
saldo_inicial numeric(18,2) NOT NULL DEFAULT 0,
|
||||
fecha_creacion timestamp without time zone NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC'),
|
||||
cerrado boolean NOT NULL DEFAULT false,
|
||||
CONSTRAINT pk_reportes_mensuales_generales PRIMARY KEY (id),
|
||||
CONSTRAINT uk_mes_anio_general UNIQUE (mes, anio),
|
||||
CONSTRAINT ck_mes_rango CHECK (mes >= 1 AND mes <= 12),
|
||||
CONSTRAINT ck_anio_valido CHECK (anio >= 2000 AND anio <= 2100)
|
||||
);
|
||||
|
||||
-- Crear tipo enum para tipo_movimiento_general si no existe
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE tipo_movimiento_general AS ENUM ('Ingreso', 'Egreso');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla: movimientos_generales
|
||||
CREATE TABLE IF NOT EXISTS public.movimientos_generales
|
||||
(
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
reporte_mensual_general_id bigint,
|
||||
tipo tipo_movimiento_general NOT NULL,
|
||||
categoria_ingreso_id bigint,
|
||||
categoria_egreso_id bigint,
|
||||
monto numeric(18,2) NOT NULL,
|
||||
fecha timestamp without time zone NOT NULL,
|
||||
descripcion character varying(200) COLLATE pg_catalog."default" NOT NULL DEFAULT '',
|
||||
numero_comprobante character varying(50) COLLATE pg_catalog."default",
|
||||
CONSTRAINT pk_movimientos_generales PRIMARY KEY (id),
|
||||
CONSTRAINT fk_movimientos_reporte FOREIGN KEY (reporte_mensual_general_id)
|
||||
REFERENCES public.reportes_mensuales_generales (id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_movimientos_categoria_ingreso FOREIGN KEY (categoria_ingreso_id)
|
||||
REFERENCES public.categorias_ingreso (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_movimientos_categoria_egreso FOREIGN KEY (categoria_egreso_id)
|
||||
REFERENCES public.categorias_egreso (id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT ck_monto_positivo CHECK (monto > 0),
|
||||
CONSTRAINT ck_categoria_tipo CHECK (
|
||||
(tipo = 'Ingreso' AND categoria_ingreso_id IS NOT NULL AND categoria_egreso_id IS NULL) OR
|
||||
(tipo = 'Egreso' AND categoria_egreso_id IS NOT NULL AND categoria_ingreso_id IS NULL)
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 2. CREAR ÍNDICES
|
||||
-- =====================================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_movimientos_reporte
|
||||
ON public.movimientos_generales (reporte_mensual_general_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_movimientos_fecha
|
||||
ON public.movimientos_generales (fecha);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_movimientos_categoria_ingreso
|
||||
ON public.movimientos_generales (categoria_ingreso_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_movimientos_categoria_egreso
|
||||
ON public.movimientos_generales (categoria_egreso_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reportes_anio
|
||||
ON public.reportes_mensuales_generales (anio);
|
||||
|
||||
-- =====================================================
|
||||
-- 3. INSERTAR DATOS INICIALES - CATEGORÍAS DE INGRESO
|
||||
-- =====================================================
|
||||
|
||||
INSERT INTO public.categorias_ingreso (nombre, descripcion, activa) VALUES
|
||||
('Ofrendas', 'Ofrendas regulares de los cultos', true),
|
||||
('Donaciones', 'Donaciones especiales de hermanos y visitantes', true),
|
||||
('Diezmos', 'Diezmos de los miembros', true),
|
||||
('Eventos Especiales', 'Ingresos de eventos, conferencias, retiros', true),
|
||||
('Alquileres', 'Ingresos por alquiler de instalaciones', true),
|
||||
('Ventas', 'Ventas de materiales, libros u otros productos', true),
|
||||
('Otros Ingresos', 'Ingresos diversos no categorizados', true)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- =====================================================
|
||||
-- 4. INSERTAR DATOS INICIALES - CATEGORÍAS DE EGRESO
|
||||
-- =====================================================
|
||||
|
||||
INSERT INTO public.categorias_egreso (nombre, descripcion, activa) VALUES
|
||||
('Agua', 'Pago del servicio de agua', true),
|
||||
('Luz', 'Pago del servicio de electricidad', true),
|
||||
('Teléfono/Internet', 'Servicios de telefonía e internet', true),
|
||||
('Impuestos', 'Pago de impuestos municipales, prediales, etc.', true),
|
||||
('Funeraria', 'Gastos relacionados con servicios funerarios', true),
|
||||
('Mantenimiento Edificio', 'Reparaciones y mantenimiento de las instalaciones', true),
|
||||
('Suministros Ministerio', 'Materiales para ministerios (niños, jóvenes, etc.)', true),
|
||||
('Salarios Personal', 'Salarios de pastores y personal administrativo', true),
|
||||
('Eventos', 'Gastos de organización de eventos', true),
|
||||
('Transporte', 'Gastos de transporte y combustible', true),
|
||||
('Limpieza', 'Servicios de limpieza y productos', true),
|
||||
('Seguridad', 'Servicio de seguridad o vigilancia', true),
|
||||
('Otros Gastos', 'Gastos diversos no categorizados', true)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- =====================================================
|
||||
-- 5. COMENTARIOS SOBRE LAS TABLAS
|
||||
-- =====================================================
|
||||
|
||||
COMMENT ON TABLE public.categorias_ingreso IS 'Categorías para clasificar los ingresos de la iglesia';
|
||||
COMMENT ON TABLE public.categorias_egreso IS 'Categorías para clasificar los egresos de la iglesia';
|
||||
COMMENT ON TABLE public.reportes_mensuales_generales IS 'Reportes mensuales de contabilidad general de la iglesia';
|
||||
COMMENT ON TABLE public.movimientos_generales IS 'Movimientos individuales de ingresos y egresos';
|
||||
|
||||
-- =====================================================
|
||||
-- 6. VERIFICACIÓN
|
||||
-- =====================================================
|
||||
|
||||
-- Verificar que se crearon las tablas
|
||||
SELECT
|
||||
table_name,
|
||||
(SELECT COUNT(*) FROM information_schema.columns WHERE table_name = t.table_name) as columnas
|
||||
FROM information_schema.tables t
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name IN (
|
||||
'categorias_ingreso',
|
||||
'categorias_egreso',
|
||||
'reportes_mensuales_generales',
|
||||
'movimientos_generales'
|
||||
)
|
||||
ORDER BY table_name;
|
||||
|
||||
-- Verificar datos iniciales de categorías
|
||||
SELECT 'Categorías de Ingreso' as tipo, COUNT(*) as total FROM public.categorias_ingreso
|
||||
UNION ALL
|
||||
SELECT 'Categorías de Egreso' as tipo, COUNT(*) as total FROM public.categorias_egreso;
|
||||
|
||||
-- =====================================================
|
||||
-- FIN DEL SCRIPT
|
||||
-- =====================================================
|
||||
97
RS_system/sql_permisos_colaboraciones.sql
Normal file
97
RS_system/sql_permisos_colaboraciones.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
-- ============================================
|
||||
-- Script: Agregar Gestión de Tipos de Colaboración
|
||||
-- Descripción: Agrega el módulo y permiso para gestionar tipos de colaboración
|
||||
-- Fecha: 2026-02-01
|
||||
-- NOTA: Este script NO debe ejecutarse automáticamente, el usuario lo ejecutará manualmente
|
||||
-- ============================================
|
||||
|
||||
-- 1. Insertar permiso para Tipo Colaboración (si no existe)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Verificar si ya existe el módulo de Finanzas
|
||||
IF NOT EXISTS (SELECT 1 FROM public.modulos WHERE codigo = 'FINANZAS') THEN
|
||||
INSERT INTO public.modulos (nombre, descripcion, codigo, icono, activo, orden, creado_en, actualizado_en)
|
||||
VALUES ('Finanzas', 'Módulo de gestión financiera', 'FINANZAS', 'bi-cash-stack', true, 5, NOW(), NOW());
|
||||
END IF;
|
||||
|
||||
-- Insertar permiso para Colaboraciones (si no existe)
|
||||
IF NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'Colaboracion') THEN
|
||||
INSERT INTO public.permisos (nombre, descripcion, codigo, modulo_id, activo, orden, creado_en, actualizado_en)
|
||||
VALUES (
|
||||
'Colaboraciones',
|
||||
'Gestión de colaboraciones económicas mensuales',
|
||||
'Colaboracion',
|
||||
(SELECT id FROM public.modulos WHERE codigo = 'FINANZAS' LIMIT 1),
|
||||
true,
|
||||
1,
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- Insertar permiso para Tipos de Colaboración (si no existe)
|
||||
IF NOT EXISTS (SELECT 1 FROM public.permisos WHERE codigo = 'TipoColaboracion') THEN
|
||||
INSERT INTO public.permisos (nombre, descripcion, codigo, modulo_id, activo, orden, creado_en, actualizado_en)
|
||||
VALUES (
|
||||
'Tipos de Colaboración',
|
||||
'Gestión de tipos de colaboración (Transporte, Limpieza, etc.)',
|
||||
'TipoColaboracion',
|
||||
(SELECT id FROM public.modulos WHERE codigo = 'FINANZAS' LIMIT 1),
|
||||
true,
|
||||
2,
|
||||
NOW(),
|
||||
NOW()
|
||||
);
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE 'Permisos para Colaboraciones creados exitosamente';
|
||||
END $$;
|
||||
|
||||
-- 2. (Opcional) Asignar permisos al rol de Administrador
|
||||
-- Descomentar si se desea asignar automáticamente
|
||||
/*
|
||||
DO $$
|
||||
DECLARE
|
||||
v_rol_admin_id BIGINT;
|
||||
v_permiso_colaboracion_id BIGINT;
|
||||
v_permiso_tipo_id BIGINT;
|
||||
BEGIN
|
||||
-- Obtener el ID del rol de administrador (ajustar el nombre según tu sistema)
|
||||
SELECT id INTO v_rol_admin_id FROM public.roles_sistema WHERE nombre = 'Administrador' LIMIT 1;
|
||||
|
||||
-- Obtener IDs de los permisos
|
||||
SELECT id INTO v_permiso_colaboracion_id FROM public.permisos WHERE codigo = 'Colaboracion' LIMIT 1;
|
||||
SELECT id INTO v_permiso_tipo_id FROM public.permisos WHERE codigo = 'TipoColaboracion' LIMIT 1;
|
||||
|
||||
IF v_rol_admin_id IS NOT NULL THEN
|
||||
-- Asignar permiso de Colaboraciones
|
||||
IF NOT EXISTS (SELECT 1 FROM public.roles_permisos WHERE rol_id = v_rol_admin_id AND permiso_id = v_permiso_colaboracion_id) THEN
|
||||
INSERT INTO public.roles_permisos (rol_id, permiso_id, creado_en)
|
||||
VALUES (v_rol_admin_id, v_permiso_colaboracion_id, NOW());
|
||||
END IF;
|
||||
|
||||
-- Asignar permiso de Tipos de Colaboración
|
||||
IF NOT EXISTS (SELECT 1 FROM public.roles_permisos WHERE rol_id = v_rol_admin_id AND permiso_id = v_permiso_tipo_id) THEN
|
||||
INSERT INTO public.roles_permisos (rol_id, permiso_id, creado_en)
|
||||
VALUES (v_rol_admin_id, v_permiso_tipo_id, NOW());
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE 'Permisos asignados al rol Administrador';
|
||||
END IF;
|
||||
END $$;
|
||||
*/
|
||||
|
||||
-- 3. Verificación: Listar permisos creados
|
||||
SELECT
|
||||
m.nombre AS modulo,
|
||||
p.nombre AS permiso,
|
||||
p.codigo,
|
||||
p.activo
|
||||
FROM public.permisos p
|
||||
INNER JOIN public.modulos m ON p.modulo_id = m.id
|
||||
WHERE p.codigo IN ('Colaboracion', 'TipoColaboracion')
|
||||
ORDER BY m.nombre, p.orden;
|
||||
|
||||
-- ============================================
|
||||
-- FIN DEL SCRIPT
|
||||
-- ============================================
|
||||
BIN
RS_system/wwwroot/Assets/apple-touch-icon.png
Normal file
BIN
RS_system/wwwroot/Assets/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
RS_system/wwwroot/Assets/favicon-16x16.png
Normal file
BIN
RS_system/wwwroot/Assets/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 892 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user