From 4e6a5448ed67e8b39bddf625b43192cee8a2c53f Mon Sep 17 00:00:00 2001 From: adalberto Date: Wed, 31 Dec 2025 15:24:23 -0600 Subject: [PATCH] Se agrego contabilidad --- .../Controllers/CajaChicaController.cs | 147 +++++++ .../Controllers/CategoriaGastoController.cs | 99 +++++ .../Controllers/MovimientoCajaController.cs | 193 ++++++++++ .../Controllers/ProveedorController.cs | 99 +++++ .../Data/ApplicationDbContext.cs | 7 + .../Scripts/FixProveedoresColumn.sql | 20 + .../Migrations/Scripts/MigrateCajaChica.sql | 65 ++++ .../Scripts/UpdateBuscarPersonas_v3.sql | 360 ++++++++++++++++++ foundation_system/Models/CajaChica.cs | 49 +++ .../Models/CajaChicaMovimiento.cs | 61 +++ foundation_system/Models/CategoriaGasto.cs | 28 ++ foundation_system/Models/DocumentoSoporte.cs | 34 ++ foundation_system/Models/Proveedor.cs | 28 ++ .../Models/ViewModels/CajaChicaViewModel.cs | 27 ++ .../ViewModels/RegistroGastoViewModel.cs | 31 ++ .../Views/CajaChica/Create.cshtml | 74 ++++ .../Views/CajaChica/Details.cshtml | 176 +++++++++ .../Views/CajaChica/Index.cshtml | 61 +++ .../Views/CategoriaGasto/Create.cshtml | 53 +++ .../Views/CategoriaGasto/Edit.cshtml | 55 +++ .../Views/CategoriaGasto/Index.cshtml | 55 +++ .../Views/MovimientoCaja/CreateGasto.cshtml | 160 ++++++++ .../Views/MovimientoCaja/Reposicion.cshtml | 91 +++++ .../Views/Proveedor/Create.cshtml | 53 +++ foundation_system/Views/Proveedor/Edit.cshtml | 55 +++ .../Views/Proveedor/Index.cshtml | 55 +++ foundation_system/Views/Shared/_Layout.cshtml | 2 + 27 files changed, 2138 insertions(+) create mode 100644 foundation_system/Controllers/CajaChicaController.cs create mode 100644 foundation_system/Controllers/CategoriaGastoController.cs create mode 100644 foundation_system/Controllers/MovimientoCajaController.cs create mode 100644 foundation_system/Controllers/ProveedorController.cs create mode 100644 foundation_system/Migrations/Scripts/FixProveedoresColumn.sql create mode 100644 foundation_system/Migrations/Scripts/MigrateCajaChica.sql create mode 100644 foundation_system/Migrations/Scripts/UpdateBuscarPersonas_v3.sql create mode 100644 foundation_system/Models/CajaChica.cs create mode 100644 foundation_system/Models/CajaChicaMovimiento.cs create mode 100644 foundation_system/Models/CategoriaGasto.cs create mode 100644 foundation_system/Models/DocumentoSoporte.cs create mode 100644 foundation_system/Models/Proveedor.cs create mode 100644 foundation_system/Models/ViewModels/CajaChicaViewModel.cs create mode 100644 foundation_system/Models/ViewModels/RegistroGastoViewModel.cs create mode 100644 foundation_system/Views/CajaChica/Create.cshtml create mode 100644 foundation_system/Views/CajaChica/Details.cshtml create mode 100644 foundation_system/Views/CajaChica/Index.cshtml create mode 100644 foundation_system/Views/CategoriaGasto/Create.cshtml create mode 100644 foundation_system/Views/CategoriaGasto/Edit.cshtml create mode 100644 foundation_system/Views/CategoriaGasto/Index.cshtml create mode 100644 foundation_system/Views/MovimientoCaja/CreateGasto.cshtml create mode 100644 foundation_system/Views/MovimientoCaja/Reposicion.cshtml create mode 100644 foundation_system/Views/Proveedor/Create.cshtml create mode 100644 foundation_system/Views/Proveedor/Edit.cshtml create mode 100644 foundation_system/Views/Proveedor/Index.cshtml diff --git a/foundation_system/Controllers/CajaChicaController.cs b/foundation_system/Controllers/CajaChicaController.cs new file mode 100644 index 0000000..d085fdd --- /dev/null +++ b/foundation_system/Controllers/CajaChicaController.cs @@ -0,0 +1,147 @@ +using foundation_system.Data; +using foundation_system.Models; +using foundation_system.Models.ViewModels; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; + +namespace foundation_system.Controllers +{ + [Authorize] + public class CajaChicaController : Controller + { + private readonly ApplicationDbContext _context; + + public CajaChicaController(ApplicationDbContext context) + { + _context = context; + } + + // GET: CajaChica + public async Task Index() + { + var cajas = await _context.CajasChicas + .Include(c => c.Responsable) + .ThenInclude(u => u.Persona) + .ToListAsync(); + return View(cajas); + } + + // GET: CajaChica/Details/5 + public async Task Details(int? id) + { + if (id == null) + { + return NotFound(); + } + + var cajaChica = await _context.CajasChicas + .Include(c => c.Responsable) + .ThenInclude(u => u.Persona) + .Include(c => c.Movimientos.OrderByDescending(m => m.FechaMovimiento).ThenByDescending(m => m.Id)) + .ThenInclude(m => m.CategoriaGasto) + .Include(c => c.Movimientos) + .ThenInclude(m => m.UsuarioRegistro) + .ThenInclude(u => u.Persona) + .FirstOrDefaultAsync(m => m.Id == id); + + if (cajaChica == null) + { + return NotFound(); + } + + return View(cajaChica); + } + + // GET: CajaChica/Create + public IActionResult Create() + { + ViewData["ResponsableUsuarioId"] = new SelectList(_context.Usuarios.Include(u => u.Persona).Select(u => new { + Id = u.Id, + NombreCompleto = u.Persona.Nombres + " " + u.Persona.Apellidos + }), "Id", "NombreCompleto"); + return View(new CajaChicaViewModel()); + } + + // POST: CajaChica/Create + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create(CajaChicaViewModel model) + { + if (ModelState.IsValid) + { + // Verify user doesn't already have an open box + var existingBox = await _context.CajasChicas + .AnyAsync(c => c.ResponsableUsuarioId == model.ResponsableUsuarioId && c.Estado == "ABIERTA"); + + if (existingBox) + { + ModelState.AddModelError("ResponsableUsuarioId", "El usuario ya tiene una caja chica abierta."); + } + else + { + var cajaChica = new CajaChica + { + Nombre = model.Nombre, + ResponsableUsuarioId = model.ResponsableUsuarioId, + MontoAsignado = model.MontoAsignado, + SaldoActual = model.MontoAsignado, // Starts full + FechaApertura = model.FechaApertura, + Estado = "ABIERTA", + CreadoEn = DateTime.UtcNow, + ActualizadoEn = DateTime.UtcNow + }; + + _context.Add(cajaChica); + await _context.SaveChangesAsync(); + + // Create initial movement + var movimiento = new CajaChicaMovimiento + { + CajaChicaId = cajaChica.Id, + TipoMovimiento = "APERTURA", + FechaMovimiento = model.FechaApertura, + Monto = model.MontoAsignado, + Descripcion = "Apertura de Fondo Fijo", + UsuarioRegistroId = long.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "1"), // Fallback to 1 if not found + CreadoEn = DateTime.UtcNow + }; + + _context.Add(movimiento); + await _context.SaveChangesAsync(); + + return RedirectToAction(nameof(Index)); + } + } + + ViewData["ResponsableUsuarioId"] = new SelectList(_context.Usuarios.Include(u => u.Persona).Select(u => new { + Id = u.Id, + NombreCompleto = u.Persona.Nombres + " " + u.Persona.Apellidos + }), "Id", "NombreCompleto", model.ResponsableUsuarioId); + return View(model); + } + + // POST: CajaChica/Close/5 + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Close(int id) + { + var caja = await _context.CajasChicas.FindAsync(id); + if (caja == null) return NotFound(); + + if (caja.SaldoActual != caja.MontoAsignado) + { + // In a real scenario, you'd require reimbursement first or an adjustment + // For now, we'll just allow closing but maybe warn (or simple logic) + } + + caja.Estado = "CERRADA"; + caja.FechaCierre = DateOnly.FromDateTime(DateTime.Today); + caja.ActualizadoEn = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + } +} diff --git a/foundation_system/Controllers/CategoriaGastoController.cs b/foundation_system/Controllers/CategoriaGastoController.cs new file mode 100644 index 0000000..4190e5c --- /dev/null +++ b/foundation_system/Controllers/CategoriaGastoController.cs @@ -0,0 +1,99 @@ +using foundation_system.Data; +using foundation_system.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace foundation_system.Controllers +{ + [Authorize] + public class CategoriaGastoController : Controller + { + private readonly ApplicationDbContext _context; + + public CategoriaGastoController(ApplicationDbContext context) + { + _context = context; + } + + // GET: CategoriaGasto + public async Task Index() + { + return View(await _context.CategoriasGastos.ToListAsync()); + } + + // GET: CategoriaGasto/Create + public IActionResult Create() + { + return View(); + } + + // POST: CategoriaGasto/Create + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create([Bind("Id,Nombre,CodigoCuentaContable,Activo")] CategoriaGasto categoriaGasto) + { + if (ModelState.IsValid) + { + _context.Add(categoriaGasto); + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + return View(categoriaGasto); + } + + // GET: CategoriaGasto/Edit/5 + public async Task Edit(int? id) + { + if (id == null) + { + return NotFound(); + } + + var categoriaGasto = await _context.CategoriasGastos.FindAsync(id); + if (categoriaGasto == null) + { + return NotFound(); + } + return View(categoriaGasto); + } + + // POST: CategoriaGasto/Edit/5 + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(int id, [Bind("Id,Nombre,CodigoCuentaContable,Activo,CreadoEn")] CategoriaGasto categoriaGasto) + { + if (id != categoriaGasto.Id) + { + return NotFound(); + } + + if (ModelState.IsValid) + { + try + { + _context.Update(categoriaGasto); + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!CategoriaGastoExists(categoriaGasto.Id)) + { + return NotFound(); + } + else + { + throw; + } + } + return RedirectToAction(nameof(Index)); + } + return View(categoriaGasto); + } + + private bool CategoriaGastoExists(int id) + { + return _context.CategoriasGastos.Any(e => e.Id == id); + } + } +} diff --git a/foundation_system/Controllers/MovimientoCajaController.cs b/foundation_system/Controllers/MovimientoCajaController.cs new file mode 100644 index 0000000..7b10afb --- /dev/null +++ b/foundation_system/Controllers/MovimientoCajaController.cs @@ -0,0 +1,193 @@ +using foundation_system.Data; +using foundation_system.Models; +using foundation_system.Models.ViewModels; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; + +namespace foundation_system.Controllers +{ + [Authorize] + public class MovimientoCajaController : Controller + { + private readonly ApplicationDbContext _context; + + public MovimientoCajaController(ApplicationDbContext context) + { + _context = context; + } + + // GET: MovimientoCaja/CreateGasto?cajaId=5 + public async Task CreateGasto(int cajaId) + { + var caja = await _context.CajasChicas.FindAsync(cajaId); + if (caja == null || caja.Estado != "ABIERTA") + { + return NotFound("La caja no existe o está cerrada."); + } + + var model = new RegistroGastoViewModel + { + CajaChicaId = caja.Id, + CajaChicaNombre = caja.Nombre, + SaldoDisponible = caja.SaldoActual, + Fecha = DateOnly.FromDateTime(DateTime.Today) + }; + + ViewData["CategoriaId"] = new SelectList(_context.CategoriasGastos.Where(c => c.Activo), "Id", "Nombre"); + ViewData["ProveedorId"] = new SelectList(_context.Proveedores.Where(p => p.Activo), "Id", "Nombre"); + return View(model); + } + + // POST: MovimientoCaja/CreateGasto + [HttpPost] + [ValidateAntiForgeryToken] + public async Task CreateGasto(RegistroGastoViewModel model) + { + var caja = await _context.CajasChicas.FindAsync(model.CajaChicaId); + if (caja == null || caja.Estado != "ABIERTA") + { + return NotFound(); + } + + if (model.Monto > caja.SaldoActual) + { + ModelState.AddModelError("Monto", "El monto del gasto excede el saldo disponible."); + } + + if (ModelState.IsValid) + { + using var transaction = _context.Database.BeginTransaction(); + try + { + // 1. Create Movement + var movimiento = new CajaChicaMovimiento + { + CajaChicaId = model.CajaChicaId, + TipoMovimiento = "GASTO", + FechaMovimiento = model.Fecha, + Monto = model.Monto, + Descripcion = model.Descripcion, + CategoriaGastoId = model.CategoriaId, + ProveedorId = model.ProveedorId, + UsuarioRegistroId = long.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "1"), + EstadoReembolso = "PENDIENTE", + CreadoEn = DateTime.UtcNow + }; + + _context.Add(movimiento); + await _context.SaveChangesAsync(); + + // 2. Update Balance + caja.SaldoActual -= model.Monto; + caja.ActualizadoEn = DateTime.UtcNow; + _context.Update(caja); + await _context.SaveChangesAsync(); + + await transaction.CommitAsync(); + return RedirectToAction("Details", "CajaChica", new { id = model.CajaChicaId }); + } + catch (Exception) + { + await transaction.RollbackAsync(); + ModelState.AddModelError("", "Ocurrió un error al registrar el gasto."); + } + } + + model.CajaChicaNombre = caja.Nombre; + model.SaldoDisponible = caja.SaldoActual; + ViewData["CategoriaId"] = new SelectList(_context.CategoriasGastos.Where(c => c.Activo), "Id", "Nombre", model.CategoriaId); + ViewData["ProveedorId"] = new SelectList(_context.Proveedores.Where(p => p.Activo), "Id", "Nombre", model.ProveedorId); + return View(model); + } + + // GET: MovimientoCaja/Reposicion?cajaId=5 + public async Task Reposicion(int cajaId) + { + var caja = await _context.CajasChicas + .Include(c => c.Movimientos.Where(m => m.TipoMovimiento == "GASTO" && m.EstadoReembolso == "PENDIENTE")) + .FirstOrDefaultAsync(c => c.Id == cajaId); + + if (caja == null) return NotFound(); + + return View(caja); + } + + // POST: MovimientoCaja/ConfirmarReposicion + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ConfirmarReposicion(int cajaId, List movimientoIds) + { + var caja = await _context.CajasChicas.FindAsync(cajaId); + if (caja == null) return NotFound(); + + if (movimientoIds == null || !movimientoIds.Any()) + { + return RedirectToAction("Reposicion", new { cajaId }); + } + + using var transaction = _context.Database.BeginTransaction(); + try + { + var movimientos = await _context.CajaChicaMovimientos + .Where(m => movimientoIds.Contains(m.Id) && m.CajaChicaId == cajaId && m.EstadoReembolso == "PENDIENTE") + .ToListAsync(); + + decimal totalReembolso = movimientos.Sum(m => m.Monto); + + // 1. Mark expenses as reimbursed + foreach (var mov in movimientos) + { + mov.EstadoReembolso = "REEMBOLSADO"; + } + _context.UpdateRange(movimientos); + + // 2. Create Replenishment Movement + var reposicion = new CajaChicaMovimiento + { + CajaChicaId = cajaId, + TipoMovimiento = "REPOSICION", + FechaMovimiento = DateOnly.FromDateTime(DateTime.Today), + Monto = totalReembolso, + Descripcion = $"Reposición de {movimientos.Count} gastos.", + UsuarioRegistroId = long.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "1"), + CreadoEn = DateTime.UtcNow + }; + _context.Add(reposicion); + + // 3. Restore Balance + caja.SaldoActual += totalReembolso; + caja.ActualizadoEn = DateTime.UtcNow; + _context.Update(caja); + + await _context.SaveChangesAsync(); + await transaction.CommitAsync(); + + return RedirectToAction("Details", "CajaChica", new { id = cajaId }); + } + catch + { + await transaction.RollbackAsync(); + return RedirectToAction("Reposicion", new { cajaId }); + } + } + // GET: MovimientoCaja/SearchProveedores + [HttpGet] + public async Task SearchProveedores(string term) + { + if (string.IsNullOrWhiteSpace(term) || term.Length < 2) + { + return Json(new List()); + } + + var results = await _context.Database + .SqlQueryRaw( + "SELECT id, text, score FROM buscar_personas_v2(@p0, 'PROVEEDOR')", + term) + .ToListAsync(); + + return Json(results); + } + } +} diff --git a/foundation_system/Controllers/ProveedorController.cs b/foundation_system/Controllers/ProveedorController.cs new file mode 100644 index 0000000..d343b09 --- /dev/null +++ b/foundation_system/Controllers/ProveedorController.cs @@ -0,0 +1,99 @@ +using foundation_system.Data; +using foundation_system.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace foundation_system.Controllers +{ + [Authorize] + public class ProveedorController : Controller + { + private readonly ApplicationDbContext _context; + + public ProveedorController(ApplicationDbContext context) + { + _context = context; + } + + // GET: Proveedor + public async Task Index() + { + return View(await _context.Proveedores.ToListAsync()); + } + + // GET: Proveedor/Create + public IActionResult Create() + { + return View(); + } + + // POST: Proveedor/Create + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create([Bind("Id,Nombre,NitDui,Activo")] Proveedor proveedor) + { + if (ModelState.IsValid) + { + _context.Add(proveedor); + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + return View(proveedor); + } + + // GET: Proveedor/Edit/5 + public async Task Edit(int? id) + { + if (id == null) + { + return NotFound(); + } + + var proveedor = await _context.Proveedores.FindAsync(id); + if (proveedor == null) + { + return NotFound(); + } + return View(proveedor); + } + + // POST: Proveedor/Edit/5 + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(int id, [Bind("Id,Nombre,NitDui,Activo,CreadoEn")] Proveedor proveedor) + { + if (id != proveedor.Id) + { + return NotFound(); + } + + if (ModelState.IsValid) + { + try + { + _context.Update(proveedor); + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!ProveedorExists(proveedor.Id)) + { + return NotFound(); + } + else + { + throw; + } + } + return RedirectToAction(nameof(Index)); + } + return View(proveedor); + } + + private bool ProveedorExists(int id) + { + return _context.Proveedores.Any(e => e.Id == id); + } + } +} diff --git a/foundation_system/Data/ApplicationDbContext.cs b/foundation_system/Data/ApplicationDbContext.cs index 9de8244..99525ce 100644 --- a/foundation_system/Data/ApplicationDbContext.cs +++ b/foundation_system/Data/ApplicationDbContext.cs @@ -19,6 +19,13 @@ public class ApplicationDbContext : DbContext public DbSet RolesPermisos { get; set; } public DbSet Colaboradores { get; set; } public DbSet CargosColaboradores { get; set; } + + // Caja Chica + public DbSet CajasChicas { get; set; } + public DbSet CajaChicaMovimientos { get; set; } + public DbSet CategoriasGastos { get; set; } + public DbSet Proveedores { get; set; } + public DbSet DocumentosSoporte { get; set; } public DbSet AsistenciasColaboradores { get; set; } public DbSet Ninos { get; set; } public DbSet Asistencias { get; set; } diff --git a/foundation_system/Migrations/Scripts/FixProveedoresColumn.sql b/foundation_system/Migrations/Scripts/FixProveedoresColumn.sql new file mode 100644 index 0000000..cb93136 --- /dev/null +++ b/foundation_system/Migrations/Scripts/FixProveedoresColumn.sql @@ -0,0 +1,20 @@ +-- Script para corregir la falta de columnas en la tabla proveedores +-- Esto puede ocurrir si la tabla ya existía antes de la migración de Caja Chica + +DO $$ +BEGIN + -- Verificar si existe la columna creado_en en proveedores + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='proveedores' AND column_name='creado_en') THEN + ALTER TABLE proveedores ADD COLUMN creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP; + END IF; + + -- Verificar si existe la columna activo en proveedores + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='proveedores' AND column_name='activo') THEN + ALTER TABLE proveedores ADD COLUMN activo BOOLEAN DEFAULT TRUE; + END IF; + + -- Verificar si existe la columna nit_dui en proveedores + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='proveedores' AND column_name='nit_dui') THEN + ALTER TABLE proveedores ADD COLUMN nit_dui VARCHAR(50); + END IF; +END $$; diff --git a/foundation_system/Migrations/Scripts/MigrateCajaChica.sql b/foundation_system/Migrations/Scripts/MigrateCajaChica.sql new file mode 100644 index 0000000..c75a7e4 --- /dev/null +++ b/foundation_system/Migrations/Scripts/MigrateCajaChica.sql @@ -0,0 +1,65 @@ +-- Catálogo de Categorías de Gasto +CREATE TABLE IF NOT EXISTS categorias_gastos ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(100) NOT NULL, + codigo_cuenta_contable VARCHAR(50), -- Para integración contable + activo BOOLEAN DEFAULT TRUE, + creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Catálogo de Proveedores +CREATE TABLE IF NOT EXISTS proveedores ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(200) NOT NULL, + nit_dui VARCHAR(50), -- Identificación fiscal + activo BOOLEAN DEFAULT TRUE, + creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Cabecera de Caja Chica +CREATE TABLE IF NOT EXISTS cajas_chicas ( + id SERIAL PRIMARY KEY, + nombre VARCHAR(100) NOT NULL, -- Ej. "Caja Chica Administración" + responsable_usuario_id BIGINT NOT NULL, -- FK a Usuarios + monto_asignado DECIMAL(10,2) NOT NULL, -- El fondo fijo (ej. $500) + saldo_actual DECIMAL(10,2) NOT NULL, -- Efectivo disponible + estado VARCHAR(20) NOT NULL DEFAULT 'ABIERTA', -- ABIERTA, CERRADA, ARQUEO + fecha_apertura DATE NOT NULL, + fecha_cierre DATE, + creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + actualizado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT chk_saldo_positivo CHECK (saldo_actual >= 0), + CONSTRAINT fk_caja_responsable FOREIGN KEY (responsable_usuario_id) REFERENCES usuarios(id) +); + +-- Movimientos (Ingresos, Egresos, Reembolsos) +CREATE TABLE IF NOT EXISTS caja_chica_movimientos ( + id BIGSERIAL PRIMARY KEY, + caja_chica_id INT NOT NULL REFERENCES cajas_chicas(id), + tipo_movimiento VARCHAR(20) NOT NULL, -- GASTO, APERTURA, REPOSICION, AUMENTO_FONDO, DISMINUCION_FONDO, AJUSTE + fecha_movimiento DATE NOT NULL, + monto DECIMAL(10,2) NOT NULL, + descripcion TEXT NOT NULL, + categoria_gasto_id INT REFERENCES categorias_gastos(id), -- Null si no es gasto + proveedor_id INT REFERENCES proveedores(id), -- Null si no aplica + usuario_registro_id BIGINT NOT NULL, -- Auditoría: Quién registró + estado_reembolso VARCHAR(20) DEFAULT 'PENDIENTE', -- PENDIENTE, REEMBOLSADO, ANULADO + creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT chk_monto_positivo CHECK (monto > 0), + CONSTRAINT fk_movimiento_usuario FOREIGN KEY (usuario_registro_id) REFERENCES usuarios(id) +); + +-- Documentos de Soporte (Adjuntos) +CREATE TABLE IF NOT EXISTS documentos_soporte ( + id BIGSERIAL PRIMARY KEY, + movimiento_id BIGINT NOT NULL REFERENCES caja_chica_movimientos(id) ON DELETE CASCADE, + tipo_documento VARCHAR(50) NOT NULL, -- FACTURA, RECIBO, VALE + numero_documento VARCHAR(50), + ruta_archivo TEXT, -- Path al archivo físico o URL + creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Índices Recomendados +CREATE INDEX IF NOT EXISTS idx_movimientos_caja ON caja_chica_movimientos(caja_chica_id); +CREATE INDEX IF NOT EXISTS idx_movimientos_fecha ON caja_chica_movimientos(fecha_movimiento); +CREATE INDEX IF NOT EXISTS idx_movimientos_estado ON caja_chica_movimientos(estado_reembolso); diff --git a/foundation_system/Migrations/Scripts/UpdateBuscarPersonas_v3.sql b/foundation_system/Migrations/Scripts/UpdateBuscarPersonas_v3.sql new file mode 100644 index 0000000..2c93767 --- /dev/null +++ b/foundation_system/Migrations/Scripts/UpdateBuscarPersonas_v3.sql @@ -0,0 +1,360 @@ +CREATE OR REPLACE FUNCTION public.buscar_personas_v2( + p_termino text, + p_tipo text, + p_limite integer DEFAULT 10) + RETURNS TABLE(id bigint, text text, score double precision, match_type text) + LANGUAGE 'plpgsql' + COST 100 + VOLATILE PARALLEL UNSAFE + ROWS 1000 + +AS $BODY$ +DECLARE + v_termino_norm TEXT; + v_tokens TEXT[]; + v_sim_threshold DOUBLE PRECISION := 0.15; -- Más permisivo + v_lev_threshold INTEGER := 3; -- Permite más errores + + -- Pesos para ranking (ajusta según importancia) + v_peso_nombre DOUBLE PRECISION := 1.0; + v_peso_apellido DOUBLE PRECISION := 0.8; + v_peso_codigo DOUBLE PRECISION := 0.6; + v_peso_dui DOUBLE PRECISION := 0.5; +BEGIN + -- Normalizar el término de búsqueda + v_termino_norm := normalizar_texto(p_termino); + + -- Tokenizar (separar por palabras) + v_tokens := string_to_array(v_termino_norm, ' '); + + -- ======================================================================== + -- BÚSQUEDA PARA NIÑOS + -- ======================================================================== + IF upper(p_tipo) = 'NINO' THEN + RETURN QUERY + WITH candidatos AS ( + -- FASE 1: Filtro rápido por índice (operador %) + SELECT + n.id, + p.nombres, + p.apellidos, + n.codigo_inscripcion, + normalizar_texto(p.nombres) AS nombres_norm, + normalizar_texto(p.apellidos) AS apellidos_norm, + normalizar_texto(n.codigo_inscripcion) AS codigo_norm + FROM ninos n + JOIN personas p ON p.id = n.persona_id + WHERE n.activo = true + AND p.activo = true + AND ( + -- Búsqueda rápida con operador % (usa índice GIN) + normalizar_texto(p.nombres || ' ' || p.apellidos) % v_termino_norm + OR normalizar_texto(n.codigo_inscripcion) % v_termino_norm + OR normalizar_texto(p.nombres) % ANY(v_tokens) + OR normalizar_texto(p.apellidos) % ANY(v_tokens) + ) + ), + scoring AS ( + -- FASE 2: Cálculo preciso de scores + SELECT + c.id, + (c.nombres || ' ' || c.apellidos || ' | Código: ' || c.codigo_inscripcion)::TEXT AS display_text, + + -- Score por similitud de nombre completo + (similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) * v_peso_nombre) AS score_nombre_completo, + + -- Score por similitud de nombres individuales + (similarity(c.nombres_norm, v_termino_norm) * v_peso_nombre) AS score_nombres, + + -- Score por similitud de apellidos + (similarity(c.apellidos_norm, v_termino_norm) * v_peso_apellido) AS score_apellidos, + + -- Score por código + (similarity(c.codigo_norm, v_termino_norm) * v_peso_codigo) AS score_codigo, + + -- Score por Levenshtein (nombres) + (1.0 - (levenshtein(c.nombres_norm, v_termino_norm)::DOUBLE PRECISION + / GREATEST(length(c.nombres_norm), length(v_termino_norm), 1))) * v_peso_nombre AS score_lev_nombres, + + -- Score por tokens (cada palabra individual) + ( + SELECT COALESCE(MAX( + GREATEST( + similarity(c.nombres_norm, tok), + similarity(c.apellidos_norm, tok) + ) + ), 0) + FROM unnest(v_tokens) tok + ) * 0.9 AS score_tokens, + + -- Determinar tipo de match + CASE + WHEN similarity(c.codigo_norm, v_termino_norm) >= v_sim_threshold THEN 'codigo' + WHEN similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) >= v_sim_threshold THEN 'nombre_completo' + WHEN levenshtein(c.nombres_norm, v_termino_norm) <= v_lev_threshold THEN 'fuzzy' + ELSE 'token' + END AS match_type + + FROM candidatos c + ) + SELECT + s.id, + s.display_text, + -- Score final: tomar el máximo de todos los métodos + GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_codigo, + s.score_lev_nombres, + s.score_tokens + ) AS score, + s.match_type + FROM scoring s + WHERE GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_codigo, + s.score_lev_nombres, + s.score_tokens + ) > 0.1 -- Umbral mínimo para aparecer en resultados + ORDER BY score DESC + LIMIT p_limite; + + -- ======================================================================== + -- BÚSQUEDA PARA ENCARGADOS + -- ======================================================================== + ELSIF upper(p_tipo) = 'ENCARGADO' THEN + RETURN QUERY + WITH candidatos AS ( + SELECT + p.id, + p.nombres, + p.apellidos, + COALESCE(p.dui, '') AS dui, + en.parentesco, + normalizar_texto(p.nombres) AS nombres_norm, + normalizar_texto(p.apellidos) AS apellidos_norm, + normalizar_texto(COALESCE(p.dui, '')) AS dui_norm + FROM encargados_nino en + JOIN personas p ON p.id = en.persona_id + WHERE p.activo = true + AND ( + normalizar_texto(p.nombres || ' ' || p.apellidos) % v_termino_norm + OR normalizar_texto(COALESCE(p.dui, '')) % v_termino_norm + OR normalizar_texto(p.nombres) % ANY(v_tokens) + OR normalizar_texto(p.apellidos) % ANY(v_tokens) + ) + ), + scoring AS ( + SELECT + c.id, + (c.nombres || ' ' || c.apellidos || ' (' || c.parentesco || ')')::TEXT AS display_text, + + (similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) * v_peso_nombre) AS score_nombre_completo, + (similarity(c.nombres_norm, v_termino_norm) * v_peso_nombre) AS score_nombres, + (similarity(c.apellidos_norm, v_termino_norm) * v_peso_apellido) AS score_apellidos, + (similarity(c.dui_norm, v_termino_norm) * v_peso_dui) AS score_dui, + + (1.0 - (levenshtein(c.nombres_norm, v_termino_norm)::DOUBLE PRECISION + / GREATEST(length(c.nombres_norm), length(v_termino_norm), 1))) * v_peso_nombre AS score_lev_nombres, + + ( + SELECT COALESCE(MAX( + GREATEST( + similarity(c.nombres_norm, tok), + similarity(c.apellidos_norm, tok) + ) + ), 0) + FROM unnest(v_tokens) tok + ) * 0.9 AS score_tokens, + + CASE + WHEN similarity(c.dui_norm, v_termino_norm) >= v_sim_threshold THEN 'dui' + WHEN similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) >= v_sim_threshold THEN 'nombre_completo' + WHEN levenshtein(c.nombres_norm, v_termino_norm) <= v_lev_threshold THEN 'fuzzy' + ELSE 'token' + END AS match_type + + FROM candidatos c + ) + SELECT + s.id, + s.display_text, + GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_dui, + s.score_lev_nombres, + s.score_tokens + ) AS score, + s.match_type + FROM scoring s + WHERE GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_dui, + s.score_lev_nombres, + s.score_tokens + ) > 0.1 + ORDER BY score DESC + LIMIT p_limite; + + -- ======================================================================== + -- BÚSQUEDA PARA COLABORADORES + -- ======================================================================== + ELSIF upper(p_tipo) = 'COLABORADOR' THEN + RETURN QUERY + WITH candidatos AS ( + SELECT + c.id, + p.nombres, + p.apellidos, + COALESCE(p.dui, '') AS dui, + cc.nombre as cargo, + normalizar_texto(p.nombres) AS nombres_norm, + normalizar_texto(p.apellidos) AS apellidos_norm, + normalizar_texto(COALESCE(p.dui, '')) AS dui_norm + FROM colaboradores c + JOIN personas p ON p.id = c.persona_id + LEFT JOIN cargos_colaboradores cc ON cc.id = c.cargo_id + WHERE c.activo = true + AND p.activo = true + AND ( + normalizar_texto(p.nombres || ' ' || p.apellidos) % v_termino_norm + OR normalizar_texto(COALESCE(p.dui, '')) % v_termino_norm + OR normalizar_texto(p.nombres) % ANY(v_tokens) + OR normalizar_texto(p.apellidos) % ANY(v_tokens) + ) + ), + scoring AS ( + SELECT + c.id, + (c.nombres || ' ' || c.apellidos || ' (' || COALESCE(c.cargo, 'Sin Cargo') || ')')::TEXT AS display_text, + + (similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) * v_peso_nombre) AS score_nombre_completo, + (similarity(c.nombres_norm, v_termino_norm) * v_peso_nombre) AS score_nombres, + (similarity(c.apellidos_norm, v_termino_norm) * v_peso_apellido) AS score_apellidos, + (similarity(c.dui_norm, v_termino_norm) * v_peso_dui) AS score_dui, + + (1.0 - (levenshtein(c.nombres_norm, v_termino_norm)::DOUBLE PRECISION + / GREATEST(length(c.nombres_norm), length(v_termino_norm), 1))) * v_peso_nombre AS score_lev_nombres, + + ( + SELECT COALESCE(MAX( + GREATEST( + similarity(c.nombres_norm, tok), + similarity(c.apellidos_norm, tok) + ) + ), 0) + FROM unnest(v_tokens) tok + ) * 0.9 AS score_tokens, + + CASE + WHEN similarity(c.dui_norm, v_termino_norm) >= v_sim_threshold THEN 'dui' + WHEN similarity(c.nombres_norm || ' ' || c.apellidos_norm, v_termino_norm) >= v_sim_threshold THEN 'nombre_completo' + WHEN levenshtein(c.nombres_norm, v_termino_norm) <= v_lev_threshold THEN 'fuzzy' + ELSE 'token' + END AS match_type + + FROM candidatos c + ) + SELECT + s.id, + s.display_text, + GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_dui, + s.score_lev_nombres, + s.score_tokens + ) AS score, + s.match_type + FROM scoring s + WHERE GREATEST( + s.score_nombre_completo, + s.score_nombres, + s.score_apellidos, + s.score_dui, + s.score_lev_nombres, + s.score_tokens + ) > 0.1 + ORDER BY score DESC + LIMIT p_limite; + + -- ======================================================================== + -- BÚSQUEDA PARA PROVEEDORES + -- ======================================================================== + ELSIF upper(p_tipo) = 'PROVEEDOR' THEN + RETURN QUERY + WITH candidatos AS ( + SELECT + pr.id, + pr.nombre, + COALESCE(pr.nit_dui, '') AS nit_dui, + normalizar_texto(pr.nombre) AS nombre_norm, + normalizar_texto(COALESCE(pr.nit_dui, '')) AS nit_norm + FROM proveedores pr + WHERE pr.activo = true + AND ( + normalizar_texto(pr.nombre) % v_termino_norm + OR normalizar_texto(COALESCE(pr.nit_dui, '')) % v_termino_norm + OR normalizar_texto(pr.nombre) % ANY(v_tokens) + ) + ), + scoring AS ( + SELECT + c.id, + (c.nombre || ' (NIT/DUI: ' || COALESCE(c.nit_dui, 'N/A') || ')')::TEXT AS display_text, + + (similarity(c.nombre_norm, v_termino_norm) * v_peso_nombre) AS score_nombre, + (similarity(c.nit_norm, v_termino_norm) * v_peso_dui) AS score_nit, + + (1.0 - (levenshtein(c.nombre_norm, v_termino_norm)::DOUBLE PRECISION + / GREATEST(length(c.nombre_norm), length(v_termino_norm), 1))) * v_peso_nombre AS score_lev_nombre, + + ( + SELECT COALESCE(MAX( + similarity(c.nombre_norm, tok) + ), 0) + FROM unnest(v_tokens) tok + ) * 0.9 AS score_tokens, + + CASE + WHEN similarity(c.nit_norm, v_termino_norm) >= v_sim_threshold THEN 'nit' + WHEN similarity(c.nombre_norm, v_termino_norm) >= v_sim_threshold THEN 'nombre' + WHEN levenshtein(c.nombre_norm, v_termino_norm) <= v_lev_threshold THEN 'fuzzy' + ELSE 'token' + END AS match_type + + FROM candidatos c + ) + SELECT + s.id::bigint, + s.display_text, + GREATEST( + s.score_nombre, + s.score_nit, + s.score_lev_nombre, + s.score_tokens + ) AS score, + s.match_type + FROM scoring s + WHERE GREATEST( + s.score_nombre, + s.score_nit, + s.score_lev_nombre, + s.score_tokens + ) > 0.1 + ORDER BY score DESC + LIMIT p_limite; + + ELSE + RAISE EXCEPTION 'Tipo no soportado: %. Use NINO, ENCARGADO, COLABORADOR o PROVEEDOR', p_tipo; + END IF; +END; +$BODY$; diff --git a/foundation_system/Models/CajaChica.cs b/foundation_system/Models/CajaChica.cs new file mode 100644 index 0000000..e2a747c --- /dev/null +++ b/foundation_system/Models/CajaChica.cs @@ -0,0 +1,49 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace foundation_system.Models +{ + [Table("cajas_chicas")] + public class CajaChica + { + [Key] + [Column("id")] + public int Id { get; set; } + + [Required] + [StringLength(100)] + [Column("nombre")] + public string Nombre { get; set; } = string.Empty; + + [Column("responsable_usuario_id")] + public long ResponsableUsuarioId { get; set; } + + [ForeignKey("ResponsableUsuarioId")] + public virtual Usuario? Responsable { get; set; } + + [Column("monto_asignado", TypeName = "decimal(10,2)")] + public decimal MontoAsignado { get; set; } + + [Column("saldo_actual", TypeName = "decimal(10,2)")] + public decimal SaldoActual { get; set; } + + [Required] + [StringLength(20)] + [Column("estado")] + public string Estado { get; set; } = "ABIERTA"; // ABIERTA, CERRADA, ARQUEO + + [Column("fecha_apertura")] + public DateOnly FechaApertura { get; set; } + + [Column("fecha_cierre")] + public DateOnly? FechaCierre { get; set; } + + [Column("creado_en")] + public DateTime CreadoEn { get; set; } = DateTime.UtcNow; + + [Column("actualizado_en")] + public DateTime ActualizadoEn { get; set; } = DateTime.UtcNow; + + public virtual ICollection Movimientos { get; set; } = new List(); + } +} diff --git a/foundation_system/Models/CajaChicaMovimiento.cs b/foundation_system/Models/CajaChicaMovimiento.cs new file mode 100644 index 0000000..bf1deed --- /dev/null +++ b/foundation_system/Models/CajaChicaMovimiento.cs @@ -0,0 +1,61 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace foundation_system.Models +{ + [Table("caja_chica_movimientos")] + public class CajaChicaMovimiento + { + [Key] + [Column("id")] + public long Id { get; set; } + + [Column("caja_chica_id")] + public int CajaChicaId { get; set; } + + [ForeignKey("CajaChicaId")] + public virtual CajaChica? CajaChica { get; set; } + + [Required] + [StringLength(20)] + [Column("tipo_movimiento")] + public string TipoMovimiento { get; set; } = string.Empty; // GASTO, APERTURA, REPOSICION, etc. + + [Column("fecha_movimiento")] + public DateOnly FechaMovimiento { get; set; } + + [Column("monto", TypeName = "decimal(10,2)")] + public decimal Monto { get; set; } + + [Required] + [Column("descripcion")] + public string Descripcion { get; set; } = string.Empty; + + [Column("categoria_gasto_id")] + public int? CategoriaGastoId { get; set; } + + [ForeignKey("CategoriaGastoId")] + public virtual CategoriaGasto? CategoriaGasto { get; set; } + + [Column("proveedor_id")] + public int? ProveedorId { get; set; } + + [ForeignKey("ProveedorId")] + public virtual Proveedor? Proveedor { get; set; } + + [Column("usuario_registro_id")] + public long UsuarioRegistroId { get; set; } + + [ForeignKey("UsuarioRegistroId")] + public virtual Usuario? UsuarioRegistro { get; set; } + + [Column("estado_reembolso")] + [StringLength(20)] + public string EstadoReembolso { get; set; } = "PENDIENTE"; // PENDIENTE, REEMBOLSADO, ANULADO + + [Column("creado_en")] + public DateTime CreadoEn { get; set; } = DateTime.UtcNow; + + public virtual ICollection Documentos { get; set; } = new List(); + } +} diff --git a/foundation_system/Models/CategoriaGasto.cs b/foundation_system/Models/CategoriaGasto.cs new file mode 100644 index 0000000..c849ba1 --- /dev/null +++ b/foundation_system/Models/CategoriaGasto.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace foundation_system.Models +{ + [Table("categorias_gastos")] + public class CategoriaGasto + { + [Key] + [Column("id")] + public int Id { get; set; } + + [Required] + [StringLength(100)] + [Column("nombre")] + public string Nombre { get; set; } = string.Empty; + + [StringLength(50)] + [Column("codigo_cuenta_contable")] + public string? CodigoCuentaContable { get; set; } + + [Column("activo")] + public bool Activo { get; set; } = true; + + [Column("creado_en")] + public DateTime CreadoEn { get; set; } = DateTime.UtcNow; + } +} diff --git a/foundation_system/Models/DocumentoSoporte.cs b/foundation_system/Models/DocumentoSoporte.cs new file mode 100644 index 0000000..cc1b01a --- /dev/null +++ b/foundation_system/Models/DocumentoSoporte.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace foundation_system.Models +{ + [Table("documentos_soporte")] + public class DocumentoSoporte + { + [Key] + [Column("id")] + public long Id { get; set; } + + [Column("movimiento_id")] + public long MovimientoId { get; set; } + + [ForeignKey("MovimientoId")] + public virtual CajaChicaMovimiento? Movimiento { get; set; } + + [Required] + [StringLength(50)] + [Column("tipo_documento")] + public string TipoDocumento { get; set; } = string.Empty; // FACTURA, RECIBO, VALE + + [StringLength(50)] + [Column("numero_documento")] + public string? NumeroDocumento { get; set; } + + [Column("ruta_archivo")] + public string? RutaArchivo { get; set; } + + [Column("creado_en")] + public DateTime CreadoEn { get; set; } = DateTime.UtcNow; + } +} diff --git a/foundation_system/Models/Proveedor.cs b/foundation_system/Models/Proveedor.cs new file mode 100644 index 0000000..a3b4188 --- /dev/null +++ b/foundation_system/Models/Proveedor.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace foundation_system.Models +{ + [Table("proveedores")] + public class Proveedor + { + [Key] + [Column("id")] + public int Id { get; set; } + + [Required] + [StringLength(200)] + [Column("nombre")] + public string Nombre { get; set; } = string.Empty; + + [StringLength(50)] + [Column("nit_dui")] + public string? NitDui { get; set; } + + [Column("activo")] + public bool Activo { get; set; } = true; + + [Column("creado_en")] + public DateTime CreadoEn { get; set; } = DateTime.UtcNow; + } +} diff --git a/foundation_system/Models/ViewModels/CajaChicaViewModel.cs b/foundation_system/Models/ViewModels/CajaChicaViewModel.cs new file mode 100644 index 0000000..e69a3d2 --- /dev/null +++ b/foundation_system/Models/ViewModels/CajaChicaViewModel.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace foundation_system.Models.ViewModels +{ + public class CajaChicaViewModel + { + public int Id { get; set; } + + [Required] + [StringLength(100)] + public string Nombre { get; set; } = string.Empty; + + [Required] + [Display(Name = "Responsable")] + public long ResponsableUsuarioId { get; set; } + + [Required] + [Range(0.01, 10000, ErrorMessage = "El monto asignado debe ser mayor a 0.")] + [Display(Name = "Monto Asignado")] + public decimal MontoAsignado { get; set; } + + [Required] + [DataType(DataType.Date)] + [Display(Name = "Fecha de Apertura")] + public DateOnly FechaApertura { get; set; } = DateOnly.FromDateTime(DateTime.Today); + } +} diff --git a/foundation_system/Models/ViewModels/RegistroGastoViewModel.cs b/foundation_system/Models/ViewModels/RegistroGastoViewModel.cs new file mode 100644 index 0000000..5f58d9b --- /dev/null +++ b/foundation_system/Models/ViewModels/RegistroGastoViewModel.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; + +namespace foundation_system.Models.ViewModels +{ + public class RegistroGastoViewModel + { + [Required] + public int CajaChicaId { get; set; } + + public string? CajaChicaNombre { get; set; } + public decimal SaldoDisponible { get; set; } + + [Required] + [Range(0.01, 10000, ErrorMessage = "El monto debe ser mayor a 0.")] + public decimal Monto { get; set; } + + [Required] + public DateOnly Fecha { get; set; } = DateOnly.FromDateTime(DateTime.Today); + + [Required] + public string Descripcion { get; set; } = string.Empty; + + [Required(ErrorMessage = "Seleccione una categoría")] + public int CategoriaId { get; set; } + + public int? ProveedorId { get; set; } + + // For file upload if needed later + // public IFormFile? Comprobante { get; set; } + } +} diff --git a/foundation_system/Views/CajaChica/Create.cshtml b/foundation_system/Views/CajaChica/Create.cshtml new file mode 100644 index 0000000..6aa14ef --- /dev/null +++ b/foundation_system/Views/CajaChica/Create.cshtml @@ -0,0 +1,74 @@ +@model foundation_system.Models.ViewModels.CajaChicaViewModel + +@{ + ViewData["Title"] = "Apertura de Caja Chica"; +} + +
+
+

@ViewData["Title"]

+ + Volver + +
+ +
+
+
+
+
Datos de Apertura
+
+
+
+
+ +
+ + + +
+ +
+ + + +
+ +
+
+ +
+ $ + +
+ +
+
+ + + +
+
+ +
+ + Al crear la caja, se registrará automáticamente un movimiento de "Apertura" por el monto asignado. +
+ +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/foundation_system/Views/CajaChica/Details.cshtml b/foundation_system/Views/CajaChica/Details.cshtml new file mode 100644 index 0000000..addd5b0 --- /dev/null +++ b/foundation_system/Views/CajaChica/Details.cshtml @@ -0,0 +1,176 @@ +@model foundation_system.Models.CajaChica + +@{ + ViewData["Title"] = "Detalle de Caja Chica"; + var porcentajeSaldo = (Model.MontoAsignado > 0) ? (Model.SaldoActual / Model.MontoAsignado) * 100 : 0; + var colorSaldo = porcentajeSaldo < 20 ? "danger" : (porcentajeSaldo < 50 ? "warning" : "success"); +} + +
+
+
+

@Model.Nombre

+

Responsable: @Model.Responsable?.Persona?.Nombres @Model.Responsable?.Persona?.Apellidos

+
+
+ + Volver + + @if (Model.Estado == "ABIERTA") + { + + Registrar Gasto + + + Reposición + +
+ +
+ } + else + { + CAJA CERRADA + } +
+
+ + +
+
+
+
+
+
+
Monto Asignado
+
@Model.MontoAsignado.ToString("C")
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
Saldo Disponible
+
@Model.SaldoActual.ToString("C")
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
Gastos Pendientes
+
+ @Model.Movimientos.Where(m => m.TipoMovimiento == "GASTO" && m.EstadoReembolso == "PENDIENTE").Sum(m => m.Monto).ToString("C") +
+
+
+ +
+
+
+
+
+
+ + +
+
+
Historial de Movimientos
+
+
+
+ + + + + + + + + + + + + + @foreach (var mov in Model.Movimientos) + { + + + + + + + + + + } + +
FechaTipoDescripciónCategoríaMontoEstadoRegistrado Por
@mov.FechaMovimiento.ToString("dd/MM/yyyy") + @if(mov.TipoMovimiento == "GASTO") { GASTO } + else if(mov.TipoMovimiento == "REPOSICION") { REPOSICIÓN } + else { @mov.TipoMovimiento } + @mov.Descripcion@(mov.CategoriaGasto?.Nombre ?? "-")@mov.Monto.ToString("C") + @if(mov.TipoMovimiento == "GASTO") + { + @if(mov.EstadoReembolso == "PENDIENTE") { Pendiente } + else { Reembolsado } + } + else + { + - + } + @mov.UsuarioRegistro?.Persona?.Nombres
+
+
+
+
+ +@section Styles { + +} + +@section Scripts { + +} diff --git a/foundation_system/Views/CajaChica/Index.cshtml b/foundation_system/Views/CajaChica/Index.cshtml new file mode 100644 index 0000000..07b0627 --- /dev/null +++ b/foundation_system/Views/CajaChica/Index.cshtml @@ -0,0 +1,61 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Cajas Chicas"; +} + +
+
+

@ViewData["Title"]

+ + Apertura de Caja + +
+ +
+
+
+ + + + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + + + } + +
NombreResponsableMonto AsignadoSaldo ActualEstadoAcciones
@item.Nombre@item.Responsable?.Persona?.Nombres @item.Responsable?.Persona?.Apellidos@item.MontoAsignado.ToString("C") + @item.SaldoActual.ToString("C") + + @if (item.Estado == "ABIERTA") + { + Abierta + } + else + { + @item.Estado + } + + + Detalle + +
+
+
+
+
diff --git a/foundation_system/Views/CategoriaGasto/Create.cshtml b/foundation_system/Views/CategoriaGasto/Create.cshtml new file mode 100644 index 0000000..fa806aa --- /dev/null +++ b/foundation_system/Views/CategoriaGasto/Create.cshtml @@ -0,0 +1,53 @@ +@model foundation_system.Models.CategoriaGasto + +@{ + ViewData["Title"] = "Nueva Categoría de Gasto"; +} + +
+
+

@ViewData["Title"]

+ + Volver + +
+ +
+
+
+
+
+
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/foundation_system/Views/CategoriaGasto/Edit.cshtml b/foundation_system/Views/CategoriaGasto/Edit.cshtml new file mode 100644 index 0000000..29a8cb8 --- /dev/null +++ b/foundation_system/Views/CategoriaGasto/Edit.cshtml @@ -0,0 +1,55 @@ +@model foundation_system.Models.CategoriaGasto + +@{ + ViewData["Title"] = "Editar Categoría de Gasto"; +} + +
+
+

@ViewData["Title"]

+ + Volver + +
+ +
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/foundation_system/Views/CategoriaGasto/Index.cshtml b/foundation_system/Views/CategoriaGasto/Index.cshtml new file mode 100644 index 0000000..d5744b0 --- /dev/null +++ b/foundation_system/Views/CategoriaGasto/Index.cshtml @@ -0,0 +1,55 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Categorías de Gasto"; +} + +
+
+

@ViewData["Title"]

+ + Nueva Categoría + +
+ +
+
+
+ + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + } + +
NombreCódigo ContableEstadoAcciones
@item.Nombre@item.CodigoCuentaContable + @if (item.Activo) + { + Activo + } + else + { + Inactivo + } + + + + +
+
+
+
+
diff --git a/foundation_system/Views/MovimientoCaja/CreateGasto.cshtml b/foundation_system/Views/MovimientoCaja/CreateGasto.cshtml new file mode 100644 index 0000000..4b38fbb --- /dev/null +++ b/foundation_system/Views/MovimientoCaja/CreateGasto.cshtml @@ -0,0 +1,160 @@ +@model foundation_system.Models.ViewModels.RegistroGastoViewModel + +@{ + ViewData["Title"] = "Registrar Gasto"; +} + +
+
+

@ViewData["Title"]

+ + Volver + +
+ +
+
+
+
+
Detalles del Gasto
+ Saldo Disponible: @Model.SaldoDisponible.ToString("C") +
+
+
+
+ + +
+
+ + + +
+
+ +
+ $ + +
+ +
+
+ +
+ + + +
+ +
+ +
+ + + + +
+
+ +
+ + + +
+ +
+ +
+
+
+
+
+
+
+ + + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} + + +} diff --git a/foundation_system/Views/MovimientoCaja/Reposicion.cshtml b/foundation_system/Views/MovimientoCaja/Reposicion.cshtml new file mode 100644 index 0000000..ea1f926 --- /dev/null +++ b/foundation_system/Views/MovimientoCaja/Reposicion.cshtml @@ -0,0 +1,91 @@ +@model foundation_system.Models.CajaChica + +@{ + ViewData["Title"] = "Reposición de Fondo"; + var gastosPendientes = Model.Movimientos.Where(m => m.TipoMovimiento == "GASTO" && m.EstadoReembolso == "PENDIENTE").ToList(); + var totalReembolso = gastosPendientes.Sum(m => m.Monto); +} + +
+
+

@ViewData["Title"]

+ + Volver + +
+ +
+
+
+
+
Gastos Pendientes de Reembolso
+
Total a Reembolsar: @totalReembolso.ToString("C")
+
+
+ @if (gastosPendientes.Any()) + { +
+ + +
+ + + + + + + + + + + + @foreach (var item in gastosPendientes) + { + + + + + + + + } + +
FechaDescripciónCategoríaMonto
+ + @item.FechaMovimiento.ToString("dd/MM/yyyy")@item.Descripcion@(item.CategoriaGasto?.Nombre ?? "-")@item.Monto.ToString("C")
+
+ +
+ + Al confirmar, se generará un movimiento de "REPOSICIÓN" por el total seleccionado y el saldo de la caja aumentará. +
+ +
+ +
+
+ } + else + { +
+

No hay gastos pendientes de reembolso.

+
+ } +
+
+
+
+
+ +@section Scripts { + +} diff --git a/foundation_system/Views/Proveedor/Create.cshtml b/foundation_system/Views/Proveedor/Create.cshtml new file mode 100644 index 0000000..5de882e --- /dev/null +++ b/foundation_system/Views/Proveedor/Create.cshtml @@ -0,0 +1,53 @@ +@model foundation_system.Models.Proveedor + +@{ + ViewData["Title"] = "Nuevo Proveedor"; +} + +
+
+

@ViewData["Title"]

+ + Volver + +
+ +
+
+
+
+
+
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/foundation_system/Views/Proveedor/Edit.cshtml b/foundation_system/Views/Proveedor/Edit.cshtml new file mode 100644 index 0000000..153f4b7 --- /dev/null +++ b/foundation_system/Views/Proveedor/Edit.cshtml @@ -0,0 +1,55 @@ +@model foundation_system.Models.Proveedor + +@{ + ViewData["Title"] = "Editar Proveedor"; +} + +
+
+

@ViewData["Title"]

+ + Volver + +
+ +
+
+
+
+
+
+ + + +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/foundation_system/Views/Proveedor/Index.cshtml b/foundation_system/Views/Proveedor/Index.cshtml new file mode 100644 index 0000000..933d72f --- /dev/null +++ b/foundation_system/Views/Proveedor/Index.cshtml @@ -0,0 +1,55 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Proveedores"; +} + +
+
+

@ViewData["Title"]

+ + Nuevo Proveedor + +
+ +
+
+
+ + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + } + +
NombreNIT / DUIEstadoAcciones
@item.Nombre@item.NitDui + @if (item.Activo) + { + Activo + } + else + { + Inactivo + } + + + + +
+
+
+
+
diff --git a/foundation_system/Views/Shared/_Layout.cshtml b/foundation_system/Views/Shared/_Layout.cshtml index 9df45d3..802b0ef 100644 --- a/foundation_system/Views/Shared/_Layout.cshtml +++ b/foundation_system/Views/Shared/_Layout.cshtml @@ -8,6 +8,7 @@ + @@ -63,6 +64,7 @@ + @await RenderSectionAsync("Scripts", required: false)