Se agrego contabilidad
This commit is contained in:
147
foundation_system/Controllers/CajaChicaController.cs
Normal file
147
foundation_system/Controllers/CajaChicaController.cs
Normal file
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
99
foundation_system/Controllers/CategoriaGastoController.cs
Normal file
99
foundation_system/Controllers/CategoriaGastoController.cs
Normal file
@@ -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<IActionResult> Index()
|
||||
{
|
||||
return View(await _context.CategoriasGastos.ToListAsync());
|
||||
}
|
||||
|
||||
// GET: CategoriaGasto/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: CategoriaGasto/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
193
foundation_system/Controllers/MovimientoCajaController.cs
Normal file
193
foundation_system/Controllers/MovimientoCajaController.cs
Normal file
@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> ConfirmarReposicion(int cajaId, List<long> 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<IActionResult> SearchProveedores(string term)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(term) || term.Length < 2)
|
||||
{
|
||||
return Json(new List<object>());
|
||||
}
|
||||
|
||||
var results = await _context.Database
|
||||
.SqlQueryRaw<SearchResult>(
|
||||
"SELECT id, text, score FROM buscar_personas_v2(@p0, 'PROVEEDOR')",
|
||||
term)
|
||||
.ToListAsync();
|
||||
|
||||
return Json(results);
|
||||
}
|
||||
}
|
||||
}
|
||||
99
foundation_system/Controllers/ProveedorController.cs
Normal file
99
foundation_system/Controllers/ProveedorController.cs
Normal file
@@ -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<IActionResult> Index()
|
||||
{
|
||||
return View(await _context.Proveedores.ToListAsync());
|
||||
}
|
||||
|
||||
// GET: Proveedor/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
// POST: Proveedor/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,13 @@ public class ApplicationDbContext : DbContext
|
||||
public DbSet<RolPermiso> RolesPermisos { get; set; }
|
||||
public DbSet<Colaborador> Colaboradores { get; set; }
|
||||
public DbSet<CargoColaborador> CargosColaboradores { get; set; }
|
||||
|
||||
// Caja Chica
|
||||
public DbSet<CajaChica> CajasChicas { get; set; }
|
||||
public DbSet<CajaChicaMovimiento> CajaChicaMovimientos { get; set; }
|
||||
public DbSet<CategoriaGasto> CategoriasGastos { get; set; }
|
||||
public DbSet<Proveedor> Proveedores { get; set; }
|
||||
public DbSet<DocumentoSoporte> DocumentosSoporte { get; set; }
|
||||
public DbSet<AsistenciaColaborador> AsistenciasColaboradores { get; set; }
|
||||
public DbSet<Nino> Ninos { get; set; }
|
||||
public DbSet<Asistencia> Asistencias { get; set; }
|
||||
|
||||
@@ -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 $$;
|
||||
65
foundation_system/Migrations/Scripts/MigrateCajaChica.sql
Normal file
65
foundation_system/Migrations/Scripts/MigrateCajaChica.sql
Normal file
@@ -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);
|
||||
360
foundation_system/Migrations/Scripts/UpdateBuscarPersonas_v3.sql
Normal file
360
foundation_system/Migrations/Scripts/UpdateBuscarPersonas_v3.sql
Normal file
@@ -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$;
|
||||
49
foundation_system/Models/CajaChica.cs
Normal file
49
foundation_system/Models/CajaChica.cs
Normal file
@@ -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<CajaChicaMovimiento> Movimientos { get; set; } = new List<CajaChicaMovimiento>();
|
||||
}
|
||||
}
|
||||
61
foundation_system/Models/CajaChicaMovimiento.cs
Normal file
61
foundation_system/Models/CajaChicaMovimiento.cs
Normal file
@@ -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<DocumentoSoporte> Documentos { get; set; } = new List<DocumentoSoporte>();
|
||||
}
|
||||
}
|
||||
28
foundation_system/Models/CategoriaGasto.cs
Normal file
28
foundation_system/Models/CategoriaGasto.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
34
foundation_system/Models/DocumentoSoporte.cs
Normal file
34
foundation_system/Models/DocumentoSoporte.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
28
foundation_system/Models/Proveedor.cs
Normal file
28
foundation_system/Models/Proveedor.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
27
foundation_system/Models/ViewModels/CajaChicaViewModel.cs
Normal file
27
foundation_system/Models/ViewModels/CajaChicaViewModel.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
74
foundation_system/Views/CajaChica/Create.cshtml
Normal file
74
foundation_system/Views/CajaChica/Create.cshtml
Normal file
@@ -0,0 +1,74 @@
|
||||
@model foundation_system.Models.ViewModels.CajaChicaViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Apertura de Caja Chica";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-action="Index" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Datos de Apertura</h6>
|
||||
</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="Nombre" class="form-label">Nombre de la Caja</label>
|
||||
<input asp-for="Nombre" class="form-control" placeholder="Ej. Caja Chica Administración" />
|
||||
<span asp-validation-for="Nombre" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="ResponsableUsuarioId" class="form-label">Responsable (Custodio)</label>
|
||||
<select asp-for="ResponsableUsuarioId" class="form-select" asp-items="ViewBag.ResponsableUsuarioId">
|
||||
<option value="">-- Seleccione un Responsable --</option>
|
||||
</select>
|
||||
<span asp-validation-for="ResponsableUsuarioId" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="MontoAsignado" class="form-label">Monto del Fondo ($)</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input asp-for="MontoAsignado" class="form-control" type="number" step="0.01" min="0" />
|
||||
</div>
|
||||
<span asp-validation-for="MontoAsignado" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="FechaApertura" class="form-label">Fecha de Apertura</label>
|
||||
<input asp-for="FechaApertura" class="form-control" type="date" />
|
||||
<span asp-validation-for="FechaApertura" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Al crear la caja, se registrará automáticamente un movimiento de "Apertura" por el monto asignado.
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle me-2"></i> Abrir Caja
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
176
foundation_system/Views/CajaChica/Details.cshtml
Normal file
176
foundation_system/Views/CajaChica/Details.cshtml
Normal file
@@ -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");
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0 text-gray-800">@Model.Nombre</h1>
|
||||
<p class="mb-0 text-muted">Responsable: @Model.Responsable?.Persona?.Nombres @Model.Responsable?.Persona?.Apellidos</p>
|
||||
</div>
|
||||
<div>
|
||||
<a asp-action="Index" class="btn btn-secondary me-2">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
@if (Model.Estado == "ABIERTA")
|
||||
{
|
||||
<a asp-controller="MovimientoCaja" asp-action="CreateGasto" asp-route-cajaId="@Model.Id" class="btn btn-danger me-2">
|
||||
<i class="bi bi-dash-circle me-1"></i> Registrar Gasto
|
||||
</a>
|
||||
<a asp-controller="MovimientoCaja" asp-action="Reposicion" asp-route-cajaId="@Model.Id" class="btn btn-success me-2">
|
||||
<i class="bi bi-arrow-repeat me-1"></i> Reposición
|
||||
</a>
|
||||
<form id="formCloseBox" asp-action="Close" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||
<button type="button" class="btn btn-secondary" onclick="confirmClose()">
|
||||
<i class="bi bi-lock me-1"></i> Cerrar Caja
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary p-2">CAJA CERRADA</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-4 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">Monto Asignado</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.MontoAsignado.ToString("C")</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-wallet2 fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-4 col-md-6 mb-4">
|
||||
<div class="card border-left-@colorSaldo 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-@colorSaldo text-uppercase mb-1">Saldo Disponible</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.SaldoActual.ToString("C")</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-cash-stack fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-4 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">Gastos Pendientes</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||
@Model.Movimientos.Where(m => m.TipoMovimiento == "GASTO" && m.EstadoReembolso == "PENDIENTE").Sum(m => m.Monto).ToString("C")
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-receipt fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Movimientos Table -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Historial de Movimientos</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Tipo</th>
|
||||
<th>Descripción</th>
|
||||
<th>Categoría</th>
|
||||
<th>Monto</th>
|
||||
<th>Estado</th>
|
||||
<th>Registrado Por</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var mov in Model.Movimientos)
|
||||
{
|
||||
<tr>
|
||||
<td>@mov.FechaMovimiento.ToString("dd/MM/yyyy")</td>
|
||||
<td>
|
||||
@if(mov.TipoMovimiento == "GASTO") { <span class="badge bg-danger">GASTO</span> }
|
||||
else if(mov.TipoMovimiento == "REPOSICION") { <span class="badge bg-success">REPOSICIÓN</span> }
|
||||
else { <span class="badge bg-info">@mov.TipoMovimiento</span> }
|
||||
</td>
|
||||
<td>@mov.Descripcion</td>
|
||||
<td>@(mov.CategoriaGasto?.Nombre ?? "-")</td>
|
||||
<td class="text-end">@mov.Monto.ToString("C")</td>
|
||||
<td>
|
||||
@if(mov.TipoMovimiento == "GASTO")
|
||||
{
|
||||
@if(mov.EstadoReembolso == "PENDIENTE") { <span class="badge bg-warning text-dark">Pendiente</span> }
|
||||
else { <span class="badge bg-success">Reembolsado</span> }
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
</td>
|
||||
<td>@mov.UsuarioRegistro?.Persona?.Nombres</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Styles {
|
||||
<style>
|
||||
.border-left-primary { border-left: 0.25rem solid #4e73df !important; }
|
||||
.border-left-success { border-left: 0.25rem solid #1cc88a !important; }
|
||||
.border-left-danger { border-left: 0.25rem solid #e74a3b !important; }
|
||||
.border-left-warning { border-left: 0.25rem solid #f6c23e !important; }
|
||||
.border-left-info { border-left: 0.25rem solid #36b9cc !important; }
|
||||
.text-xs { font-size: .7rem; }
|
||||
</style>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function confirmClose() {
|
||||
Swal.fire({
|
||||
title: '¿Cerrar Caja Chica?',
|
||||
text: "Esta acción cambiará el estado de la caja a CERRADA. Asegúrese de haber realizado el arqueo final.",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#6c757d',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Sí, cerrar caja',
|
||||
cancelButtonText: 'Cancelar'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
document.getElementById('formCloseBox').submit();
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
}
|
||||
61
foundation_system/Views/CajaChica/Index.cshtml
Normal file
61
foundation_system/Views/CajaChica/Index.cshtml
Normal file
@@ -0,0 +1,61 @@
|
||||
@model IEnumerable<foundation_system.Models.CajaChica>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Cajas Chicas";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i> Apertura de Caja
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Responsable</th>
|
||||
<th>Monto Asignado</th>
|
||||
<th>Saldo Actual</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Nombre</td>
|
||||
<td>@item.Responsable?.Persona?.Nombres @item.Responsable?.Persona?.Apellidos</td>
|
||||
<td>@item.MontoAsignado.ToString("C")</td>
|
||||
<td class="@(item.SaldoActual < (item.MontoAsignado * 0.2m) ? "text-danger fw-bold" : "text-success fw-bold")">
|
||||
@item.SaldoActual.ToString("C")
|
||||
</td>
|
||||
<td>
|
||||
@if (item.Estado == "ABIERTA")
|
||||
{
|
||||
<span class="badge bg-success">Abierta</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">@item.Estado</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-info text-white">
|
||||
<i class="bi bi-eye"></i> Detalle
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
53
foundation_system/Views/CategoriaGasto/Create.cshtml
Normal file
53
foundation_system/Views/CategoriaGasto/Create.cshtml
Normal file
@@ -0,0 +1,53 @@
|
||||
@model foundation_system.Models.CategoriaGasto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Nueva Categoría de Gasto";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-action="Index" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<form asp-action="Create">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Nombre" class="form-label">Nombre</label>
|
||||
<input asp-for="Nombre" class="form-control" />
|
||||
<span asp-validation-for="Nombre" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="CodigoCuentaContable" class="form-label">Código Contable</label>
|
||||
<input asp-for="CodigoCuentaContable" class="form-control" />
|
||||
<span asp-validation-for="CodigoCuentaContable" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input class="form-check-input" asp-for="Activo" />
|
||||
<label class="form-check-label" asp-for="Activo">
|
||||
Activo
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
55
foundation_system/Views/CategoriaGasto/Edit.cshtml
Normal file
55
foundation_system/Views/CategoriaGasto/Edit.cshtml
Normal file
@@ -0,0 +1,55 @@
|
||||
@model foundation_system.Models.CategoriaGasto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Editar Categoría de Gasto";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-action="Index" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<form asp-action="Edit">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<input type="hidden" asp-for="CreadoEn" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Nombre" class="form-label">Nombre</label>
|
||||
<input asp-for="Nombre" class="form-control" />
|
||||
<span asp-validation-for="Nombre" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="CodigoCuentaContable" class="form-label">Código Contable</label>
|
||||
<input asp-for="CodigoCuentaContable" class="form-control" />
|
||||
<span asp-validation-for="CodigoCuentaContable" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input class="form-check-input" asp-for="Activo" />
|
||||
<label class="form-check-label" asp-for="Activo">
|
||||
Activo
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
55
foundation_system/Views/CategoriaGasto/Index.cshtml
Normal file
55
foundation_system/Views/CategoriaGasto/Index.cshtml
Normal file
@@ -0,0 +1,55 @@
|
||||
@model IEnumerable<foundation_system.Models.CategoriaGasto>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Categorías de Gasto";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i> Nueva Categoría
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Código Contable</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Nombre</td>
|
||||
<td>@item.CodigoCuentaContable</td>
|
||||
<td>
|
||||
@if (item.Activo)
|
||||
{
|
||||
<span class="badge bg-success">Activo</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Inactivo</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
160
foundation_system/Views/MovimientoCaja/CreateGasto.cshtml
Normal file
160
foundation_system/Views/MovimientoCaja/CreateGasto.cshtml
Normal file
@@ -0,0 +1,160 @@
|
||||
@model foundation_system.Models.ViewModels.RegistroGastoViewModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Registrar Gasto";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-controller="CajaChica" asp-action="Details" asp-route-id="@Model.CajaChicaId" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Detalles del Gasto</h6>
|
||||
<span class="badge bg-success">Saldo Disponible: @Model.SaldoDisponible.ToString("C")</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-action="CreateGasto">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<input type="hidden" asp-for="CajaChicaId" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label asp-for="Fecha" class="form-label">Fecha</label>
|
||||
<input asp-for="Fecha" class="form-control" type="date" />
|
||||
<span asp-validation-for="Fecha" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-6 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" min="0.01" max="@Model.SaldoDisponible" />
|
||||
</div>
|
||||
<span asp-validation-for="Monto" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="CategoriaId" class="form-label">Categoría</label>
|
||||
<select asp-for="CategoriaId" class="form-select" asp-items="ViewBag.CategoriaId">
|
||||
<option value="">-- Seleccione una Categoría --</option>
|
||||
</select>
|
||||
<span asp-validation-for="CategoriaId" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="ProveedorId" class="form-label">Proveedor (Opcional)</label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" asp-for="ProveedorId" id="ProveedorId" />
|
||||
<input type="text" id="ProveedorDisplay" class="form-control" placeholder="Buscar proveedor..." readonly />
|
||||
<button class="btn btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#searchModal">
|
||||
<i class="bi bi-search"></i> Buscar
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" type="button" id="btnClearProveedor" title="Limpiar selección">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Descripcion" class="form-label">Descripción / Concepto</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">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-dash-circle me-2"></i> Registrar Gasto
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<div class="modal fade" id="searchModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Buscar Proveedor</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Escriba el nombre o NIT del proveedor..." autocomplete="off">
|
||||
<button class="btn btn-outline-primary" type="button" id="btnSearch">Buscar</button>
|
||||
</div>
|
||||
<div class="list-group" id="searchResults">
|
||||
<!-- Results will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let searchTimeout;
|
||||
|
||||
// Search function
|
||||
function performSearch() {
|
||||
const term = $('#searchInput').val();
|
||||
if (term.length < 2) return;
|
||||
|
||||
$('#searchResults').html('<div class="text-center p-3"><div class="spinner-border text-primary" role="status"></div></div>');
|
||||
|
||||
$.get('@Url.Action("SearchProveedores", "MovimientoCaja")', { term: term })
|
||||
.done(function(data) {
|
||||
$('#searchResults').empty();
|
||||
if (data.length === 0) {
|
||||
$('#searchResults').append('<div class="list-group-item text-muted">No se encontraron resultados</div>');
|
||||
} else {
|
||||
data.forEach(function(item) {
|
||||
const btn = $('<button type="button" class="list-group-item list-group-item-action"></button>')
|
||||
.text(item.text)
|
||||
.click(function() {
|
||||
$('#ProveedorId').val(item.id);
|
||||
$('#ProveedorDisplay').val(item.text);
|
||||
$('#searchModal').modal('hide');
|
||||
});
|
||||
$('#searchResults').append(btn);
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
$('#searchResults').html('<div class="list-group-item text-danger">Error al buscar proveedores</div>');
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
$('#btnSearch').click(performSearch);
|
||||
|
||||
$('#searchInput').on('input', function() {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(performSearch, 500);
|
||||
});
|
||||
|
||||
$('#btnClearProveedor').click(function() {
|
||||
$('#ProveedorId').val('');
|
||||
$('#ProveedorDisplay').val('');
|
||||
});
|
||||
|
||||
// Focus input when modal opens
|
||||
$('#searchModal').on('shown.bs.modal', function () {
|
||||
$('#searchInput').focus();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
91
foundation_system/Views/MovimientoCaja/Reposicion.cshtml
Normal file
91
foundation_system/Views/MovimientoCaja/Reposicion.cshtml
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-controller="CajaChica" asp-action="Details" asp-route-id="@Model.Id" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Gastos Pendientes de Reembolso</h6>
|
||||
<h5 class="m-0 font-weight-bold text-success">Total a Reembolsar: @totalReembolso.ToString("C")</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (gastosPendientes.Any())
|
||||
{
|
||||
<form asp-action="ConfirmarReposicion" method="post">
|
||||
<input type="hidden" name="cajaId" value="@Model.Id" />
|
||||
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="checkAll" checked /></th>
|
||||
<th>Fecha</th>
|
||||
<th>Descripción</th>
|
||||
<th>Categoría</th>
|
||||
<th class="text-end">Monto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in gastosPendientes)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="movimientoIds" value="@item.Id" checked class="mov-check" />
|
||||
</td>
|
||||
<td>@item.FechaMovimiento.ToString("dd/MM/yyyy")</td>
|
||||
<td>@item.Descripcion</td>
|
||||
<td>@(item.CategoriaGasto?.Nombre ?? "-")</td>
|
||||
<td class="text-end">@item.Monto.ToString("C")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
Al confirmar, se generará un movimiento de "REPOSICIÓN" por el total seleccionado y el saldo de la caja aumentará.
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" class="btn btn-success btn-lg">
|
||||
<i class="bi bi-check-lg me-2"></i> Confirmar Reposición
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-5">
|
||||
<p class="text-muted">No hay gastos pendientes de reembolso.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
document.getElementById('checkAll').addEventListener('change', function() {
|
||||
var checkboxes = document.getElementsByClassName('mov-check');
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = this.checked;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
53
foundation_system/Views/Proveedor/Create.cshtml
Normal file
53
foundation_system/Views/Proveedor/Create.cshtml
Normal file
@@ -0,0 +1,53 @@
|
||||
@model foundation_system.Models.Proveedor
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Nuevo Proveedor";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-action="Index" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<form asp-action="Create">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Nombre" class="form-label">Nombre / Razón Social</label>
|
||||
<input asp-for="Nombre" class="form-control" />
|
||||
<span asp-validation-for="Nombre" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="NitDui" class="form-label">NIT / DUI</label>
|
||||
<input asp-for="NitDui" class="form-control" />
|
||||
<span asp-validation-for="NitDui" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input class="form-check-input" asp-for="Activo" />
|
||||
<label class="form-check-label" asp-for="Activo">
|
||||
Activo
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
55
foundation_system/Views/Proveedor/Edit.cshtml
Normal file
55
foundation_system/Views/Proveedor/Edit.cshtml
Normal file
@@ -0,0 +1,55 @@
|
||||
@model foundation_system.Models.Proveedor
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Editar Proveedor";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-action="Index" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<form asp-action="Edit">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<input type="hidden" asp-for="Id" />
|
||||
<input type="hidden" asp-for="CreadoEn" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="Nombre" class="form-label">Nombre / Razón Social</label>
|
||||
<input asp-for="Nombre" class="form-control" />
|
||||
<span asp-validation-for="Nombre" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="NitDui" class="form-label">NIT / DUI</label>
|
||||
<input asp-for="NitDui" class="form-control" />
|
||||
<span asp-validation-for="NitDui" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input class="form-check-input" asp-for="Activo" />
|
||||
<label class="form-check-label" asp-for="Activo">
|
||||
Activo
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Guardar Cambios</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
||||
}
|
||||
55
foundation_system/Views/Proveedor/Index.cshtml
Normal file
55
foundation_system/Views/Proveedor/Index.cshtml
Normal file
@@ -0,0 +1,55 @@
|
||||
@model IEnumerable<foundation_system.Models.Proveedor>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Proveedores";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">@ViewData["Title"]</h1>
|
||||
<a asp-action="Create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i> Nuevo Proveedor
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>NIT / DUI</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@item.Nombre</td>
|
||||
<td>@item.NitDui</td>
|
||||
<td>
|
||||
@if (item.Activo)
|
||||
{
|
||||
<span class="badge bg-success">Activo</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Inactivo</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -8,6 +8,7 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
@@ -63,6 +64,7 @@
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user