Reportes de contabilidad

This commit is contained in:
2025-12-31 18:35:30 -06:00
parent 4e6a5448ed
commit 2c0c3e7148
39 changed files with 1681 additions and 15 deletions

View File

@@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using foundation_system.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace foundation_system.Controllers
{
[Authorize]
public class ReportesController : Controller
{
private readonly IReporteService _reporteService;
public ReportesController(IReporteService reporteService)
{
_reporteService = reporteService;
}
public IActionResult Index()
{
return View();
}
[HttpGet]
public async Task<IActionResult> ArqueoCaja(DateOnly? fecha)
{
var fechaReporte = fecha ?? DateOnly.FromDateTime(DateTime.Today);
var model = await _reporteService.ObtenerArqueoCajaAsync(fechaReporte);
return View(model);
}
[HttpGet]
public async Task<IActionResult> Movimientos(DateOnly? inicio, DateOnly? fin)
{
var fInicio = inicio ?? DateOnly.FromDateTime(DateTime.Today.AddDays(-30));
var fFin = fin ?? DateOnly.FromDateTime(DateTime.Today);
var model = await _reporteService.ObtenerMovimientosAsync(fInicio, fFin);
return View(model);
}
[HttpGet]
public async Task<IActionResult> HistoricoSaldos(DateOnly? inicio, DateOnly? fin)
{
var fInicio = inicio ?? DateOnly.FromDateTime(DateTime.Today.AddDays(-30));
var fFin = fin ?? DateOnly.FromDateTime(DateTime.Today);
var model = await _reporteService.ObtenerHistoricoSaldosAsync(fInicio, fFin);
return View(model);
}
[HttpGet]
public async Task<IActionResult> GastosCategoria(DateOnly? inicio, DateOnly? fin)
{
var fInicio = inicio ?? DateOnly.FromDateTime(DateTime.Today.AddDays(-30));
var fFin = fin ?? DateOnly.FromDateTime(DateTime.Today);
var model = await _reporteService.ObtenerGastosCategoriaAsync(fInicio, fFin);
return View(model);
}
[HttpGet]
public async Task<IActionResult> Reposiciones(DateOnly? inicio, DateOnly? fin)
{
var fInicio = inicio ?? DateOnly.FromDateTime(DateTime.Today.AddDays(-30));
var fFin = fin ?? DateOnly.FromDateTime(DateTime.Today);
var model = await _reporteService.ObtenerReposicionesAsync(fInicio, fFin);
return View(model);
}
}
}

View File

@@ -0,0 +1,33 @@
-- =============================================
-- Author: System
-- Create date: 2025-12-31
-- Description: Adds permissions for new financial reports.
-- =============================================
DO $$
DECLARE
v_modulo_id INT;
BEGIN
SELECT id INTO v_modulo_id FROM modulos WHERE nombre = 'Reportes';
-- 1. Movimientos
INSERT INTO permisos (modulo_id, codigo, nombre, descripcion, url, icono, orden, es_menu)
SELECT v_modulo_id, 'reportes.movimientos', 'Movimientos de Caja', 'Detalle de ingresos y egresos', '/Reportes/Movimientos', 'bi bi-list-check', 2, true
WHERE NOT EXISTS (SELECT 1 FROM permisos WHERE codigo = 'reportes.movimientos');
-- 2. Histórico de Saldos
INSERT INTO permisos (modulo_id, codigo, nombre, descripcion, url, icono, orden, es_menu)
SELECT v_modulo_id, 'reportes.saldos', 'Histórico de Saldos', 'Evolución diaria del saldo', '/Reportes/HistoricoSaldos', 'bi bi-graph-up', 3, true
WHERE NOT EXISTS (SELECT 1 FROM permisos WHERE codigo = 'reportes.saldos');
-- 3. Gastos por Categoría
INSERT INTO permisos (modulo_id, codigo, nombre, descripcion, url, icono, orden, es_menu)
SELECT v_modulo_id, 'reportes.gastos', 'Gastos por Categoría', 'Análisis de gastos', '/Reportes/GastosCategoria', 'bi bi-pie-chart', 4, true
WHERE NOT EXISTS (SELECT 1 FROM permisos WHERE codigo = 'reportes.gastos');
-- 4. Reposiciones
INSERT INTO permisos (modulo_id, codigo, nombre, descripcion, url, icono, orden, es_menu)
SELECT v_modulo_id, 'reportes.reposiciones', 'Reposiciones', 'Reporte de reintegros', '/Reportes/Reposiciones', 'bi bi-cash-stack', 5, true
WHERE NOT EXISTS (SELECT 1 FROM permisos WHERE codigo = 'reportes.reposiciones');
END $$;

View File

@@ -0,0 +1,22 @@
-- =============================================
-- Author: System
-- Create date: 2025-12-31
-- Description: Adds 'Reportes' module and 'Arqueo de Caja' menu item.
-- =============================================
-- 1. Ensure 'Reportes' module exists
INSERT INTO modulos (nombre, descripcion, icono, orden, activo)
SELECT 'Reportes', 'Módulo de Reportes', 'bi bi-file-earmark-bar-graph', 90, true
WHERE NOT EXISTS (SELECT 1 FROM modulos WHERE nombre = 'Reportes');
-- 2. Add 'Arqueo de Caja' permission/menu item
DO $$
DECLARE
v_modulo_id INT;
BEGIN
SELECT id INTO v_modulo_id FROM modulos WHERE nombre = 'Reportes';
INSERT INTO permisos (modulo_id, codigo, nombre, descripcion, url, icono, orden, es_menu)
SELECT v_modulo_id, 'reportes.arqueo', 'Arqueo de Caja', 'Reporte diario de caja', '/Reportes/ArqueoCaja', 'bi bi-cash-coin', 1, true
WHERE NOT EXISTS (SELECT 1 FROM permisos WHERE codigo = 'reportes.arqueo');
END $$;

View File

@@ -0,0 +1,134 @@
-- =============================================
-- Author: System
-- Create date: 2025-12-31
-- Description: Stored Procedures for Financial Reports
-- =============================================
-- 1. Reporte de Movimientos Detallado
CREATE OR REPLACE FUNCTION sp_reporte_movimientos(p_fecha_inicio DATE, p_fecha_fin DATE)
RETURNS TABLE (
id BIGINT,
fecha DATE,
tipo_movimiento VARCHAR,
concepto TEXT,
categoria VARCHAR,
monto DECIMAL(12,2),
usuario VARCHAR
) AS $$
BEGIN
RETURN QUERY
SELECT
m.id,
m.fecha_movimiento,
m.tipo_movimiento::VARCHAR,
m.descripcion::TEXT as concepto,
COALESCE(c.nombre, 'General')::VARCHAR as categoria,
m.monto,
COALESCE(p.nombres || ' ' || p.apellidos, 'Sistema')::VARCHAR as usuario
FROM caja_chica_movimientos m
LEFT JOIN categorias_gastos c ON m.categoria_gasto_id = c.id
LEFT JOIN usuarios u ON m.usuario_registro_id = u.id
LEFT JOIN personas p ON u.persona_id = p.id
WHERE m.fecha_movimiento BETWEEN p_fecha_inicio AND p_fecha_fin
ORDER BY m.fecha_movimiento DESC, m.creado_en DESC;
END;
$$ LANGUAGE plpgsql;
-- 2. Reporte de Gastos por Categoría
CREATE OR REPLACE FUNCTION sp_reporte_gastos_categoria(p_fecha_inicio DATE, p_fecha_fin DATE)
RETURNS TABLE (
categoria VARCHAR,
cantidad BIGINT,
monto_total DECIMAL(12,2)
) AS $$
BEGIN
RETURN QUERY
SELECT
COALESCE(c.nombre, 'Sin Categoría')::VARCHAR as categoria,
COUNT(*) as cantidad,
SUM(m.monto) as monto_total
FROM caja_chica_movimientos m
LEFT JOIN categorias_gastos c ON m.categoria_gasto_id = c.id
WHERE m.fecha_movimiento BETWEEN p_fecha_inicio AND p_fecha_fin
AND m.tipo_movimiento IN ('GASTO', 'DISMINUCION_FONDO')
GROUP BY COALESCE(c.nombre, 'Sin Categoría')
ORDER BY monto_total DESC;
END;
$$ LANGUAGE plpgsql;
-- 3. Reporte de Reposiciones
CREATE OR REPLACE FUNCTION sp_reporte_reposiciones(p_fecha_inicio DATE, p_fecha_fin DATE)
RETURNS TABLE (
fecha DATE,
monto DECIMAL(12,2),
usuario VARCHAR,
descripcion TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
m.fecha_movimiento,
m.monto,
COALESCE(p.nombres || ' ' || p.apellidos, 'Sistema')::VARCHAR as usuario,
m.descripcion::TEXT
FROM caja_chica_movimientos m
LEFT JOIN usuarios u ON m.usuario_registro_id = u.id
LEFT JOIN personas p ON u.persona_id = p.id
WHERE m.fecha_movimiento BETWEEN p_fecha_inicio AND p_fecha_fin
AND m.tipo_movimiento IN ('REPOSICION', 'AUMENTO_FONDO', 'APERTURA')
ORDER BY m.fecha_movimiento DESC;
END;
$$ LANGUAGE plpgsql;
-- 4. Histórico de Saldos (Complejo: Requiere calcular saldo día a día)
-- Nota: Esto puede ser lento en rangos grandes. Para producción real se recomienda una tabla de snapshots diarios.
-- Aquí usaremos una aproximación calculando el saldo acumulado.
CREATE OR REPLACE FUNCTION sp_reporte_historico_saldos(p_fecha_inicio DATE, p_fecha_fin DATE)
RETURNS TABLE (
fecha DATE,
saldo_inicial DECIMAL(12,2),
ingresos DECIMAL(12,2),
egresos DECIMAL(12,2),
saldo_final DECIMAL(12,2)
) AS $$
DECLARE
r RECORD;
v_saldo_acumulado DECIMAL(12,2) := 0;
v_fecha_iteracion DATE;
BEGIN
-- 1. Calcular saldo inicial antes del periodo
SELECT COALESCE(SUM(
CASE
WHEN tipo_movimiento IN ('APERTURA', 'REPOSICION', 'AUMENTO_FONDO') THEN monto
WHEN tipo_movimiento IN ('GASTO', 'DISMINUCION_FONDO') THEN -monto
ELSE 0
END), 0)
INTO v_saldo_acumulado
FROM caja_chica_movimientos
WHERE fecha_movimiento < p_fecha_inicio;
-- 2. Iterar por cada día del rango (o solo días con movimientos si se prefiere, aquí haremos todos los días)
-- Generar serie de fechas
FOR v_fecha_iteracion IN SELECT generate_series(p_fecha_inicio, p_fecha_fin, '1 day'::interval)::date
LOOP
-- Calcular movimientos del día
SELECT
COALESCE(SUM(CASE WHEN tipo_movimiento IN ('APERTURA', 'REPOSICION', 'AUMENTO_FONDO') THEN monto ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN tipo_movimiento IN ('GASTO', 'DISMINUCION_FONDO') THEN monto ELSE 0 END), 0)
INTO ingresos, egresos
FROM caja_chica_movimientos
WHERE fecha_movimiento = v_fecha_iteracion;
-- Asignar valores
fecha := v_fecha_iteracion;
saldo_inicial := v_saldo_acumulado;
saldo_final := saldo_inicial + ingresos - egresos;
-- Actualizar acumulado para el siguiente día
v_saldo_acumulado := saldo_final;
RETURN NEXT;
END LOOP;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,59 @@
-- =============================================
-- Author: System
-- Create date: 2025-12-31
-- Description: Generates the daily cash balance report (Arqueo de Caja)
-- Uses 'caja_chica_movimientos' table.
-- =============================================
CREATE OR REPLACE FUNCTION sp_reporte_arqueo_caja(p_fecha DATE)
RETURNS TABLE (
saldo_inicial DECIMAL(12,2),
total_ingresos DECIMAL(12,2),
total_egresos DECIMAL(12,2),
saldo_final_teorico DECIMAL(12,2),
ingresos_efectivo DECIMAL(12,2),
ingresos_cheque DECIMAL(12,2),
ingresos_transferencia DECIMAL(12,2)
) AS $$
DECLARE
v_saldo_inicial DECIMAL(12,2);
v_total_ingresos DECIMAL(12,2);
v_total_egresos DECIMAL(12,2);
BEGIN
-- 1. Saldo Inicial (Todo lo anterior a p_fecha)
-- Ingresos: APERTURA, REPOSICION, AUMENTO_FONDO
-- Egresos: GASTO, DISMINUCION_FONDO
SELECT COALESCE(SUM(
CASE
WHEN tipo_movimiento IN ('APERTURA', 'REPOSICION', 'AUMENTO_FONDO') THEN monto
WHEN tipo_movimiento IN ('GASTO', 'DISMINUCION_FONDO') THEN -monto
ELSE 0
END), 0)
INTO v_saldo_inicial
FROM caja_chica_movimientos
WHERE fecha_movimiento < p_fecha;
-- 2. Totales del Día
SELECT
COALESCE(SUM(CASE WHEN tipo_movimiento IN ('APERTURA', 'REPOSICION', 'AUMENTO_FONDO') THEN monto ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN tipo_movimiento IN ('GASTO', 'DISMINUCION_FONDO') THEN monto ELSE 0 END), 0)
INTO
v_total_ingresos,
v_total_egresos
FROM caja_chica_movimientos
WHERE fecha_movimiento = p_fecha;
-- 3. Retornar
saldo_inicial := v_saldo_inicial;
total_ingresos := v_total_ingresos;
total_egresos := v_total_egresos;
saldo_final_teorico := v_saldo_inicial + v_total_ingresos - v_total_egresos;
-- Asumimos todo es efectivo en Caja Chica por definición
ingresos_efectivo := v_total_ingresos;
ingresos_cheque := 0;
ingresos_transferencia := 0;
RETURN NEXT;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,22 @@
using System;
namespace foundation_system.Models.ViewModels.Reportes
{
public class ReporteArqueoViewModel
{
public DateOnly Fecha { get; set; }
public decimal SaldoInicial { get; set; }
public decimal TotalIngresos { get; set; }
public decimal TotalEgresos { get; set; }
public decimal SaldoFinalTeorico { get; set; }
// Desglose de Ingresos
public decimal IngresosEfectivo { get; set; }
public decimal IngresosCheque { get; set; }
public decimal IngresosTransferencia { get; set; }
// Campos para auditoría/firma
public string GeneradoPor { get; set; }
public DateTime FechaGeneracion { get; set; }
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
namespace foundation_system.Models.ViewModels.Reportes
{
// 1. Movimientos de Caja Chica
public class ReporteMovimientosViewModel
{
public DateOnly FechaInicio { get; set; }
public DateOnly FechaFin { get; set; }
public List<MovimientoDetalleItem> Movimientos { get; set; } = new();
public decimal TotalIngresos { get; set; }
public decimal TotalEgresos { get; set; }
public decimal SaldoPeriodo => TotalIngresos - TotalEgresos;
public string GeneradoPor { get; set; } = string.Empty;
public DateTime FechaGeneracion { get; set; }
}
public class MovimientoDetalleItem
{
public long Id { get; set; }
public DateOnly Fecha { get; set; }
public string Tipo { get; set; } = string.Empty; // INGRESO/EGRESO
public string Concepto { get; set; } = string.Empty;
public string Categoria { get; set; } = string.Empty;
public decimal Monto { get; set; }
public string Usuario { get; set; } = string.Empty;
}
// 2. Histórico de Saldos
public class ReporteHistoricoSaldosViewModel
{
public DateOnly FechaInicio { get; set; }
public DateOnly FechaFin { get; set; }
public List<SaldoDiarioItem> Saldos { get; set; } = new();
public string GeneradoPor { get; set; } = string.Empty;
public DateTime FechaGeneracion { get; set; }
}
public class SaldoDiarioItem
{
public DateOnly Fecha { get; set; }
public decimal SaldoInicial { get; set; }
public decimal Ingresos { get; set; }
public decimal Egresos { get; set; }
public decimal SaldoFinal { get; set; }
}
// 3. Gastos por Categoría
public class ReporteGastosCategoriaViewModel
{
public DateOnly FechaInicio { get; set; }
public DateOnly FechaFin { get; set; }
public List<GastoCategoriaItem> Categorias { get; set; } = new();
public decimal TotalGastos { get; set; }
public string GeneradoPor { get; set; } = string.Empty;
public DateTime FechaGeneracion { get; set; }
}
public class GastoCategoriaItem
{
public string Categoria { get; set; } = string.Empty;
public int CantidadTransacciones { get; set; }
public decimal MontoTotal { get; set; }
public decimal Porcentaje { get; set; }
}
// 4. Reposiciones
public class ReporteReposicionesViewModel
{
public DateOnly FechaInicio { get; set; }
public DateOnly FechaFin { get; set; }
public List<ReposicionItem> Reposiciones { get; set; } = new();
public decimal TotalRepuesto { get; set; }
public string GeneradoPor { get; set; } = string.Empty;
public DateTime FechaGeneracion { get; set; }
}
public class ReposicionItem
{
public DateOnly Fecha { get; set; }
public decimal Monto { get; set; }
public string Usuario { get; set; } = string.Empty;
public string Descripcion { get; set; } = string.Empty;
}
}

View File

@@ -17,6 +17,7 @@ builder.Services.AddDatabaseDeveloperPageExceptionFilter();
// Register services // Register services
builder.Services.AddScoped<IAuthService, AuthService>(); builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IAntecedentesService, AntecedentesService>(); builder.Services.AddScoped<IAntecedentesService, AntecedentesService>();
builder.Services.AddScoped<IReporteService, ReporteService>();
// Configure cookie authentication // Configure cookie authentication
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)

View File

@@ -0,0 +1,15 @@
using System;
using System.Threading.Tasks;
using foundation_system.Models.ViewModels.Reportes;
namespace foundation_system.Services
{
public interface IReporteService
{
Task<ReporteArqueoViewModel> ObtenerArqueoCajaAsync(DateOnly fecha);
Task<ReporteMovimientosViewModel> ObtenerMovimientosAsync(DateOnly inicio, DateOnly fin);
Task<ReporteHistoricoSaldosViewModel> ObtenerHistoricoSaldosAsync(DateOnly inicio, DateOnly fin);
Task<ReporteGastosCategoriaViewModel> ObtenerGastosCategoriaAsync(DateOnly inicio, DateOnly fin);
Task<ReporteReposicionesViewModel> ObtenerReposicionesAsync(DateOnly inicio, DateOnly fin);
}
}

View File

@@ -0,0 +1,278 @@
using System;
using System.Data;
using System.Threading.Tasks;
using foundation_system.Data;
using foundation_system.Models.ViewModels.Reportes;
using Microsoft.EntityFrameworkCore;
namespace foundation_system.Services
{
public class ReporteService : IReporteService
{
private readonly ApplicationDbContext _context;
public ReporteService(ApplicationDbContext context)
{
_context = context;
}
public async Task<ReporteArqueoViewModel> ObtenerArqueoCajaAsync(DateOnly fecha)
{
var vm = new ReporteArqueoViewModel
{
Fecha = fecha,
FechaGeneracion = DateTime.Now,
GeneradoPor = "Sistema" // En un escenario real, inyectar IHttpContextAccessor para obtener el usuario
};
var connection = _context.Database.GetDbConnection();
bool wasOpen = connection.State == ConnectionState.Open;
if (!wasOpen)
await connection.OpenAsync();
try
{
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM sp_reporte_arqueo_caja(@p_fecha)";
var param = command.CreateParameter();
param.ParameterName = "@p_fecha";
param.Value = fecha.ToDateTime(TimeOnly.MinValue);
param.DbType = DbType.Date;
command.Parameters.Add(param);
using (var reader = await command.ExecuteReaderAsync())
{
if (await reader.ReadAsync())
{
vm.SaldoInicial = reader.GetDecimal(reader.GetOrdinal("saldo_inicial"));
vm.TotalIngresos = reader.GetDecimal(reader.GetOrdinal("total_ingresos"));
vm.TotalEgresos = reader.GetDecimal(reader.GetOrdinal("total_egresos"));
vm.SaldoFinalTeorico = reader.GetDecimal(reader.GetOrdinal("saldo_final_teorico"));
vm.IngresosEfectivo = reader.GetDecimal(reader.GetOrdinal("ingresos_efectivo"));
vm.IngresosCheque = reader.GetDecimal(reader.GetOrdinal("ingresos_cheque"));
vm.IngresosTransferencia = reader.GetDecimal(reader.GetOrdinal("ingresos_transferencia"));
}
}
}
}
finally
{
if (!wasOpen)
await connection.CloseAsync();
}
return vm;
}
public async Task<ReporteMovimientosViewModel> ObtenerMovimientosAsync(DateOnly inicio, DateOnly fin)
{
var vm = new ReporteMovimientosViewModel
{
FechaInicio = inicio,
FechaFin = fin,
FechaGeneracion = DateTime.Now,
GeneradoPor = "Sistema"
};
var connection = _context.Database.GetDbConnection();
bool wasOpen = connection.State == ConnectionState.Open;
if (!wasOpen) await connection.OpenAsync();
try
{
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM sp_reporte_movimientos(@p_inicio, @p_fin)";
AddDateParams(command, inicio, fin);
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
var item = new MovimientoDetalleItem
{
Id = reader.GetInt64(reader.GetOrdinal("id")),
Fecha = DateOnly.FromDateTime(reader.GetDateTime(reader.GetOrdinal("fecha"))),
Tipo = reader.GetString(reader.GetOrdinal("tipo_movimiento")),
Concepto = reader.GetString(reader.GetOrdinal("concepto")),
Categoria = reader.GetString(reader.GetOrdinal("categoria")),
Monto = reader.GetDecimal(reader.GetOrdinal("monto")),
Usuario = reader.GetString(reader.GetOrdinal("usuario"))
};
vm.Movimientos.Add(item);
if (item.Tipo == "INGRESO" || item.Tipo == "APERTURA" || item.Tipo == "REPOSICION" || item.Tipo == "AUMENTO_FONDO")
vm.TotalIngresos += item.Monto;
else
vm.TotalEgresos += item.Monto;
}
}
}
}
finally
{
if (!wasOpen) await connection.CloseAsync();
}
return vm;
}
public async Task<ReporteHistoricoSaldosViewModel> ObtenerHistoricoSaldosAsync(DateOnly inicio, DateOnly fin)
{
var vm = new ReporteHistoricoSaldosViewModel
{
FechaInicio = inicio,
FechaFin = fin,
FechaGeneracion = DateTime.Now,
GeneradoPor = "Sistema"
};
var connection = _context.Database.GetDbConnection();
bool wasOpen = connection.State == ConnectionState.Open;
if (!wasOpen) await connection.OpenAsync();
try
{
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM sp_reporte_historico_saldos(@p_inicio, @p_fin)";
AddDateParams(command, inicio, fin);
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
vm.Saldos.Add(new SaldoDiarioItem
{
Fecha = DateOnly.FromDateTime(reader.GetDateTime(reader.GetOrdinal("fecha"))),
SaldoInicial = reader.GetDecimal(reader.GetOrdinal("saldo_inicial")),
Ingresos = reader.GetDecimal(reader.GetOrdinal("ingresos")),
Egresos = reader.GetDecimal(reader.GetOrdinal("egresos")),
SaldoFinal = reader.GetDecimal(reader.GetOrdinal("saldo_final"))
});
}
}
}
}
finally
{
if (!wasOpen) await connection.CloseAsync();
}
return vm;
}
public async Task<ReporteGastosCategoriaViewModel> ObtenerGastosCategoriaAsync(DateOnly inicio, DateOnly fin)
{
var vm = new ReporteGastosCategoriaViewModel
{
FechaInicio = inicio,
FechaFin = fin,
FechaGeneracion = DateTime.Now,
GeneradoPor = "Sistema"
};
var connection = _context.Database.GetDbConnection();
bool wasOpen = connection.State == ConnectionState.Open;
if (!wasOpen) await connection.OpenAsync();
try
{
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM sp_reporte_gastos_categoria(@p_inicio, @p_fin)";
AddDateParams(command, inicio, fin);
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
var item = new GastoCategoriaItem
{
Categoria = reader.GetString(reader.GetOrdinal("categoria")),
CantidadTransacciones = reader.GetInt32(reader.GetOrdinal("cantidad")),
MontoTotal = reader.GetDecimal(reader.GetOrdinal("monto_total"))
};
vm.Categorias.Add(item);
vm.TotalGastos += item.MontoTotal;
}
}
}
// Calculate percentages
if (vm.TotalGastos > 0)
{
foreach (var cat in vm.Categorias)
{
cat.Porcentaje = (cat.MontoTotal / vm.TotalGastos) * 100;
}
}
}
finally
{
if (!wasOpen) await connection.CloseAsync();
}
return vm;
}
public async Task<ReporteReposicionesViewModel> ObtenerReposicionesAsync(DateOnly inicio, DateOnly fin)
{
var vm = new ReporteReposicionesViewModel
{
FechaInicio = inicio,
FechaFin = fin,
FechaGeneracion = DateTime.Now,
GeneradoPor = "Sistema"
};
var connection = _context.Database.GetDbConnection();
bool wasOpen = connection.State == ConnectionState.Open;
if (!wasOpen) await connection.OpenAsync();
try
{
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM sp_reporte_reposiciones(@p_inicio, @p_fin)";
AddDateParams(command, inicio, fin);
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
var item = new ReposicionItem
{
Fecha = DateOnly.FromDateTime(reader.GetDateTime(reader.GetOrdinal("fecha"))),
Monto = reader.GetDecimal(reader.GetOrdinal("monto")),
Usuario = reader.GetString(reader.GetOrdinal("usuario")),
Descripcion = reader.GetString(reader.GetOrdinal("descripcion"))
};
vm.Reposiciones.Add(item);
vm.TotalRepuesto += item.Monto;
}
}
}
}
finally
{
if (!wasOpen) await connection.CloseAsync();
}
return vm;
}
private void AddDateParams(System.Data.Common.DbCommand command, DateOnly inicio, DateOnly fin)
{
var p1 = command.CreateParameter();
p1.ParameterName = "@p_inicio";
p1.Value = inicio.ToDateTime(TimeOnly.MinValue);
p1.DbType = DbType.Date;
command.Parameters.Add(p1);
var p2 = command.CreateParameter();
p2.ParameterName = "@p_fin";
p2.Value = fin.ToDateTime(TimeOnly.MaxValue);
p2.DbType = DbType.Date;
command.Parameters.Add(p2);
}
}
}

View File

@@ -0,0 +1,238 @@
@model foundation_system.Models.ViewModels.Reportes.ReporteArqueoViewModel
@{
ViewData["Title"] = "Arqueo de Caja";
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800">
<i class="fas fa-cash-register me-2 text-primary"></i>Arqueo de Caja
</h1>
<p class="text-muted small mb-0">Reporte diario de movimientos de efectivo</p>
</div>
<div class="d-flex gap-2">
<button onclick="window.print()" class="btn btn-outline-secondary">
<i class="fas fa-print me-2"></i>Imprimir
</button>
<!-- TODO: Implement Excel Export -->
<button class="btn btn-success" disabled title="Próximamente">
<i class="fas fa-file-excel me-2"></i>Exportar Excel
</button>
</div>
</div>
<!-- Filtro de Fecha -->
<div class="card shadow-sm mb-4 d-print-none">
<div class="card-body py-3">
<form method="get" class="row g-3 align-items-end">
<div class="col-auto">
<label for="fecha" class="form-label fw-bold small">Fecha de Arqueo</label>
<input type="date" class="form-control" id="fecha" name="fecha"
value="@Model.Fecha.ToString("yyyy-MM-dd")" onchange="this.form.submit()">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sync-alt me-2"></i>Actualizar
</button>
</div>
</form>
</div>
</div>
<!-- Resumen Principal -->
<div class="row g-4 mb-4">
<!-- Saldo Inicial -->
<div class="col-md-3">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Saldo Inicial</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.SaldoInicial.ToString("C2")</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar-minus fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Ingresos -->
<div class="col-md-3">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Total Ingresos</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.TotalIngresos.ToString("C2")</div>
</div>
<div class="col-auto">
<i class="fas fa-arrow-up fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Egresos -->
<div class="col-md-3">
<div class="card border-left-danger shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Total Egresos</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.TotalEgresos.ToString("C2")</div>
</div>
<div class="col-auto">
<i class="fas fa-arrow-down fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Saldo Final -->
<div class="col-md-3">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Saldo Final Teórico</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.SaldoFinalTeorico.ToString("C2")</div>
</div>
<div class="col-auto">
<i class="fas fa-wallet fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Detalle Impreso -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Detalle del Arqueo</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" width="100%" cellspacing="0">
<thead class="table-light">
<tr>
<th>Concepto</th>
<th class="text-end">Monto</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>(+) Saldo Inicial</strong></td>
<td class="text-end">@Model.SaldoInicial.ToString("C2")</td>
</tr>
<tr>
<td colspan="2" class="bg-light small fw-bold text-uppercase">Ingresos</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;&nbsp;Efectivo / Reposiciones</td>
<td class="text-end">@Model.IngresosEfectivo.ToString("C2")</td>
</tr>
@if (Model.IngresosCheque > 0) {
<tr>
<td>&nbsp;&nbsp;&nbsp;&nbsp;Cheques</td>
<td class="text-end">@Model.IngresosCheque.ToString("C2")</td>
</tr>
}
@if (Model.IngresosTransferencia > 0) {
<tr>
<td>&nbsp;&nbsp;&nbsp;&nbsp;Transferencias</td>
<td class="text-end">@Model.IngresosTransferencia.ToString("C2")</td>
</tr>
}
<tr class="table-success">
<td><strong>(=) Total Ingresos</strong></td>
<td class="text-end fw-bold">@Model.TotalIngresos.ToString("C2")</td>
</tr>
<tr>
<td colspan="2" class="bg-light small fw-bold text-uppercase">Egresos</td>
</tr>
<tr>
<td>&nbsp;&nbsp;&nbsp;&nbsp;Gastos Operativos</td>
<td class="text-end">@Model.TotalEgresos.ToString("C2")</td>
</tr>
<tr class="table-danger">
<td><strong>(=) Total Egresos</strong></td>
<td class="text-end fw-bold">@Model.TotalEgresos.ToString("C2")</td>
</tr>
<tr class="table-primary border-top-2">
<td class="h5"><strong>Saldo Final Teórico</strong></td>
<td class="text-end h5 fw-bold">@Model.SaldoFinalTeorico.ToString("C2")</td>
</tr>
</tbody>
</table>
</div>
<!-- Sección de Firmas (Solo visible en impresión) -->
<div class="row mt-5 pt-5 d-none d-print-flex">
<div class="col-6 text-center">
<div class="border-top border-dark w-75 mx-auto pt-2">
<p class="mb-0 fw-bold">Entregado Por</p>
<small>Cajero / Responsable</small>
</div>
</div>
<div class="col-6 text-center">
<div class="border-top border-dark w-75 mx-auto pt-2">
<p class="mb-0 fw-bold">Revisado Por</p>
<small>Administrador / Auditor</small>
</div>
</div>
</div>
<div class="text-center mt-4 text-muted small">
<p>Generado por: @Model.GeneradoPor | Fecha: @Model.FechaGeneracion.ToString("g")</p>
</div>
</div>
</div>
</div>
@section Styles {
<style>
.border-left-primary { border-left: .25rem solid #4e73df!important; }
.border-left-success { border-left: .25rem solid #1cc88a!important; }
.border-left-info { border-left: .25rem solid #36b9cc!important; }
.border-left-danger { border-left: .25rem solid #e74a3b!important; }
@@media print {
.d-print-none, .no-print { display: none !important; }
.d-print-flex { display: flex !important; }
/* Hide Layout Elements */
.sidebar, .top-header, .footer { display: none !important; }
.main-content { margin-left: 0 !important; padding: 0 !important; }
.page-container { padding: 0 !important; }
/* Reset Container */
.container-fluid { width: 100% !important; padding: 0 !important; }
body { background: white !important; }
/* Card Styling for Print */
.card { border: 1px solid #ddd !important; box-shadow: none !important; }
.card-body { padding: 1rem !important; }
.btn { display: none !important; }
/* Force Row Layout for Summary Cards */
.row {
display: flex !important;
flex-wrap: nowrap !important;
gap: 10px;
}
.col-md-3 {
flex: 0 0 25% !important;
max-width: 25% !important;
}
}
</style>
}

View File

@@ -0,0 +1,136 @@
@model foundation_system.Models.ViewModels.Reportes.ReporteGastosCategoriaViewModel
@{
ViewData["Title"] = "Gastos por Categoría";
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800">
<i class="fas fa-chart-pie me-2 text-primary"></i>Gastos por Categoría
</h1>
<p class="text-muted small mb-0">Distribución de gastos por tipo</p>
</div>
<div class="d-flex gap-2">
<button onclick="window.print()" class="btn btn-outline-secondary">
<i class="fas fa-print me-2"></i>Imprimir
</button>
</div>
</div>
<!-- Filtro de Fecha -->
<div class="card shadow-sm mb-4 d-print-none">
<div class="card-body py-3">
<form method="get" class="row g-3 align-items-end">
<div class="col-auto">
<label for="inicio" class="form-label fw-bold small">Desde</label>
<input type="date" class="form-control" id="inicio" name="inicio"
value="@Model.FechaInicio.ToString("yyyy-MM-dd")">
</div>
<div class="col-auto">
<label for="fin" class="form-label fw-bold small">Hasta</label>
<input type="date" class="form-control" id="fin" name="fin"
value="@Model.FechaFin.ToString("yyyy-MM-dd")">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sync-alt me-2"></i>Generar
</button>
</div>
</form>
</div>
</div>
<div class="row">
<!-- Tabla Detalle -->
<div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Desglose por Categoría</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" width="100%" cellspacing="0">
<thead class="table-light">
<tr>
<th>Categoría</th>
<th class="text-center">Transacciones</th>
<th class="text-end">Monto Total</th>
<th class="text-end">%</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Categorias)
{
<tr>
<td>
<span class="fw-bold text-dark">@item.Categoria</span>
<div class="progress mt-1" style="height: 4px;">
<div class="progress-bar bg-info" role="progressbar"
style="width: @item.Porcentaje.ToString("0")%"
aria-valuenow="@item.Porcentaje" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</td>
<td class="text-center">@item.CantidadTransacciones</td>
<td class="text-end">@item.MontoTotal.ToString("C2")</td>
<td class="text-end small">@item.Porcentaje.ToString("F1")%</td>
</tr>
}
<tr class="table-secondary fw-bold">
<td>TOTAL</td>
<td class="text-center">@Model.Categorias.Sum(c => c.CantidadTransacciones)</td>
<td class="text-end">@Model.TotalGastos.ToString("C2")</td>
<td class="text-end">100.0%</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Resumen Visual (Solo visible en pantalla) -->
<div class="col-lg-4 d-print-none">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Resumen</h6>
</div>
<div class="card-body">
<div class="text-center mb-4">
<h1 class="display-4 fw-bold text-gray-800">@Model.TotalGastos.ToString("C2")</h1>
<p class="text-muted">Gasto Total del Período</p>
</div>
<hr>
<div class="small text-muted">
<i class="fas fa-info-circle me-1"></i>
Las categorías con mayor consumo representan el
<strong>@(Model.Categorias.OrderByDescending(c => c.MontoTotal).FirstOrDefault()?.Porcentaje.ToString("F0") ?? "0")%</strong>
del total.
</div>
</div>
</div>
</div>
</div>
<div class="text-center mt-4 text-muted small">
<p>Generado por: @Model.GeneradoPor | Fecha: @Model.FechaGeneracion.ToString("g")</p>
</div>
</div>
@section Styles {
<style>
@@media print {
.d-print-none, .no-print { display: none !important; }
.sidebar, .top-header, .footer { display: none !important; }
.main-content { margin-left: 0 !important; padding: 0 !important; }
.page-container { padding: 0 !important; }
.container-fluid { width: 100% !important; padding: 0 !important; }
body { background: white !important; }
.card { border: 1px solid #ddd !important; box-shadow: none !important; }
.card-body { padding: 1rem !important; }
.btn { display: none !important; }
.col-lg-8 { width: 100% !important; flex: 0 0 100% !important; max-width: 100% !important; }
}
</style>
}

View File

@@ -0,0 +1,103 @@
@model foundation_system.Models.ViewModels.Reportes.ReporteHistoricoSaldosViewModel
@{
ViewData["Title"] = "Histórico de Saldos";
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800">
<i class="fas fa-chart-line me-2 text-primary"></i>Histórico de Saldos
</h1>
<p class="text-muted small mb-0">Evolución diaria del saldo de caja</p>
</div>
<div class="d-flex gap-2">
<button onclick="window.print()" class="btn btn-outline-secondary">
<i class="fas fa-print me-2"></i>Imprimir
</button>
</div>
</div>
<!-- Filtro de Fecha -->
<div class="card shadow-sm mb-4 d-print-none">
<div class="card-body py-3">
<form method="get" class="row g-3 align-items-end">
<div class="col-auto">
<label for="inicio" class="form-label fw-bold small">Desde</label>
<input type="date" class="form-control" id="inicio" name="inicio"
value="@Model.FechaInicio.ToString("yyyy-MM-dd")">
</div>
<div class="col-auto">
<label for="fin" class="form-label fw-bold small">Hasta</label>
<input type="date" class="form-control" id="fin" name="fin"
value="@Model.FechaFin.ToString("yyyy-MM-dd")">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sync-alt me-2"></i>Generar
</button>
</div>
</form>
</div>
</div>
<!-- Tabla Detalle -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Saldos Diarios</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" width="100%" cellspacing="0">
<thead class="table-light">
<tr>
<th>Fecha</th>
<th class="text-end">Saldo Inicial</th>
<th class="text-end text-success">Ingresos</th>
<th class="text-end text-danger">Egresos</th>
<th class="text-end fw-bold">Saldo Final</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Saldos)
{
<tr>
<td>@item.Fecha.ToString("dd/MM/yyyy")</td>
<td class="text-end">@item.SaldoInicial.ToString("N2")</td>
<td class="text-end text-success">@item.Ingresos.ToString("N2")</td>
<td class="text-end text-danger">@item.Egresos.ToString("N2")</td>
<td class="text-end fw-bold">@item.SaldoFinal.ToString("C2")</td>
</tr>
}
@if (!Model.Saldos.Any())
{
<tr>
<td colspan="5" class="text-center text-muted py-4">No hay datos en este período.</td>
</tr>
}
</tbody>
</table>
</div>
<div class="text-center mt-4 text-muted small">
<p>Generado por: @Model.GeneradoPor | Fecha: @Model.FechaGeneracion.ToString("g")</p>
</div>
</div>
</div>
</div>
@section Styles {
<style>
@@media print {
.d-print-none, .no-print { display: none !important; }
.sidebar, .top-header, .footer { display: none !important; }
.main-content { margin-left: 0 !important; padding: 0 !important; }
.page-container { padding: 0 !important; }
.container-fluid { width: 100% !important; padding: 0 !important; }
body { background: white !important; }
.card { border: 1px solid #ddd !important; box-shadow: none !important; }
.card-body { padding: 1rem !important; }
.btn { display: none !important; }
}
</style>
}

View File

@@ -0,0 +1,158 @@
@model foundation_system.Models.ViewModels.Reportes.ReporteMovimientosViewModel
@{
ViewData["Title"] = "Movimientos de Caja Chica";
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800">
<i class="fas fa-list-alt me-2 text-primary"></i>Movimientos de Caja Chica
</h1>
<p class="text-muted small mb-0">Detalle de ingresos y egresos por período</p>
</div>
<div class="d-flex gap-2">
<button onclick="window.print()" class="btn btn-outline-secondary">
<i class="fas fa-print me-2"></i>Imprimir
</button>
</div>
</div>
<!-- Filtro de Fecha -->
<div class="card shadow-sm mb-4 d-print-none">
<div class="card-body py-3">
<form method="get" class="row g-3 align-items-end">
<div class="col-auto">
<label for="inicio" class="form-label fw-bold small">Desde</label>
<input type="date" class="form-control" id="inicio" name="inicio"
value="@Model.FechaInicio.ToString("yyyy-MM-dd")">
</div>
<div class="col-auto">
<label for="fin" class="form-label fw-bold small">Hasta</label>
<input type="date" class="form-control" id="fin" name="fin"
value="@Model.FechaFin.ToString("yyyy-MM-dd")">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sync-alt me-2"></i>Generar
</button>
</div>
</form>
</div>
</div>
<!-- Resumen -->
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Total Ingresos</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.TotalIngresos.ToString("C2")</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-left-danger shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Total Egresos</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.TotalEgresos.ToString("C2")</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-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">Flujo Neto</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.SaldoPeriodo.ToString("C2")</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabla Detalle -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Detalle de Transacciones</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-striped" width="100%" cellspacing="0">
<thead class="table-light">
<tr>
<th>Fecha</th>
<th>Tipo</th>
<th>Concepto</th>
<th>Categoría</th>
<th>Usuario</th>
<th class="text-end">Monto</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movimientos)
{
<tr>
<td>@item.Fecha.ToString("dd/MM/yyyy")</td>
<td>
<span class="badge @(item.Tipo == "INGRESO" || item.Tipo == "REPOSICION" ? "bg-success" : "bg-danger")">
@item.Tipo
</span>
</td>
<td>@item.Concepto</td>
<td>@item.Categoria</td>
<td class="small">@item.Usuario</td>
<td class="text-end fw-bold @(item.Tipo == "INGRESO" || item.Tipo == "REPOSICION" ? "text-success" : "text-danger")">
@item.Monto.ToString("C2")
</td>
</tr>
}
@if (!Model.Movimientos.Any())
{
<tr>
<td colspan="6" class="text-center text-muted py-4">No hay movimientos en este período.</td>
</tr>
}
</tbody>
</table>
</div>
<div class="text-center mt-4 text-muted small">
<p>Generado por: @Model.GeneradoPor | Fecha: @Model.FechaGeneracion.ToString("g")</p>
</div>
</div>
</div>
</div>
@section Styles {
<style>
.border-left-success { border-left: .25rem solid #1cc88a!important; }
.border-left-info { border-left: .25rem solid #36b9cc!important; }
.border-left-danger { border-left: .25rem solid #e74a3b!important; }
@@media print {
.d-print-none, .no-print { display: none !important; }
.sidebar, .top-header, .footer { display: none !important; }
.main-content { margin-left: 0 !important; padding: 0 !important; }
.page-container { padding: 0 !important; }
.container-fluid { width: 100% !important; padding: 0 !important; }
body { background: white !important; }
.card { border: 1px solid #ddd !important; box-shadow: none !important; }
.card-body { padding: 1rem !important; }
.btn { display: none !important; }
.row { display: flex !important; flex-wrap: nowrap !important; gap: 10px; }
.col-md-4 { flex: 0 0 33% !important; max-width: 33% !important; }
}
</style>
}

View File

@@ -0,0 +1,141 @@
@model foundation_system.Models.ViewModels.Reportes.ReporteReposicionesViewModel
@{
ViewData["Title"] = "Reporte de Reposiciones";
}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0 text-gray-800">
<i class="fas fa-money-bill-wave me-2 text-primary"></i>Reporte de Reposiciones
</h1>
<p class="text-muted small mb-0">Historial de reintegros de fondos a Caja Chica</p>
</div>
<div class="d-flex gap-2">
<button onclick="window.print()" class="btn btn-outline-secondary">
<i class="fas fa-print me-2"></i>Imprimir
</button>
</div>
</div>
<!-- Filtro de Fecha -->
<div class="card shadow-sm mb-4 d-print-none">
<div class="card-body py-3">
<form method="get" class="row g-3 align-items-end">
<div class="col-auto">
<label for="inicio" class="form-label fw-bold small">Desde</label>
<input type="date" class="form-control" id="inicio" name="inicio"
value="@Model.FechaInicio.ToString("yyyy-MM-dd")">
</div>
<div class="col-auto">
<label for="fin" class="form-label fw-bold small">Hasta</label>
<input type="date" class="form-control" id="fin" name="fin"
value="@Model.FechaFin.ToString("yyyy-MM-dd")">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary">
<i class="fas fa-sync-alt me-2"></i>Generar
</button>
</div>
</form>
</div>
</div>
<!-- Resumen -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Total Repuesto</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.TotalRepuesto.ToString("C2")</div>
</div>
<div class="col-auto">
<i class="fas fa-hand-holding-usd fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-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">Cantidad Reposiciones</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">@Model.Reposiciones.Count</div>
</div>
<div class="col-auto">
<i class="fas fa-hashtag fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabla Detalle -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Detalle de Reposiciones</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover" width="100%" cellspacing="0">
<thead class="table-light">
<tr>
<th>Fecha</th>
<th>Responsable</th>
<th>Descripción / Justificación</th>
<th class="text-end">Monto</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Reposiciones)
{
<tr>
<td>@item.Fecha.ToString("dd/MM/yyyy")</td>
<td>@item.Usuario</td>
<td>@item.Descripcion</td>
<td class="text-end fw-bold text-success">@item.Monto.ToString("C2")</td>
</tr>
}
@if (!Model.Reposiciones.Any())
{
<tr>
<td colspan="4" class="text-center text-muted py-4">No se encontraron reposiciones en este período.</td>
</tr>
}
</tbody>
</table>
</div>
<div class="text-center mt-4 text-muted small">
<p>Generado por: @Model.GeneradoPor | Fecha: @Model.FechaGeneracion.ToString("g")</p>
</div>
</div>
</div>
</div>
@section Styles {
<style>
.border-left-success { border-left: .25rem solid #1cc88a!important; }
.border-left-primary { border-left: .25rem solid #4e73df!important; }
@@media print {
.d-print-none, .no-print { display: none !important; }
.sidebar, .top-header, .footer { display: none !important; }
.main-content { margin-left: 0 !important; padding: 0 !important; }
.page-container { padding: 0 !important; }
.container-fluid { width: 100% !important; padding: 0 !important; }
body { background: white !important; }
.card { border: 1px solid #ddd !important; box-shadow: none !important; }
.card-body { padding: 1rem !important; }
.btn { display: none !important; }
.row { display: flex !important; flex-wrap: nowrap !important; gap: 10px; }
.col-md-4 { flex: 0 0 50% !important; max-width: 50% !important; }
}
</style>
}

View File

@@ -5,19 +5,18 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>@ViewData["Title"] - MIES</title> <title>@ViewData["Title"] - MIES</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="~/css/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="~/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css"> <link rel="stylesheet" href="~/css/toastr.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css"> <link rel="stylesheet" href="~/css/sweetalert2.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="stylesheet" href="~/css/inter.css" asp-append-version="true" />
<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">
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/foundation_system.styles.css" asp-append-version="true"/> <!--<link rel="stylesheet" href="~/foundation_system.styles.css" asp-append-version="true"/>-->
@RenderSection("Styles", required: false) @RenderSection("Styles", required: false)
</head> </head>
<body> <body>
<div class="app-wrapper"> <div class="app-wrapper">
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- Sidebar --> <!-- Sidebar -->
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
@@ -36,7 +35,10 @@
<!-- Main Content --> <!-- Main Content -->
<main class="main-content"> <main class="main-content">
<header class="top-header"> <header class="top-header">
<div class="header-left"> <div class="header-left d-flex align-items-center">
<button id="sidebarToggle" class="btn btn-link text-dark p-0 me-3">
<i class="bi bi-list fs-4"></i>
</button>
<h5 class="mb-0 fw-semibold">@ViewData["Title"]</h5> <h5 class="mb-0 fw-semibold">@ViewData["Title"]</h5>
</div> </div>
<div class="header-right"> <div class="header-right">
@@ -63,8 +65,8 @@
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.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="~/js/toastr.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="~/js/sweetalert.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script> <script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</body> </body>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -e
BASE_DIR="wwwroot/assets"
CSS_DIR="$BASE_DIR/css"
FONTS_DIR="$BASE_DIR/fonts"
echo "📁 Creando carpetas..."
mkdir -p "$CSS_DIR" "$FONTS_DIR"
echo "⬇️ Descargando CSS..."
# Bootstrap Icons
curl -L -o "$CSS_DIR/bootstrap-icons.min.css" \
https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css
# Font Awesome
curl -L -o "$CSS_DIR/fontawesome.min.css" \
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css
# Toastr
curl -L -o "$CSS_DIR/toastr.min.css" \
https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css
# SweetAlert2
curl -L -o "$CSS_DIR/sweetalert2.min.css" \
https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css
echo "⬇️ Descargando Google Font (Inter)..."
# Descargar CSS de Google Fonts
curl -L -o "$CSS_DIR/inter.css" \
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
echo "⚠️ Nota:"
echo "Google Fonts usa URLs dinámicas."
echo "Abre inter.css, descarga los .woff2 referenciados"
echo "y ajusta las rutas a ../fonts/"
echo "✅ Descargas completadas"

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,28 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf) format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fMZg.ttf) format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf) format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(../fonts/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf) format('truetype');
}

View File

@@ -217,11 +217,45 @@ h6 {
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebar { .sidebar {
transform: translateX(-100%); transform: translateX(-100%);
box-shadow: 4px 0 24px 0 rgba(0, 0, 0, 0.1);
} }
.main-content {
margin-left: 0;
}
.sidebar.open { .sidebar.open {
transform: translateX(0); transform: translateX(0);
} }
.main-content {
margin-left: 0;
}
/* Overlay for mobile */
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.sidebar-overlay.show {
opacity: 1;
visibility: visible;
}
}
/* Desktop toggle behavior (optional, if we want to collapse on desktop too) */
@media (min-width: 769px) {
.sidebar.collapsed {
width: 0;
overflow: hidden;
}
.main-content.expanded {
margin-left: 0;
}
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,39 @@
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification // Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets. // for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code. $(document).ready(function () {
const sidebar = $('.sidebar');
const sidebarOverlay = $('#sidebarOverlay');
const sidebarToggle = $('#sidebarToggle');
const mainContent = $('.main-content');
// Toggle Sidebar
sidebarToggle.on('click', function (e) {
e.stopPropagation();
sidebar.toggleClass('open');
sidebarOverlay.toggleClass('show');
// Desktop collapse behavior
if (window.innerWidth > 768) {
sidebar.toggleClass('collapsed');
mainContent.toggleClass('expanded');
}
});
// Close sidebar when clicking overlay (mobile)
sidebarOverlay.on('click', function () {
sidebar.removeClass('open');
sidebarOverlay.removeClass('show');
});
// Close sidebar when clicking outside (mobile) - optional extra safety
$(document).on('click', function (e) {
if (window.innerWidth <= 768) {
if (!sidebar.is(e.target) && sidebar.has(e.target).length === 0 &&
!sidebarToggle.is(e.target) && sidebarToggle.has(e.target).length === 0) {
sidebar.removeClass('open');
sidebarOverlay.removeClass('show');
}
}
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.