Autosave: 20260203-052927
This commit is contained in:
parent
ea1b1360ff
commit
2caed5f3df
18
db/migrations/048_create_flujo_caja_table.sql
Normal file
18
db/migrations/048_create_flujo_caja_table.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `flujo_caja` (
|
||||||
|
`fecha` date NOT NULL,
|
||||||
|
`bcp_yape` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`b_nacion` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`interbank` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`bbva` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`otros_ingresos` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`tu1` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`tu2` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`tu3` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`fl1` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`fl2` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`fl3` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`rc_envio` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`rc_contraent` decimal(10,2) DEFAULT '0.00',
|
||||||
|
`total_inversion_publicitaria` decimal(10,2) DEFAULT '0.00',
|
||||||
|
PRIMARY KEY (`fecha`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
151
flujo_de_caja.php
Normal file
151
flujo_de_caja.php
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'layout_header.php';
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
$month = isset($_GET['month']) ? $_GET['month'] : date('m');
|
||||||
|
$year = isset($_GET['year']) ? $_GET['year'] : date('Y');
|
||||||
|
|
||||||
|
$start_date = "$year-$month-01";
|
||||||
|
$days_in_month = cal_days_in_month(CAL_GREGORIAN, $month, $year);
|
||||||
|
$end_date = "$year-$month-$days_in_month";
|
||||||
|
|
||||||
|
$flujo_data = [];
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM flujo_caja WHERE fecha BETWEEN ? AND ?");
|
||||||
|
$stmt->execute([$start_date, $end_date]);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$flujo_data[$row['fecha']] = $row;
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns = [
|
||||||
|
'bcp_yape', 'b_nacion', 'interbank', 'bbva', 'otros_ingresos',
|
||||||
|
'tu1', 'tu2', 'tu3', 'fl1', 'fl2', 'fl3',
|
||||||
|
'rc_envio', 'rc_contraent', 'total_inversion_publicitaria'
|
||||||
|
];
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<h2>Flujo de Caja</h2>
|
||||||
|
<p>Registro y control de los movimientos de ingresos y gastos.</p>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered table-striped table-hover" style="width: 100%;">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2" style="vertical-align: middle; text-align: center;">Fecha</th>
|
||||||
|
<th colspan="5" style="text-align: center;">Ingresos</th>
|
||||||
|
<th colspan="6" style="text-align: center;">Inversion Publicitaria</th>
|
||||||
|
<th rowspan="2" style="vertical-align: middle; text-align: center;">RC ENVIO</th>
|
||||||
|
<th rowspan="2" style="vertical-align: middle; text-align: center;">RC CONTRAENT</th>
|
||||||
|
<th rowspan="2" style="vertical-align: middle; text-align: center;">Total Ingresos</th>
|
||||||
|
<th rowspan="2" style="vertical-align: middle; text-align: center;">Total Inversion Publicitaria</th>
|
||||||
|
<th rowspan="2" style="vertical-align: middle; text-align: center;">Recaudo final</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>BCP/YAPE</th>
|
||||||
|
<th>B. NACION</th>
|
||||||
|
<th>INTERBANK</th>
|
||||||
|
<th>BBVA</th>
|
||||||
|
<th>Otros Ingresos</th>
|
||||||
|
<th>TU 1</th>
|
||||||
|
<th>TU 2</th>
|
||||||
|
<th>TU 3</th>
|
||||||
|
<th>FL1</th>
|
||||||
|
<th>FL2</th>
|
||||||
|
<th>FL3</th>
|
||||||
|
</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');
|
||||||
|
?>
|
||||||
|
<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>
|
||||||
|
<?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; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ingresos = getVal('bcp_yape') + getVal('b_nacion') + getVal('interbank') + getVal('bbva') + getVal('otros_ingresos') + getVal('rc_envio') + getVal('rc_contraent');
|
||||||
|
const inversionManual = getVal('total_inversion_publicitaria');
|
||||||
|
const inversionAuto = getVal('tu1') + getVal('tu2') + getVal('tu3') + getVal('fl1') + getVal('fl2') + getVal('fl3');
|
||||||
|
const totalInversion = inversionManual > 0 ? inversionManual : inversionAuto;
|
||||||
|
|
||||||
|
const recaudoFinal = ingresos - totalInversion;
|
||||||
|
|
||||||
|
row.querySelector('.total-ingresos').textContent = ingresos.toFixed(2);
|
||||||
|
row.querySelector('.total-inversion').textContent = totalInversion.toFixed(2);
|
||||||
|
row.querySelector('.recaudo-final').textContent = recaudoFinal.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial calculation for all rows
|
||||||
|
table.querySelectorAll('tbody tr').forEach(updateTotals);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ fecha: date, columna: column, valor: value })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once 'layout_footer.php';
|
||||||
|
?>
|
||||||
@ -103,6 +103,12 @@ $navItems = [
|
|||||||
'text' => 'Rentabilidad de Producto',
|
'text' => 'Rentabilidad de Producto',
|
||||||
'roles' => ['Administrador', 'admin']
|
'roles' => ['Administrador', 'admin']
|
||||||
],
|
],
|
||||||
|
'flujo_de_caja' => [
|
||||||
|
'url' => 'flujo_de_caja.php',
|
||||||
|
'icon' => 'fa-wallet',
|
||||||
|
'text' => 'Flujo de Caja',
|
||||||
|
'roles' => ['Administrador', 'admin']
|
||||||
|
],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'info_producto' => [
|
'info_producto' => [
|
||||||
|
|||||||
@ -30,7 +30,48 @@ function normalize_for_grouping($string) {
|
|||||||
return trim($string);
|
return trim($string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nueva función para parsear los productos usando la lista oficial
|
||||||
|
function parse_product_string($pedido_producto, $official_product_names) {
|
||||||
|
$found_products = [];
|
||||||
|
$remaining_string = $pedido_producto;
|
||||||
|
|
||||||
|
while (strlen($remaining_string) > 0) {
|
||||||
|
$match_found = false;
|
||||||
|
foreach ($official_product_names as $official_name) {
|
||||||
|
if (stripos($remaining_string, $official_name) === 0) {
|
||||||
|
$found_products[] = $official_name;
|
||||||
|
$remaining_string = trim(substr($remaining_string, strlen($official_name)));
|
||||||
|
// Remove leading comma and spaces
|
||||||
|
if (isset($remaining_string[0]) && $remaining_string[0] == ',') {
|
||||||
|
$remaining_string = trim(substr($remaining_string, 1));
|
||||||
|
}
|
||||||
|
$match_found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no official product is matched, we fallback to comma separation for robustness
|
||||||
|
if (!$match_found) {
|
||||||
|
$parts = explode(',', $remaining_string, 2);
|
||||||
|
$first_part = trim($parts[0]);
|
||||||
|
if (!empty($first_part)) {
|
||||||
|
$found_products[] = $first_part;
|
||||||
|
}
|
||||||
|
$remaining_string = isset($parts[1]) ? trim($parts[1]) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $found_products;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!empty($fecha_inicio_str) && !empty($fecha_fin_str)) {
|
if (!empty($fecha_inicio_str) && !empty($fecha_fin_str)) {
|
||||||
|
// --- Obtener nombres oficiales de productos ---
|
||||||
|
$stmt_products = $pdo->query("SELECT nombre FROM products");
|
||||||
|
$official_product_names = $stmt_products->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
// Sort by length descending to match longer names first
|
||||||
|
usort($official_product_names, function($a, $b) {
|
||||||
|
return strlen($b) - strlen($a);
|
||||||
|
});
|
||||||
|
|
||||||
// --- Recaudo por Asesora ---
|
// --- Recaudo por Asesora ---
|
||||||
$stmt_asesoras = $pdo->prepare("
|
$stmt_asesoras = $pdo->prepare("
|
||||||
SELECT
|
SELECT
|
||||||
@ -64,33 +105,30 @@ if (!empty($fecha_inicio_str) && !empty($fecha_fin_str)) {
|
|||||||
|
|
||||||
if ($pedidos) {
|
if ($pedidos) {
|
||||||
foreach ($pedidos as $pedido) {
|
foreach ($pedidos as $pedido) {
|
||||||
$product_names = array_map('trim', explode(',', $pedido['producto']));
|
// Usar la nueva función de parseo
|
||||||
$quantities = array_map('trim', explode('+', $pedido['cantidad']));
|
$productos_nombres = parse_product_string($pedido['producto'], $official_product_names);
|
||||||
|
$cantidades = array_map('trim', explode('+', $pedido['cantidad']));
|
||||||
$monto_total_pedido = (float)$pedido['monto_total'];
|
$monto_total_pedido = (float)$pedido['monto_total'];
|
||||||
$asesor = $pedido['nombre_asesor'] ?? 'Sin Asesor';
|
$asesor = $pedido['nombre_asesor'] ?? 'Sin Asesor';
|
||||||
|
|
||||||
foreach ($product_names as $index => $producto_nombre) {
|
foreach ($productos_nombres as $index => $nombre_producto) {
|
||||||
if (empty($producto_nombre)) continue;
|
if (empty($nombre_producto)) continue;
|
||||||
|
|
||||||
$cantidad = isset($quantities[$index]) ? (int)$quantities[$index] : 1;
|
$cantidad_producto = isset($cantidades[$index]) ? (int)$cantidades[$index] : 1;
|
||||||
if ($cantidad == 0) $cantidad = 1; // Evitar división por cero y asegurar al menos una unidad
|
$monto_producto = ($index == 0) ? $monto_total_pedido : 0;
|
||||||
|
|
||||||
// El primer producto obtiene el monto total, los demás obtienen 0
|
$normalized_key = normalize_for_grouping($nombre_producto);
|
||||||
$monto_asignado = ($index === 0) ? $monto_total_pedido : 0;
|
|
||||||
|
|
||||||
// Clave normalizada para agrupación
|
|
||||||
$normalized_key = normalize_for_grouping($producto_nombre);
|
|
||||||
|
|
||||||
// Acumular para "Resumen General de Productos Vendidos"
|
// Acumular para "Resumen General de Productos Vendidos"
|
||||||
if (!isset($productos_vendidos[$normalized_key])) {
|
if (!isset($productos_vendidos[$normalized_key])) {
|
||||||
$productos_vendidos[$normalized_key] = [
|
$productos_vendidos[$normalized_key] = [
|
||||||
'nombre_display' => $producto_nombre,
|
'nombre_display' => $nombre_producto,
|
||||||
'unidades' => 0,
|
'unidades' => 0,
|
||||||
'total_ingresos' => 0,
|
'total_ingresos' => 0
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
$productos_vendidos[$normalized_key]['unidades'] += $cantidad;
|
$productos_vendidos[$normalized_key]['unidades'] += $cantidad_producto;
|
||||||
$productos_vendidos[$normalized_key]['total_ingresos'] += $monto_asignado;
|
$productos_vendidos[$normalized_key]['total_ingresos'] += $monto_producto;
|
||||||
|
|
||||||
// Acumular para "Desglose de Ventas por Asesor"
|
// Acumular para "Desglose de Ventas por Asesor"
|
||||||
if (!isset($ventas_por_asesor[$asesor])) {
|
if (!isset($ventas_por_asesor[$asesor])) {
|
||||||
@ -98,13 +136,13 @@ if (!empty($fecha_inicio_str) && !empty($fecha_fin_str)) {
|
|||||||
}
|
}
|
||||||
if (!isset($ventas_por_asesor[$asesor][$normalized_key])) {
|
if (!isset($ventas_por_asesor[$asesor][$normalized_key])) {
|
||||||
$ventas_por_asesor[$asesor][$normalized_key] = [
|
$ventas_por_asesor[$asesor][$normalized_key] = [
|
||||||
'nombre_display' => $producto_nombre,
|
'nombre_display' => $nombre_producto,
|
||||||
'unidades' => 0,
|
'unidades' => 0,
|
||||||
'total_ingresos' => 0,
|
'total_ingresos' => 0
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
$ventas_por_asesor[$asesor][$normalized_key]['unidades'] += $cantidad;
|
$ventas_por_asesor[$asesor][$normalized_key]['unidades'] += $cantidad_producto;
|
||||||
$ventas_por_asesor[$asesor][$normalized_key]['total_ingresos'] += $monto_asignado;
|
$ventas_por_asesor[$asesor][$normalized_key]['total_ingresos'] += $monto_producto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,7 +223,9 @@ if (!empty($fecha_inicio_str) && !empty($fecha_fin_str)) {
|
|||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php uasort($productos_vendidos, function($a, $b) { return $b['unidades'] <=> $a['unidades']; }); ?>
|
<?php uasort($productos_vendidos, function($a, $b) { return $b['unidades'] <=> $a['unidades']; }); ?>
|
||||||
<?php foreach ($productos_vendidos as $producto): ?>
|
<?php foreach ($productos_vendidos as $producto): ?>
|
||||||
<?php $precio_promedio = ($producto['unidades'] > 0) ? $producto['total_ingresos'] / $producto['unidades'] : 0; ?>
|
<?php
|
||||||
|
$precio_promedio = ($producto['unidades'] > 0) ? $producto['total_ingresos'] / $producto['unidades'] : 0;
|
||||||
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= htmlspecialchars($producto['nombre_display']) ?></td>
|
<td><?= htmlspecialchars($producto['nombre_display']) ?></td>
|
||||||
<td><?= $producto['unidades'] ?></td>
|
<td><?= $producto['unidades'] ?></td>
|
||||||
@ -222,7 +262,9 @@ if (!empty($fecha_inicio_str) && !empty($fecha_fin_str)) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php uasort($productos, function($a, $b) { return $b['unidades'] <=> $a['unidades']; }); ?>
|
<?php uasort($productos, function($a, $b) { return $b['unidades'] <=> $a['unidades']; }); ?>
|
||||||
<?php foreach ($productos as $data): ?>
|
<?php foreach ($productos as $data): ?>
|
||||||
<?php $precio_promedio = ($data['unidades'] > 0) ? $data['total_ingresos'] / $data['unidades'] : 0; ?>
|
<?php
|
||||||
|
$precio_promedio = ($data['unidades'] > 0) ? $data['total_ingresos'] / $data['unidades'] : 0;
|
||||||
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= htmlspecialchars($data['nombre_display']) ?></td>
|
<td><?= htmlspecialchars($data['nombre_display']) ?></td>
|
||||||
<td><?= $data['unidades'] ?></td>
|
<td><?= $data['unidades'] ?></td>
|
||||||
@ -239,4 +281,4 @@ if (!empty($fecha_inicio_str) && !empty($fecha_fin_str)) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php include 'layout_footer.php'; ?>
|
<?php include 'layout_footer.php'; ?>
|
||||||
|
|||||||
40
save_flujo_caja.php
Normal file
40
save_flujo_caja.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if ($data) {
|
||||||
|
$fecha = $data['fecha'];
|
||||||
|
$columna = $data['columna'];
|
||||||
|
$valor = $data['valor'];
|
||||||
|
|
||||||
|
// Column name validation to prevent SQL injection
|
||||||
|
$allowed_columns = [
|
||||||
|
'bcp_yape', 'b_nacion', 'interbank', 'bbva', 'otros_ingresos',
|
||||||
|
'tu1', 'tu2', 'tu3', 'fl1', 'fl2', 'fl3',
|
||||||
|
'rc_envio', 'rc_contraent', 'total_inversion_publicitaria'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (in_array($columna, $allowed_columns)) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
// Use INSERT ... ON DUPLICATE KEY UPDATE to handle both new and existing rows
|
||||||
|
$sql = "INSERT INTO flujo_caja (fecha, $columna) VALUES (:fecha, :valor)
|
||||||
|
ON DUPLICATE KEY UPDATE $columna = :valor";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute(['fecha' => $fecha, 'valor' => $valor]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid column name.']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'No data received.']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
Loading…
x
Reference in New Issue
Block a user