Files
ibiza_sistema/views/charts/index.php
nickpons666 535f7c5963 feat: Agregar filtros avanzados a reporte de Deudores de Conceptos
- 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
2026-01-05 16:12:24 -06:00

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>