Autosave: 20260203-072219

This commit is contained in:
Flatlogic Bot 2026-02-03 07:22:19 +00:00
parent 9a3241c3ec
commit e2dbd8b86f
3 changed files with 317 additions and 66 deletions

View File

@ -9,6 +9,13 @@ $start_date = "$year-$month-01";
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $month, $year);
$end_date = "$year-$month-$days_in_month";
$columns = [
'bcp_yape', 'b_nacion', 'interbank', 'bbva', 'otros_ingresos',
'tu1', 'tu2', 'tu3', 'fl1', 'fl2', 'fl3',
'rc_envio', 'rc_contraent'
];
// Fetch data from DB
$flujo_data = [];
try {
$pdo = db();
@ -22,22 +29,51 @@ try {
// Handle error
}
$columns = [
'bcp_yape', 'b_nacion', 'interbank', 'bbva', 'otros_ingresos',
'tu1', 'tu2', 'tu3', 'fl1', 'fl2', 'fl3',
'rc_envio', 'rc_contraent'
];
// Prepare data for the entire month
$all_days_data = [];
for ($day = 1; $day <= $days_in_month; $day++) {
$date = date('Y-m-d', strtotime("$year-$month-$day"));
$all_days_data[$date] = isset($flujo_data[$date]) ? $flujo_data[$date] : array_fill_keys($columns, '0.00');
}
// Calculate totals
$totals = array_fill_keys($columns, 0);
$totals['total_ingresos'] = 0;
$totals['total_inversion'] = 0;
$totals['recaudo_final'] = 0;
foreach ($all_days_data as $day_data) {
$ingresos_dia = (float)$day_data['bcp_yape'] + (float)$day_data['b_nacion'] + (float)$day_data['interbank'] + (float)$day_data['bbva'] + (float)$day_data['otros_ingresos'] + (float)$day_data['rc_envio'] + (float)$day_data['rc_contraent'];
$inversion_dia = (float)$day_data['tu1'] + (float)$day_data['tu2'] + (float)$day_data['tu3'] + (float)$day_data['fl1'] + (float)$day_data['fl2'] + (float)$day_data['fl3'];
foreach ($columns as $col) {
$totals[$col] += (float)$day_data[$col];
}
$totals['total_ingresos'] += $ingresos_dia;
$totals['total_inversion'] += $inversion_dia;
$totals['recaudo_final'] += ($ingresos_dia - $inversion_dia);
}
?>
<style>
.table thead th {
background-color: #e9ecef; /* Un gris claro y profesional */
background-color: #e9ecef;
color: #495057;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
}
.table tfoot th {
position: sticky;
bottom: 0;
background-color: #e9ecef;
z-index: 10;
font-weight: 700;
}
.table td[contenteditable="true"]:focus {
background-color: #fff3cd; /* Un amarillo suave para resaltar la celda activa */
background-color: #fff3cd;
}
</style>
@ -67,7 +103,7 @@ $columns = [
<button type="submit" class="btn btn-primary">Filtrar</button>
</form>
<div class="table-responsive">
<div class="table-responsive" style="max-height: 70vh; overflow-y: auto;">
<table class="table table-bordered table-striped table-hover" style="width: 100%;">
<thead>
<tr>
@ -95,21 +131,29 @@ $columns = [
</tr>
</thead>
<tbody>
<?php for ($day = 1; $day <= $days_in_month; $day++):
$date = date('Y-m-d', strtotime("$year-$month-$day"));
$day_data = isset($flujo_data[$date]) ? $flujo_data[$date] : array_fill_keys($columns, '0.00');
?>
<?php foreach ($all_days_data as $date => $day_data): ?>
<tr data-date="<?php echo $date; ?>">
<td><?php echo $date; ?></td>
<?php foreach ($columns as $col): ?>
<td contenteditable="true" data-column="<?php echo $col; ?>"><?php echo htmlspecialchars($day_data[$col]); ?></td>
<td contenteditable="true" data-column="<?php echo $col; ?>"><?php echo htmlspecialchars(number_format((float)$day_data[$col], 2, '.', '')); ?></td>
<?php endforeach; ?>
<td class="total-ingresos">0.00</td>
<td class="total-inversion">0.00</td>
<td class="recaudo-final">0.00</td>
</tr>
<?php endfor; ?>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th>TOTAL</th>
<?php foreach ($columns as $col): ?>
<th data-total-column="<?php echo $col; ?>"><?php echo number_format($totals[$col], 2); ?></th>
<?php endforeach; ?>
<th data-total-column="total-ingresos"><?php echo number_format($totals['total_ingresos'], 2); ?></th>
<th data-total-column="total-inversion"><?php echo number_format($totals['total_inversion'], 2); ?></th>
<th data-total-column="recaudo-final"><?php echo number_format($totals['recaudo_final'], 2); ?></th>
</tr>
</tfoot>
</table>
</div>
</div>
@ -118,17 +162,14 @@ $columns = [
document.addEventListener('DOMContentLoaded', function() {
const table = document.querySelector('.table');
// Function to update totals for a specific row
function updateTotals(row) {
const getVal = (selector) => {
const cell = row.querySelector(`[data-column="${selector}"]`);
return parseFloat(cell.textContent) || 0;
return parseFloat(cell.textContent.replace(/,/g, '')) || 0;
};
const ingresos = getVal('bcp_yape') + getVal('b_nacion') + getVal('interbank') + getVal('bbva') + getVal('otros_ingresos') + getVal('rc_envio') + getVal('rc_contraent');
const totalInversion = getVal('tu1') + getVal('tu2') + getVal('tu3') + getVal('fl1') + getVal('fl2') + getVal('fl3');
const recaudoFinal = ingresos - totalInversion;
row.querySelector('.total-ingresos').textContent = ingresos.toFixed(2);
@ -136,10 +177,34 @@ document.addEventListener('DOMContentLoaded', function() {
row.querySelector('.recaudo-final').textContent = recaudoFinal.toFixed(2);
}
// Initial calculation for all rows
table.querySelectorAll('tbody tr').forEach(updateTotals);
function updateGrandTotals() {
const grandTotals = {
<?php foreach ($columns as $col) echo "'$col': 0,"; ?>
'total-ingresos': 0,
'total-inversion': 0,
'recaudo-final': 0
};
table.querySelectorAll('tbody tr').forEach(row => {
<?php foreach ($columns as $col): ?>
grandTotals['<?php echo $col; ?>'] += parseFloat(row.querySelector(`[data-column="<?php echo $col; ?>"]`).textContent.replace(/,/g, '')) || 0;
<?php endforeach; ?>
grandTotals['total-ingresos'] += parseFloat(row.querySelector('.total-ingresos').textContent.replace(/,/g, '')) || 0;
grandTotals['total-inversion'] += parseFloat(row.querySelector('.total-inversion').textContent.replace(/,/g, '')) || 0;
grandTotals['recaudo-final'] += parseFloat(row.querySelector('.recaudo-final').textContent.replace(/,/g, '')) || 0;
});
for (const key in grandTotals) {
const th = table.querySelector(`tfoot [data-total-column="${key}"]`);
if (th) {
th.textContent = grandTotals[key].toFixed(2);
}
}
}
table.querySelectorAll('tbody tr').forEach(updateTotals);
updateGrandTotals();
// Event listener for cell focus to select all content
table.addEventListener('focus', function(e) {
if (e.target.hasAttribute('contenteditable')) {
const range = document.createRange();
@ -148,50 +213,43 @@ document.addEventListener('DOMContentLoaded', function() {
sel.removeAllRanges();
sel.addRange(range);
}
}, true); // Use capturing for focus
}, true);
// Event listener for cell edits
table.addEventListener('blur', function(e) {
if (e.target.hasAttribute('contenteditable')) {
const cell = e.target;
const row = cell.closest('tr');
const date = row.dataset.date;
const column = cell.dataset.column;
let value = parseFloat(cell.textContent);
let value = parseFloat(cell.textContent.replace(/,/g, ''));
if (isNaN(value)) {
value = 0;
cell.textContent = '0.00';
}
cell.textContent = value.toFixed(2);
// Save data to server
fetch('save_flujo_caja.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fecha: date, columna: column, valor: value })
})
.then(response => response.json())
.then(data => {
if (!data.success) {
if (data.success) {
updateTotals(row);
updateGrandTotals(); // Update grand totals after successful save
} else {
console.error('Error saving data:', data.message);
// Optional: Add user feedback for save failure
}
})
.catch(error => console.error('Fetch error:', error));
// Update totals for the edited row
updateTotals(row);
}
}, true); // Use capturing to ensure blur event is handled properly
}, true);
// Event listener for Enter key to save and not expand
table.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.hasAttribute('contenteditable')) {
e.preventDefault(); // Prevent new line
e.target.blur(); // Trigger blur to save
e.preventDefault();
e.target.blur();
}
});
});
@ -199,4 +257,4 @@ document.addEventListener('DOMContentLoaded', function() {
<?php
require_once 'layout_footer.php';
?>
?>

View File

@ -76,7 +76,7 @@ $navItems = [
],
'inventario_group' => [
'icon' => 'fa-warehouse',
'text' => 'Inventario',
'text' => 'Inventario General',
'roles' => ['Administrador', 'admin', 'Control Logistico'],
'submenu' => [
'panel_inventario' => [

View File

@ -9,51 +9,160 @@ require_once 'db/config.php';
try {
$pdo = db();
$stmt = $pdo->query("SELECT * FROM products ORDER BY order_position ASC");
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 1. Datos para las tarjetas de resumen
$total_productos = $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
$total_sedes = $pdo->query("SELECT COUNT(*) FROM sedes")->fetchColumn();
$total_stock = $pdo->query("SELECT SUM(quantity) FROM stock_sedes")->fetchColumn();
// 2. Datos para la tabla de inventario
$sedes_stmt = $pdo->query("SELECT id, nombre FROM sedes ORDER BY nombre");
$sedes = $sedes_stmt->fetchAll(PDO::FETCH_ASSOC);
$products_stmt = $pdo->query("SELECT id, nombre FROM products ORDER BY nombre");
$products = $products_stmt->fetchAll(PDO::FETCH_ASSOC);
$stock_stmt = $pdo->query("SELECT product_id, sede_id, quantity FROM stock_sedes");
$stock_data = $stock_stmt->fetchAll(PDO::FETCH_ASSOC);
// Procesar datos para la tabla
$inventario = [];
foreach ($products as $product) {
$inventario[$product['id']] = [
'nombre' => $product['nombre'],
'total' => 0,
'sedes' => array_fill_keys(array_column($sedes, 'id'), 0)
];
}
foreach ($stock_data as $stock_item) {
if (isset($inventario[$stock_item['product_id']])) {
$inventario[$stock_item['product_id']]['sedes'][$stock_item['sede_id']] = $stock_item['quantity'];
$inventario[$stock_item['product_id']]['total'] += $stock_item['quantity'];
}
}
// 3. Datos para el gráfico circular (Distribución por Sede)
$stock_por_sede_stmt = $pdo->query("
SELECT s.nombre, SUM(ss.quantity) as total_stock
FROM sedes s
JOIN stock_sedes ss ON s.id = ss.sede_id
GROUP BY s.nombre
HAVING total_stock > 0
ORDER BY s.nombre
");
$stock_por_sede = $stock_por_sede_stmt->fetchAll(PDO::FETCH_ASSOC);
$sede_chart_labels = json_encode(array_column($stock_por_sede, 'nombre'));
$sede_chart_data = json_encode(array_column($stock_por_sede, 'total_stock'));
// 4. Datos para el gráfico de barras (Stock por Producto)
$stock_por_producto_stmt = $pdo->query("
SELECT p.nombre, SUM(ss.quantity) as total_stock
FROM products p
JOIN stock_sedes ss ON p.id = ss.product_id
GROUP BY p.nombre
HAVING total_stock > 0
ORDER BY total_stock DESC
LIMIT 15
");
$stock_por_producto = $stock_por_producto_stmt->fetchAll(PDO::FETCH_ASSOC);
$producto_chart_labels = json_encode(array_column($stock_por_producto, 'nombre'));
$producto_chart_data = json_encode(array_column($stock_por_producto, 'total_stock'));
} catch (PDOException $e) {
echo "<div class='alert alert-danger'>Error al conectar con la base de datos: " . $e->getMessage() . "</div>";
// Consider logging the error and showing a more user-friendly message
// For now, we stop execution if the database connection fails.
echo "<div class='alert alert-danger'>Error al conectar o consultar la base de datos: " . $e->getMessage() . "</div>";
require_once 'layout_footer.php';
die();
}
?>
<div class="container-fluid mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="m-0">Inventario General</h1>
<a href="edit_product.php" class="btn btn-primary">Añadir Producto</a>
<h1 class="mb-4">Dashboard de Inventario</h1>
<!-- Tarjetas de Resumen -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card text-white bg-primary mb-3">
<div class="card-body">
<h5 class="card-title">Total de Productos</h5>
<p class="card-text fs-4"><?php echo $total_productos; ?></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white bg-success mb-3">
<div class="card-body">
<h5 class="card-title">Sedes Activas</h5>
<p class="card-text fs-4"><?php echo $total_sedes; ?></p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-white bg-info mb-3">
<div class="card-body">
<h5 class="card-title">Unidades Totales en Stock</h5>
<p class="card-text fs-4"><?php echo $total_stock ?: 0; ?></p>
</div>
</div>
</div>
</div>
<!-- Sección de Gráficos -->
<div class="row mb-4">
<div class="col-lg-6 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="m-0">Distribución por Sede</h5>
</div>
<div class="card-body d-flex justify-content-center align-items-center">
<canvas id="distribucionSedeChart" style="max-height: 300px;"></canvas>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="m-0">Top 15 Productos con Más Stock</h5>
</div>
<div class="card-body">
<canvas id="stockProductoChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Tabla de Inventario Detallado -->
<div class="card">
<div class="card-header">
<h5 class="m-0">Inventario por Sede</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<table class="table table-bordered table-hover">
<thead class="thead-light">
<tr>
<th>ID</th>
<th>Nombre</th>
<th>SKU</th>
<th>Precio</th>
<th class="text-center">Acciones</th>
<th>Producto</th>
<th class="text-center">Stock Total</th>
<?php foreach ($sedes as $sede): ?>
<th class="text-center"><?php echo htmlspecialchars($sede['nombre']); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php if (empty($products)): ?>
<?php if (empty($inventario)): ?>
<tr>
<td colspan="5" class="text-center">No hay productos en el inventario. <a href="edit_product.php">Agrega el primero</a>.</td>
<td colspan="<?php echo count($sedes) + 2; ?>" class="text-center">No hay productos para mostrar.</td>
</tr>
<?php else: ?>
<?php foreach ($products as $product): ?>
<?php foreach ($inventario as $datos_producto): ?>
<tr>
<td><?php echo htmlspecialchars($product['id']); ?></td>
<td><?php echo htmlspecialchars($product['nombre']); ?></td>
<td><?php echo htmlspecialchars($product['sku']); ?></td>
<td>S/ <?php echo htmlspecialchars(number_format($product['precio'], 2)); ?></td>
<td class="text-center">
<a href="edit_product.php?id=<?php echo $product['id']; ?>" class="btn btn-sm btn-warning">Editar</a>
<a href="delete_product.php?id=<?php echo $product['id']; ?>" class="btn btn-sm btn-danger" onclick="return confirm('¿Estás seguro de que quieres eliminar este producto?');">Eliminar</a>
</td>
<td><?php echo htmlspecialchars($datos_producto['nombre']); ?></td>
<td class="text-center fw-bold"><?php echo $datos_producto['total']; ?></td>
<?php foreach ($datos_producto['sedes'] as $cantidad): ?>
<td class="text-center"><?php echo $cantidad; ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
<?php endif; ?>
@ -64,4 +173,88 @@ try {
</div>
</div>
<!-- Incluir Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// --- Gráfico 1: Distribución por Sede (Circular) ---
const sedeLabels = <?php echo $sede_chart_labels; ?>;
const sedeData = <?php echo $sede_chart_data; ?>;
if (sedeLabels.length > 0) {
const defaultColors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40'];
const alidrvColor = '#28a745'; // Verde
const backgroundColors = sedeLabels.map((label, index) => {
if (label.toLowerCase() === 'alidrv') {
return alidrvColor;
}
return defaultColors[index % defaultColors.length];
});
const ctxSede = document.getElementById('distribucionSedeChart').getContext('2d');
new Chart(ctxSede, {
type: 'pie',
data: {
labels: sedeLabels,
datasets: [{
label: 'Stock',
data: sedeData,
backgroundColor: backgroundColors,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
}
}
}
});
}
// --- Gráfico 2: Stock por Producto (Barras) ---
const productoLabels = <?php echo $producto_chart_labels; ?>;
const productoData = <?php echo $producto_chart_data; ?>;
if (productoLabels.length > 0) {
const ctxProducto = document.getElementById('stockProductoChart').getContext('2d');
new Chart(ctxProducto, {
type: 'bar',
data: {
labels: productoLabels,
datasets: [{
label: 'Cantidad en Stock',
data: productoData,
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
indexAxis: 'y', // Hace que el gráfico sea horizontal
responsive: true,
plugins: {
legend: {
display: false
},
title: {
display: true,
text: 'Stock por Producto'
}
},
scales: {
x: {
beginAtZero: true
}
}
}
});
}
});
</script>
<?php require_once 'layout_footer.php'; ?>