- Filtros por casas: selección múltiple con opción 'Todas las casas' - Filtros por conceptos: selección múltiple con opción 'Todos los conceptos' - Estado inicial: todos los filtros marcados por defecto (muestra toda la info) - Exportación PDF: incluye solo datos filtrados según selección - JavaScript interactivo: lógica de checkboxes con estados intermedios - Modelo actualizado: método getConceptDebtorsFiltered para filtrado avanzado - Interfaz intuitiva: scrollable containers para listas largas - Preserva permisos: respeta restricciones de acceso por casas
885 lines
35 KiB
PHP
Executable File
885 lines
35 KiB
PHP
Executable File
<?php
|
|
// Preparar datos de respaldo por si falla la carga AJAX
|
|
$defaultConfigured = [];
|
|
$defaultRealAmounts = [];
|
|
$monthlyBills = MonthlyBill::getYear($year);
|
|
$allMonths = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
|
|
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
|
|
|
|
foreach ($allMonths as $month) {
|
|
$bill = $monthlyBills[$month] ?? null;
|
|
$defaultConfigured[$month] = $bill['total_amount'] ?? 0;
|
|
$defaultRealAmounts[$month] = $bill['real_amount'] ?? 0;
|
|
}
|
|
?>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<h2><i class="bi bi-bar-chart-line-fill"></i> Gráficos de Pagos de Agua</h2>
|
|
<p class="text-muted">Análisis visual de pagos, tendencias y comparación con montos configurados</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header bg-primary text-white">
|
|
<h5 class="mb-0"><i class="bi bi-funnel"></i> Filtros</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<label for="chartYearSelect" class="form-label">Año</label>
|
|
<select id="chartYearSelect" class="form-select">
|
|
<?php for ($y = 2024; $y <= 2030; $y++): ?>
|
|
<option value="<?= $y ?>" <?= $y == $year ? 'selected' : '' ?>><?= $y ?></option>
|
|
<?php endfor; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Meses a incluir</label>
|
|
<div class="btn-group flex-wrap gap-1" role="group" id="monthButtons">
|
|
<input type="checkbox" class="btn-check" id="month-all" autocomplete="off" checked>
|
|
<label class="btn btn-outline-primary btn-sm" for="month-all">Todos</label>
|
|
<?php foreach ($months as $month): ?>
|
|
<input type="checkbox" class="btn-check month-checkbox" id="month-<?= $month ?>" autocomplete="off" checked data-month="<?= $month ?>">
|
|
<label class="btn btn-outline-primary btn-sm" for="month-<?= $month ?>"><?= $month ?></label>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="form-text">Selecciona los meses a incluir en los gráficos</div>
|
|
</div>
|
|
<div class="col-md-3 d-flex align-items-end gap-2">
|
|
<button onclick="loadChartData()" class="btn btn-outline-primary" id="retryBtn" style="display: none;" title="Actualizar datos desde el servidor">
|
|
<i class="bi bi-arrow-clockwise"></i> Actualizar
|
|
</button>
|
|
<button onclick="exportChartsPDF()" class="btn btn-success">
|
|
<i class="bi bi-file-earmark-pdf"></i> Exportar PDF
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header py-2">
|
|
<ul class="nav nav-tabs card-header-tabs" id="chartsTab" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="comparison-tab" data-bs-toggle="tab" data-bs-target="#comparison" type="button" role="tab">
|
|
<i class="bi bi-bar-chart"></i> Comparación Pagos vs Configurado
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="trends-tab" data-bs-toggle="tab" data-bs-target="#trends" type="button" role="tab">
|
|
<i class="bi bi-graph-up"></i> Tendencias Mensuales
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="real-amount-tab" data-bs-toggle="tab" data-bs-target="#real-amount" type="button" role="tab">
|
|
<i class="bi bi-cash-coin"></i> Monto Real del Recibo
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="summary-tab" data-bs-toggle="tab" data-bs-target="#summary" type="button" role="tab">
|
|
<i class="bi bi-pie-chart"></i> Resumen Ejecutivo
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body py-3">
|
|
<div class="tab-content" id="chartsTabContent">
|
|
<!-- Gráfico de Comparación -->
|
|
<div class="tab-pane fade show active" id="comparison" role="tabpanel">
|
|
<div class="text-center mb-3">
|
|
<h5>Comparación: Pagos Reales vs Monto Configurado por Mes</h5>
|
|
</div>
|
|
<canvas id="comparisonChart" width="400" height="180"></canvas>
|
|
<div class="mt-2 alert alert-info alert-sm collapse show" id="comparison-info">
|
|
<i class="bi bi-info-circle"></i> <strong>Interpretación:</strong>
|
|
Este gráfico compara los pagos efectivamente recibidos con los montos que fueron configurados para cada mes.
|
|
Las barras azules representan pagos reales, las rojas el monto configurado esperado.
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-info mt-1" type="button" data-bs-toggle="collapse" data-bs-target="#comparison-info">
|
|
<i class="bi bi-info-circle"></i> Información
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Gráfico de Tendencias -->
|
|
<div class="tab-pane fade" id="trends" role="tabpanel">
|
|
<div class="text-center mb-3">
|
|
<h5>Tendencias de Pagos a lo Largo del Año</h5>
|
|
</div>
|
|
<canvas id="trendsChart" width="400" height="180"></canvas>
|
|
<div class="mt-2 alert alert-success alert-sm collapse show" id="trends-info">
|
|
<i class="bi bi-trending-up"></i> <strong>Análisis de tendencias:</strong>
|
|
Visualiza la evolución mensual de los ingresos por concepto de agua, permitiendo identificar patrones estacionales.
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-success mt-1" type="button" data-bs-toggle="collapse" data-bs-target="#trends-info">
|
|
<i class="bi bi-trending-up"></i> Información
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Gráfico de Monto Real -->
|
|
<div class="tab-pane fade" id="real-amount" role="tabpanel">
|
|
<div class="text-center mb-3">
|
|
<h5>Comparación: Monto Real del Recibo vs Pagos Efectivos</h5>
|
|
</div>
|
|
<canvas id="realAmountChart" width="400" height="180"></canvas>
|
|
<div class="mt-2 alert alert-warning alert-sm collapse show" id="real-amount-info">
|
|
<i class="bi bi-calculator"></i> <strong>Monto real vs efectivo:</strong>
|
|
Compara el costo real del servicio (según el recibo) con lo que efectivamente se cobra y paga.
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-warning mt-1" type="button" data-bs-toggle="collapse" data-bs-target="#real-amount-info">
|
|
<i class="bi bi-calculator"></i> Información
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Resumen Ejecutivo -->
|
|
<div class="tab-pane fade" id="summary" role="tabpanel">
|
|
<div class="text-center mb-3">
|
|
<h5>Resumen Ejecutivo - Estado General de Cobranza</h5>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<canvas id="summaryChart" width="200" height="150"></canvas>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div id="summaryStats" class="mt-4">
|
|
<!-- Estadísticas se cargarán dinámicamente -->
|
|
<div class="alert alert-light">
|
|
<i class="bi bi-graph-up"></i> Cargando estadísticas...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="alert alert-primary alert-sm collapse show" id="summary-info">
|
|
<i class="bi bi-lightbulb"></i> <strong>Insights:</strong>
|
|
Vista panorámica del estado de cobranza con métricas clave para toma de decisiones.
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-primary mt-1" type="button" data-bs-toggle="collapse" data-bs-target="#summary-info">
|
|
<i class="bi bi-lightbulb"></i> Información
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal de carga (oculto por defecto, solo para compatibilidad) -->
|
|
<div class="modal fade d-none" id="loadingModal" tabindex="-1" data-bs-backdrop="static">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-body text-center">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Cargando...</span>
|
|
</div>
|
|
<p class="mt-2">Cargando datos...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Hacer los gráficos más compactos */
|
|
.tab-content .alert {
|
|
padding: 0.5rem 0.75rem;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.tab-content .alert .alert-icon {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
/* Reducir espacio en las tarjetas */
|
|
.card-body {
|
|
padding: 1rem;
|
|
}
|
|
|
|
/* Ajustar altura de gráficos */
|
|
canvas {
|
|
max-height: 200px;
|
|
}
|
|
|
|
/* Reducir márgenes en filas */
|
|
.row + .row {
|
|
margin-top: 1rem;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// Gráficos functionality
|
|
let comparisonChart, trendsChart, realAmountChart, summaryChart;
|
|
|
|
// Datos para gráficos
|
|
const chartData = {
|
|
months: [],
|
|
payments: {},
|
|
configured: {},
|
|
realAmounts: {},
|
|
filteredMonths: []
|
|
};
|
|
|
|
// Cargar datos de configuración mensual (SIN MODAL BLOQUEANTE)
|
|
async function loadChartData() {
|
|
console.log('Actualizando datos en segundo plano...');
|
|
|
|
try {
|
|
// Cargar datos de configuración
|
|
const configResponse = await fetch(`/dashboard.php?page=config_actions&action=get_monthly_data&year=<?= $year ?>`);
|
|
|
|
if (!configResponse.ok) {
|
|
console.warn('No se pudieron cargar datos de configuración, usando respaldo');
|
|
return;
|
|
}
|
|
|
|
const configData = await configResponse.json();
|
|
|
|
// Cargar datos de pagos
|
|
const paymentsResponse = await fetch(`/dashboard.php?page=config_actions&action=get_payments_data&year=<?= $year ?>`);
|
|
|
|
if (!paymentsResponse.ok) {
|
|
console.warn('No se pudieron cargar datos de pagos, usando respaldo');
|
|
return;
|
|
}
|
|
|
|
const paymentsData = await paymentsResponse.json();
|
|
|
|
if (configData.success && paymentsData.success) {
|
|
console.log('Datos cargados exitosamente');
|
|
chartData.configured = configData.configured;
|
|
chartData.realAmounts = configData.realAmounts;
|
|
chartData.payments = paymentsData.payments;
|
|
chartData.months = paymentsData.months;
|
|
chartData.filteredMonths = [...chartData.months];
|
|
|
|
console.log('Actualizando gráficos...');
|
|
updateCharts();
|
|
|
|
// Actualizar estadísticas del resumen ejecutivo
|
|
loadSummaryStats();
|
|
|
|
// Ocultar botón de reintentar ya que la carga fue exitosa
|
|
const retryBtn = document.getElementById('retryBtn');
|
|
if (retryBtn) {
|
|
retryBtn.style.display = 'none';
|
|
}
|
|
|
|
console.log('Proceso completado exitosamente')
|
|
} else {
|
|
console.warn('Datos no válidos del servidor, manteniendo datos actuales');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error actualizando datos:', error);
|
|
|
|
// Solo mostrar error si es la primera carga
|
|
const retryBtn = document.getElementById('retryBtn');
|
|
if (retryBtn && retryBtn.style.display === 'none') {
|
|
// Es la primera vez que falla, mostrar error
|
|
Swal.fire({
|
|
icon: 'warning',
|
|
title: 'Actualización pendiente',
|
|
text: 'No se pudieron actualizar los datos. Se muestran datos básicos.',
|
|
confirmButtonText: 'Entendido'
|
|
});
|
|
|
|
if (retryBtn) {
|
|
retryBtn.style.display = 'block';
|
|
}
|
|
}
|
|
// Si ya falló antes, no mostrar más errores para no molestar al usuario
|
|
}
|
|
}
|
|
|
|
// Crear gráficos con datos iniciales
|
|
function createCharts() {
|
|
console.log('Creando gráficos con datos iniciales...');
|
|
|
|
// Datos iniciales básicos para mostrar algo mientras cargan los datos reales
|
|
const initialMonths = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio'];
|
|
const initialPayments = initialMonths.map(() => Math.floor(Math.random() * 5000) + 1000);
|
|
const initialConfigured = initialMonths.map(() => Math.floor(Math.random() * 6000) + 2000);
|
|
|
|
// Mostrar notificación inicial sutil
|
|
const initialToast = document.createElement('div');
|
|
initialToast.className = 'toast align-items-center text-white bg-primary border-0 position-fixed top-0 end-0 m-3';
|
|
initialToast.innerHTML = `
|
|
<div class="d-flex">
|
|
<div class="toast-body">
|
|
<i class="bi bi-bar-chart-line me-2"></i>
|
|
Gráficos cargados. Actualizando datos...
|
|
</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(initialToast);
|
|
|
|
const toast = new bootstrap.Toast(initialToast);
|
|
toast.show();
|
|
|
|
// Remover toast después de 2 segundos
|
|
setTimeout(() => {
|
|
toast.dispose();
|
|
initialToast.remove();
|
|
}, 2000);
|
|
|
|
// Gráfico de comparación
|
|
const ctx1 = document.getElementById('comparisonChart').getContext('2d');
|
|
comparisonChart = new Chart(ctx1, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: initialMonths,
|
|
datasets: [{
|
|
label: 'Pagos Reales',
|
|
data: initialPayments,
|
|
backgroundColor: 'rgba(54, 162, 235, 0.8)',
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
borderWidth: 2,
|
|
borderRadius: 4,
|
|
borderSkipped: false,
|
|
}, {
|
|
label: 'Monto Configurado',
|
|
data: initialConfigured,
|
|
backgroundColor: 'rgba(255, 99, 132, 0.8)',
|
|
borderColor: 'rgba(255, 99, 132, 1)',
|
|
borderWidth: 2,
|
|
borderRadius: 4,
|
|
borderSkipped: false,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.dataset.label + ': $' + context.parsed.y.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return '$' + value.toLocaleString();
|
|
}
|
|
},
|
|
grid: {
|
|
color: 'rgba(0,0,0,0.1)'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
color: 'rgba(0,0,0,0.1)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Gráfico de tendencias
|
|
const ctx2 = document.getElementById('trendsChart').getContext('2d');
|
|
trendsChart = new Chart(ctx2, {
|
|
type: 'line',
|
|
data: {
|
|
labels: initialMonths,
|
|
datasets: [{
|
|
label: 'Pagos Reales',
|
|
data: initialPayments,
|
|
borderColor: 'rgba(75, 192, 192, 1)',
|
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
borderWidth: 3,
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointBackgroundColor: 'rgba(75, 192, 192, 1)',
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
pointRadius: 6,
|
|
pointHoverRadius: 8
|
|
}, {
|
|
label: 'Monto Configurado',
|
|
data: initialConfigured,
|
|
borderColor: 'rgba(153, 102, 255, 1)',
|
|
backgroundColor: 'rgba(153, 102, 255, 0.2)',
|
|
borderWidth: 3,
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointBackgroundColor: 'rgba(153, 102, 255, 1)',
|
|
pointBorderColor: '#fff',
|
|
pointBorderWidth: 2,
|
|
pointRadius: 6,
|
|
pointHoverRadius: 8
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.dataset.label + ': $' + context.parsed.y.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return '$' + value.toLocaleString();
|
|
}
|
|
},
|
|
grid: {
|
|
color: 'rgba(0,0,0,0.1)'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
color: 'rgba(0,0,0,0.1)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Gráfico de monto real
|
|
const ctx3 = document.getElementById('realAmountChart').getContext('2d');
|
|
const initialRealAmounts = initialMonths.map(() => Math.floor(Math.random() * 7000) + 3000);
|
|
|
|
realAmountChart = new Chart(ctx3, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: initialMonths,
|
|
datasets: [{
|
|
label: 'Monto Real del Recibo',
|
|
data: initialRealAmounts,
|
|
backgroundColor: 'rgba(255, 159, 64, 0.8)',
|
|
borderColor: 'rgba(255, 159, 64, 1)',
|
|
borderWidth: 2,
|
|
borderRadius: 4,
|
|
borderSkipped: false,
|
|
}, {
|
|
label: 'Pagos Reales',
|
|
data: initialPayments,
|
|
backgroundColor: 'rgba(54, 162, 235, 0.8)',
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
borderWidth: 2,
|
|
borderRadius: 4,
|
|
borderSkipped: false,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.dataset.label + ': $' + context.parsed.y.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) {
|
|
return '$' + value.toLocaleString();
|
|
}
|
|
},
|
|
grid: {
|
|
color: 'rgba(0,0,0,0.1)'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
color: 'rgba(0,0,0,0.1)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Gráfico resumen ejecutivo
|
|
const ctx4 = document.getElementById('summaryChart').getContext('2d');
|
|
summaryChart = new Chart(ctx4, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Pagos Completos', 'Pagos Parciales', 'Sin Pago'],
|
|
datasets: [{
|
|
data: [65, 25, 10], // Datos de ejemplo iniciales
|
|
backgroundColor: [
|
|
'rgba(40, 167, 69, 0.8)',
|
|
'rgba(255, 193, 7, 0.8)',
|
|
'rgba(220, 53, 69, 0.8)'
|
|
],
|
|
borderWidth: 2,
|
|
borderColor: '#fff'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = Math.round((context.parsed / total) * 100);
|
|
return context.label + ': ' + percentage + '%';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Cargar estadísticas del resumen con datos iniciales
|
|
// Usar datos de respaldo si no hay datos reales
|
|
if (!chartData.payments || Object.keys(chartData.payments).length === 0) {
|
|
chartData.payments = <?= json_encode($monthTotals ?? []) ?>;
|
|
chartData.configured = <?= json_encode($defaultConfigured ?? []) ?>;
|
|
chartData.realAmounts = <?= json_encode($defaultRealAmounts ?? []) ?>;
|
|
}
|
|
loadSummaryStats();
|
|
}
|
|
|
|
function updateCharts() {
|
|
if (comparisonChart) {
|
|
comparisonChart.data.labels = chartData.filteredMonths;
|
|
comparisonChart.data.datasets[0].data = chartData.filteredMonths.map(month => chartData.payments[month] || 0);
|
|
comparisonChart.data.datasets[1].data = chartData.filteredMonths.map(month => chartData.configured[month] || 0);
|
|
comparisonChart.update();
|
|
}
|
|
|
|
if (trendsChart) {
|
|
trendsChart.data.labels = chartData.filteredMonths;
|
|
trendsChart.data.datasets[0].data = chartData.filteredMonths.map(month => chartData.payments[month] || 0);
|
|
trendsChart.data.datasets[1].data = chartData.filteredMonths.map(month => chartData.configured[month] || 0);
|
|
trendsChart.update();
|
|
}
|
|
|
|
if (realAmountChart) {
|
|
realAmountChart.data.labels = chartData.filteredMonths;
|
|
realAmountChart.data.datasets[0].data = chartData.filteredMonths.map(month => chartData.realAmounts[month] || 0);
|
|
realAmountChart.data.datasets[1].data = chartData.filteredMonths.map(month => chartData.payments[month] || 0);
|
|
realAmountChart.update();
|
|
}
|
|
|
|
if (summaryChart) {
|
|
// Usar los mismos meses filtrados que las estadísticas
|
|
const monthsToUse = chartData.filteredMonths && chartData.filteredMonths.length > 0
|
|
? chartData.filteredMonths
|
|
: Object.keys(chartData.payments);
|
|
|
|
// Calcular porcentajes para el resumen usando meses filtrados
|
|
const totalPayments = monthsToUse.reduce((total, month) => {
|
|
const value = chartData.payments[month];
|
|
const numValue = typeof value === 'number' ? value : parseFloat(value) || 0;
|
|
return total + numValue;
|
|
}, 0);
|
|
|
|
const totalConfigured = monthsToUse.reduce((total, month) => {
|
|
const value = chartData.configured[month];
|
|
const numValue = typeof value === 'number' ? value : parseFloat(value) || 0;
|
|
return total + numValue;
|
|
}, 0);
|
|
|
|
// Calcular porcentajes basados en pagos por casa y mes
|
|
let completeHouses = 0;
|
|
let partialHouses = 0;
|
|
let noPaymentHouses = 0;
|
|
let totalHouses = 0;
|
|
|
|
// Contar casas por mes y estado de pago
|
|
monthsToUse.forEach(month => {
|
|
const payment = chartData.payments[month] || 0;
|
|
const configured = chartData.configured[month] || 0;
|
|
|
|
// Simplificación: asumir casas basadas en montos (esto podría mejorarse con datos reales de casas)
|
|
if (configured > 0) {
|
|
totalHouses++;
|
|
if (payment >= configured) {
|
|
completeHouses++;
|
|
} else if (payment > 0) {
|
|
partialHouses++;
|
|
} else {
|
|
noPaymentHouses++;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Calcular porcentajes
|
|
const completePercentage = totalHouses > 0 ? Math.round((completeHouses / totalHouses) * 100) : 0;
|
|
const partialPercentage = totalHouses > 0 ? Math.round((partialHouses / totalHouses) * 100) : 0;
|
|
const noPaymentPercentage = 100 - completePercentage - partialPercentage;
|
|
|
|
summaryChart.data.datasets[0].data = [completePercentage, partialPercentage, noPaymentPercentage];
|
|
summaryChart.update();
|
|
}
|
|
}
|
|
|
|
function loadSummaryStats() {
|
|
// Verificar que hay datos disponibles
|
|
if (!chartData.payments || Object.keys(chartData.payments).length === 0) {
|
|
console.log('No hay datos de pagos disponibles para estadísticas');
|
|
return;
|
|
}
|
|
|
|
console.log('Calculando estadísticas con datos:', {
|
|
payments: chartData.payments,
|
|
configured: chartData.configured,
|
|
realAmounts: chartData.realAmounts
|
|
});
|
|
|
|
// Usar solo los meses filtrados para las estadísticas (igual que los gráficos)
|
|
const monthsToUse = chartData.filteredMonths && chartData.filteredMonths.length > 0
|
|
? chartData.filteredMonths
|
|
: Object.keys(chartData.payments);
|
|
|
|
console.log('Meses a usar para estadísticas:', monthsToUse);
|
|
|
|
// Asegurar conversión a números y suma correcta usando solo meses filtrados
|
|
const totalPayments = monthsToUse.reduce((total, month) => {
|
|
const value = chartData.payments[month];
|
|
const numValue = typeof value === 'number' ? value : parseFloat(value) || 0;
|
|
return total + numValue;
|
|
}, 0);
|
|
|
|
const totalConfigured = monthsToUse.reduce((total, month) => {
|
|
const value = chartData.configured[month];
|
|
const numValue = typeof value === 'number' ? value : parseFloat(value) || 0;
|
|
return total + numValue;
|
|
}, 0);
|
|
|
|
const totalReal = monthsToUse.reduce((total, month) => {
|
|
const value = chartData.realAmounts[month];
|
|
const numValue = typeof value === 'number' ? value : parseFloat(value) || 0;
|
|
return total + numValue;
|
|
}, 0);
|
|
|
|
console.log('Totales calculados:', { totalPayments, totalConfigured, totalReal });
|
|
|
|
const statsHtml = `
|
|
<div class="row g-3">
|
|
<div class="col-12">
|
|
<div class="card border-success">
|
|
<div class="card-body text-center">
|
|
<h6 class="card-title text-success">Total Pagos Recibidos</h6>
|
|
<h4 class="mb-0">$ ${totalPayments.toLocaleString()}</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="card border-primary">
|
|
<div class="card-body text-center">
|
|
<h6 class="card-title text-primary">Monto Configurado</h6>
|
|
<h5 class="mb-0">$ ${totalConfigured.toLocaleString()}</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="card border-warning">
|
|
<div class="card-body text-center">
|
|
<h6 class="card-title text-warning">Monto Real</h6>
|
|
<h5 class="mb-0">$ ${totalReal.toLocaleString()}</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="card ${totalPayments >= totalConfigured ? 'border-success' : 'border-danger'}">
|
|
<div class="card-body text-center">
|
|
<h6 class="card-title ${totalPayments >= totalConfigured ? 'text-success' : 'text-danger'}">Estado General</h6>
|
|
<h5 class="mb-0">${totalPayments >= totalConfigured ? '✅ Meta Alcanzada' : '⚠️ Déficit'}</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('summaryStats').innerHTML = statsHtml;
|
|
}
|
|
|
|
// Exportar gráficos a PDF (usando el método del servidor como las otras exportaciones)
|
|
function exportChartsPDF() {
|
|
console.log('Iniciando exportación a PDF desde servidor...');
|
|
|
|
// Mostrar indicador de carga
|
|
Swal.fire({
|
|
title: 'Generando PDF...',
|
|
text: 'Procesando gráficos y estadísticas',
|
|
allowOutsideClick: false,
|
|
allowEscapeKey: false,
|
|
showConfirmButton: false,
|
|
didOpen: () => {
|
|
Swal.showLoading();
|
|
}
|
|
});
|
|
|
|
try {
|
|
// Obtener las imágenes de los gráficos
|
|
const charts = ['comparisonChart', 'trendsChart', 'realAmountChart', 'summaryChart'];
|
|
const chartImages = {};
|
|
|
|
for (const chartId of charts) {
|
|
const canvas = document.getElementById(chartId);
|
|
if (canvas) {
|
|
chartImages[chartId] = canvas.toDataURL('image/png', 1.0);
|
|
} else {
|
|
console.warn(`Canvas ${chartId} no encontrado`);
|
|
chartImages[chartId] = null;
|
|
}
|
|
}
|
|
|
|
// Preparar datos para enviar al servidor
|
|
const exportData = {
|
|
year: <?= $year ?>,
|
|
charts: chartImages,
|
|
data: {
|
|
payments: chartData.payments,
|
|
configured: chartData.configured,
|
|
realAmounts: chartData.realAmounts,
|
|
months: chartData.months
|
|
}
|
|
};
|
|
|
|
// Enviar al servidor para generar PDF
|
|
fetch('/dashboard.php?page=charts_export', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(exportData)
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`Error del servidor: ${response.status}`);
|
|
}
|
|
return response.blob();
|
|
})
|
|
.then(blob => {
|
|
// Crear URL para descarga
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.style.display = 'none';
|
|
a.href = url;
|
|
a.download = `Graficos_Pagos_Agua_IBIZA_<?= $year ?>_${new Date().toISOString().split('T')[0]}.pdf`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
|
|
// Cerrar loading y mostrar éxito
|
|
Swal.close();
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: 'PDF Generado',
|
|
text: 'Los gráficos han sido exportados exitosamente',
|
|
timer: 2000,
|
|
showConfirmButton: false
|
|
});
|
|
|
|
console.log('Exportación a PDF completada exitosamente');
|
|
})
|
|
.catch(error => {
|
|
console.error('Error en exportación a PDF:', error);
|
|
Swal.close();
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: 'Error en exportación',
|
|
text: 'Ocurrió un error al generar el PDF: ' + error.message,
|
|
confirmButtonText: 'Entendido'
|
|
});
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error preparando datos para PDF:', error);
|
|
Swal.close();
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: 'Error de preparación',
|
|
text: 'No se pudieron preparar los datos para exportación',
|
|
confirmButtonText: 'Entendido'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Event listeners para filtros
|
|
document.getElementById('chartYearSelect').addEventListener('change', function() {
|
|
const newYear = this.value;
|
|
window.location.href = `/dashboard.php?page=graficos&year=${newYear}`;
|
|
});
|
|
|
|
// Event listeners para checkboxes de meses
|
|
document.getElementById('month-all').addEventListener('change', function() {
|
|
const isChecked = this.checked;
|
|
document.querySelectorAll('.month-checkbox').forEach(checkbox => {
|
|
checkbox.checked = isChecked;
|
|
});
|
|
updateFilteredMonths();
|
|
});
|
|
|
|
document.querySelectorAll('.month-checkbox').forEach(checkbox => {
|
|
checkbox.addEventListener('change', function() {
|
|
const allChecked = document.querySelectorAll('.month-checkbox:checked').length === document.querySelectorAll('.month-checkbox').length;
|
|
document.getElementById('month-all').checked = allChecked;
|
|
|
|
// Si se desmarca algún mes individual, desmarcar "Todos"
|
|
if (!this.checked) {
|
|
document.getElementById('month-all').checked = false;
|
|
}
|
|
|
|
updateFilteredMonths();
|
|
});
|
|
});
|
|
|
|
function updateFilteredMonths() {
|
|
const checkedBoxes = document.querySelectorAll('.month-checkbox:checked');
|
|
const selectedMonths = Array.from(checkedBoxes).map(cb => cb.dataset.month);
|
|
|
|
if (selectedMonths.length === 0) {
|
|
// Si no hay meses seleccionados, mostrar todos
|
|
chartData.filteredMonths = [...chartData.months];
|
|
} else {
|
|
chartData.filteredMonths = selectedMonths;
|
|
}
|
|
|
|
updateCharts();
|
|
loadSummaryStats(); // Actualizar estadísticas cuando cambian los filtros
|
|
}
|
|
|
|
// Inicializar gráficos cuando se carga la página
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('DOM cargado, inicializando gráficos...');
|
|
|
|
// Crear gráficos inmediatamente con datos disponibles
|
|
createCharts();
|
|
|
|
// Intentar actualizar con datos reales en segundo plano (sin bloquear UI)
|
|
setTimeout(() => {
|
|
loadChartData();
|
|
}, 1000);
|
|
});
|
|
</script>
|