feat: Implementar página dedicada de gráficos para análisis de pagos de agua
- Crear nueva página /graficos con 4 tipos de gráficos interactivos - Agregar compatibilidad con tema oscuro en selectores - Implementar exportación a PDF profesional con encabezados - Agregar campo 'Monto Real del Recibo' a configuración mensual - Crear migración para nueva columna real_amount en monthly_bills - Mejorar filtros con botones interactivos en lugar de select múltiple - Agregar resumen ejecutivo con estadísticas detalladas - Optimizar espacio visual y responsividad de gráficos - Integrar Chart.js y jsPDF para funcionalidad avanzada - Corregir problemas de carga de datos y filtros dinámicos
This commit is contained in:
@@ -93,6 +93,26 @@ body {
|
||||
box-shadow: 0 0 0 0.25rem rgba(var(--navbar-bg-color), 0.25);
|
||||
}
|
||||
|
||||
/* Select múltiple específico */
|
||||
.form-select[multiple] {
|
||||
background-color: var(--input-bg-color) !important;
|
||||
border-color: var(--input-border-color) !important;
|
||||
color: var(--input-text-color) !important;
|
||||
}
|
||||
.form-select[multiple] option {
|
||||
background-color: var(--input-bg-color) !important;
|
||||
color: var(--input-text-color) !important;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
}
|
||||
.form-select[multiple] option:checked {
|
||||
background-color: rgba(13, 110, 253, 0.2) !important;
|
||||
color: var(--input-text-color) !important;
|
||||
}
|
||||
.dark-mode .form-select[multiple] option:checked {
|
||||
background-color: rgba(105, 166, 255, 0.2) !important;
|
||||
color: var(--input-text-color) !important;
|
||||
}
|
||||
|
||||
/* --- TABLAS (Método Bootstrap) --- */
|
||||
.table {
|
||||
--bs-table-color: var(--table-color);
|
||||
@@ -198,3 +218,20 @@ table { font-size: 14px; }
|
||||
.dark-mode #theme-toggle {
|
||||
color: var(--navbar-color);
|
||||
}
|
||||
|
||||
/* Botones de checkboxes para meses */
|
||||
.dark-mode .btn-outline-primary {
|
||||
border-color: var(--input-border-color) !important;
|
||||
color: var(--input-text-color) !important;
|
||||
background-color: var(--input-bg-color) !important;
|
||||
}
|
||||
.dark-mode .btn-outline-primary:hover {
|
||||
background-color: var(--navbar-bg-color) !important;
|
||||
border-color: var(--navbar-bg-color) !important;
|
||||
color: var(--navbar-color) !important;
|
||||
}
|
||||
.dark-mode .btn-check:checked + .btn-outline-primary {
|
||||
background-color: var(--navbar-bg-color) !important;
|
||||
border-color: var(--navbar-bg-color) !important;
|
||||
color: var(--navbar-color) !important;
|
||||
}
|
||||
|
||||
208
dashboard.php
208
dashboard.php
@@ -94,7 +94,55 @@ switch ($page) {
|
||||
|
||||
case 'config_actions':
|
||||
header('Content-Type: application/json');
|
||||
Auth::requireAdmin();
|
||||
// Permitir acceso a capturistas para ver gráficos
|
||||
if (!Auth::isCapturist()) {
|
||||
echo json_encode(['success' => false, 'message' => 'Acceso denegado']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['action']) && $_GET['action'] == 'get_monthly_data') {
|
||||
$year = $_GET['year'] ?? date('Y');
|
||||
$monthlyBills = MonthlyBill::getYear($year);
|
||||
|
||||
$configured = [];
|
||||
$realAmounts = [];
|
||||
|
||||
$months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
|
||||
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
|
||||
|
||||
foreach ($months as $month) {
|
||||
$bill = $monthlyBills[$month] ?? null;
|
||||
$configured[$month] = $bill['total_amount'] ?? 0;
|
||||
$realAmounts[$month] = $bill['real_amount'] ?? 0;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'configured' => $configured,
|
||||
'realAmounts' => $realAmounts
|
||||
]);
|
||||
exit;
|
||||
|
||||
} elseif (isset($_GET['action']) && $_GET['action'] == 'get_payments_data') {
|
||||
$year = $_GET['year'] ?? date('Y');
|
||||
$matrix = Payment::getMatrix($year);
|
||||
$months = $matrix['months'];
|
||||
|
||||
$monthTotals = [];
|
||||
foreach ($months as $month) {
|
||||
$monthTotals[$month] = 0;
|
||||
foreach ($matrix['payments'][$month] ?? [] as $houseId => $paymentData) {
|
||||
$monthTotals[$month] += $paymentData['amount'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'payments' => $monthTotals,
|
||||
'months' => $months
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_GET['action']) && $_GET['action'] == 'save_monthly_bill') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
@@ -563,11 +611,27 @@ switch ($page) {
|
||||
Auth::requireAdmin();
|
||||
$view = 'users/index';
|
||||
break;
|
||||
case 'importar':
|
||||
Auth::requireAdmin();
|
||||
$concepts = CollectionConcept::all(true);
|
||||
$view = 'import/index';
|
||||
break;
|
||||
case 'graficos':
|
||||
$year = $_GET['year'] ?? date('Y');
|
||||
$matrix = Payment::getMatrix($year);
|
||||
$monthlyBills = MonthlyBill::getYear($year);
|
||||
$months = $matrix['months'];
|
||||
$monthTotals = [];
|
||||
|
||||
foreach ($months as $month) {
|
||||
$monthTotals[$month] = 0;
|
||||
foreach ($matrix['payments'][$month] ?? [] as $houseId => $paymentData) {
|
||||
$monthTotals[$month] += $paymentData['amount'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
$view = 'charts/index';
|
||||
break;
|
||||
case 'importar':
|
||||
Auth::requireAdmin();
|
||||
$concepts = CollectionConcept::all(true);
|
||||
$view = 'import/index';
|
||||
break;
|
||||
case 'concept_view_actions': // Nuevo case para acciones AJAX de concept_view
|
||||
if (isset($_GET['action'])) {
|
||||
header('Content-Type: application/json');
|
||||
@@ -634,9 +698,137 @@ switch ($page) {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'reportes_actions': // Nuevo case para acciones de exportación de reportes
|
||||
case 'charts_export':
|
||||
// Exportación de gráficos a PDF usando TCPDF (igual que otras exportaciones)
|
||||
date_default_timezone_set('America/Mexico_City');
|
||||
|
||||
// Obtener datos del POST
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$year = $input['year'] ?? date('Y');
|
||||
$chartImages = $input['charts'] ?? [];
|
||||
$chartData = $input['data'] ?? [];
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/vendor/tecnickcom/tcpdf/tcpdf.php';
|
||||
|
||||
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
|
||||
|
||||
$pdf->SetCreator(PDF_CREATOR);
|
||||
$pdf->SetAuthor('Ibiza Condominium');
|
||||
$pdf->SetTitle('Gráficos de Pagos de Agua - IBIZA ' . $year);
|
||||
$pdf->SetSubject('Análisis Gráfico de Pagos');
|
||||
|
||||
$pdf->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, 'Condominio IBIZA-Cto Sierra Morena 152 - Gráficos de Pagos de Agua ' . $year, 'Generado el ' . date('d/m/Y H:i'));
|
||||
|
||||
$pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN));
|
||||
$pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA));
|
||||
|
||||
$pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED);
|
||||
$pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT);
|
||||
$pdf->SetHeaderMargin(PDF_MARGIN_HEADER);
|
||||
$pdf->SetFooterMargin(PDF_MARGIN_FOOTER);
|
||||
$pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);
|
||||
$pdf->setImageScale(PDF_IMAGE_SCALE_RATIO);
|
||||
|
||||
if (@file_exists(dirname(__FILE__).'/lang/eng.php')) {
|
||||
require_once(dirname(__FILE__).'/lang/eng.php');
|
||||
$pdf->setLanguageArray($l);
|
||||
}
|
||||
|
||||
$pdf->SetFont('helvetica', '', 12);
|
||||
$pdf->AddPage();
|
||||
|
||||
// Título principal
|
||||
$pdf->SetFont('helvetica', 'B', 18);
|
||||
$pdf->Cell(0, 15, 'Gráficos de Pagos de Agua - IBIZA', 0, 1, 'C');
|
||||
$pdf->SetFont('helvetica', '', 10);
|
||||
$pdf->Cell(0, 8, 'Año: ' . $year . ' - Generado: ' . date('d/m/Y H:i'), 0, 1, 'C');
|
||||
$pdf->Ln(10);
|
||||
|
||||
// Función para agregar gráfico
|
||||
function addChartToPDF($pdf, $imageData, $title, $description = '') {
|
||||
if ($imageData) {
|
||||
$pdf->SetFont('helvetica', 'B', 14);
|
||||
$pdf->Cell(0, 10, $title, 0, 1, 'L');
|
||||
|
||||
if ($description) {
|
||||
$pdf->SetFont('helvetica', 'I', 10);
|
||||
$pdf->Cell(0, 6, $description, 0, 1, 'L');
|
||||
}
|
||||
|
||||
// Agregar imagen del gráfico
|
||||
$pdf->Image('@' . base64_decode(preg_replace('#^data:image/[^;]+;base64,#', '', $imageData)), 15, $pdf->GetY(), 180, 80, 'PNG');
|
||||
$pdf->Ln(90);
|
||||
}
|
||||
}
|
||||
|
||||
// Página 1: Gráficos principales
|
||||
$pdf->SetFont('helvetica', 'B', 16);
|
||||
$pdf->Cell(0, 12, 'Análisis Comparativo de Pagos', 0, 1, 'L');
|
||||
$pdf->Ln(5);
|
||||
|
||||
if (isset($chartImages['comparisonChart'])) {
|
||||
addChartToPDF($pdf, $chartImages['comparisonChart'],
|
||||
'1. Comparación: Pagos Reales vs Monto Configurado',
|
||||
'Este gráfico compara los pagos efectivamente recibidos con los montos que fueron configurados para cada mes.');
|
||||
}
|
||||
|
||||
if (isset($chartImages['trendsChart'])) {
|
||||
addChartToPDF($pdf, $chartImages['trendsChart'],
|
||||
'2. Tendencias de Pagos a lo Largo del Año',
|
||||
'Visualiza la evolución mensual de los ingresos por concepto de agua.');
|
||||
}
|
||||
|
||||
if (isset($chartImages['realAmountChart'])) {
|
||||
addChartToPDF($pdf, $chartImages['realAmountChart'],
|
||||
'3. Comparación: Monto Real del Recibo vs Pagos Efectivos',
|
||||
'Compara el costo real del servicio con lo que efectivamente se cobra y paga.');
|
||||
}
|
||||
|
||||
// Nueva página para resumen ejecutivo
|
||||
$pdf->AddPage();
|
||||
$pdf->SetFont('helvetica', 'B', 16);
|
||||
$pdf->Cell(0, 12, 'Resumen Ejecutivo', 0, 1, 'L');
|
||||
$pdf->Ln(5);
|
||||
|
||||
if (isset($chartImages['summaryChart'])) {
|
||||
addChartToPDF($pdf, $chartImages['summaryChart'],
|
||||
'Distribución General de Cobranza',
|
||||
'Vista panorámica del estado de cobranza con métricas clave.');
|
||||
}
|
||||
|
||||
// Estadísticas textuales
|
||||
$pdf->SetFont('helvetica', 'B', 12);
|
||||
$pdf->Cell(0, 10, 'Métricas de Rendimiento', 0, 1, 'L');
|
||||
$pdf->Ln(5);
|
||||
|
||||
$pdf->SetFont('helvetica', '', 11);
|
||||
$totalPayments = array_sum($chartData['payments'] ?? []);
|
||||
$totalConfigured = array_sum($chartData['configured'] ?? []);
|
||||
$totalReal = array_sum($chartData['realAmounts'] ?? []);
|
||||
|
||||
$pdf->Cell(80, 8, 'Total Pagos Recibidos:', 0, 0, 'L');
|
||||
$pdf->Cell(40, 8, '$ ' . number_format($totalPayments, 2), 0, 1, 'R');
|
||||
|
||||
$pdf->Cell(80, 8, 'Monto Total Configurado:', 0, 0, 'L');
|
||||
$pdf->Cell(40, 8, '$ ' . number_format($totalConfigured, 2), 0, 1, 'R');
|
||||
|
||||
$pdf->Cell(80, 8, 'Monto Real Total:', 0, 0, 'L');
|
||||
$pdf->Cell(40, 8, '$ ' . number_format($totalReal, 2), 0, 1, 'R');
|
||||
|
||||
if ($totalConfigured > 0) {
|
||||
$efficiency = round(($totalPayments / $totalConfigured) * 100, 1);
|
||||
$pdf->Cell(80, 8, 'Eficiencia de Cobranza:', 0, 0, 'L');
|
||||
$pdf->Cell(40, 8, $efficiency . '%', 0, 1, 'R');
|
||||
}
|
||||
|
||||
// Salida del PDF
|
||||
$pdf->Output('Graficos_Pagos_Agua_IBIZA_' . $year . '_' . date('Y-m-d') . '.pdf', 'D');
|
||||
exit;
|
||||
|
||||
case 'reportes_actions': // Nuevo case para acciones de exportación de reportes
|
||||
date_default_timezone_set('America/Mexico_City'); // Asegurar zona horaria
|
||||
|
||||
if (isset($_GET['action'])) {
|
||||
|
||||
3
database/migration_add_real_amount_to_monthly_bills.sql
Normal file
3
database/migration_add_real_amount_to_monthly_bills.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Migration to add real_amount to monthly_bills table
|
||||
ALTER TABLE `monthly_bills`
|
||||
ADD COLUMN `real_amount` DECIMAL(10, 2) NULL DEFAULT NULL AFTER `amount_per_house`;
|
||||
@@ -36,10 +36,11 @@ class MonthlyBill {
|
||||
|
||||
if ($existing) {
|
||||
$db->execute(
|
||||
"UPDATE monthly_bills SET total_amount = ?, amount_per_house = ?, due_date = ? WHERE id = ?",
|
||||
"UPDATE monthly_bills SET total_amount = ?, amount_per_house = ?, real_amount = ?, due_date = ? WHERE id = ?",
|
||||
[
|
||||
$data['total_amount'],
|
||||
$amountPerHouse,
|
||||
$data['real_amount'] ?? null,
|
||||
$data['due_date'] ?? null,
|
||||
$existing['id']
|
||||
]
|
||||
@@ -47,13 +48,14 @@ class MonthlyBill {
|
||||
return $existing['id'];
|
||||
} else {
|
||||
$db->execute(
|
||||
"INSERT INTO monthly_bills (year, month, total_amount, amount_per_house, due_date)
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
"INSERT INTO monthly_bills (year, month, total_amount, amount_per_house, real_amount, due_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
$data['year'],
|
||||
$data['month'],
|
||||
$data['total_amount'],
|
||||
$amountPerHouse,
|
||||
$data['real_amount'] ?? null,
|
||||
$data['due_date'] ?? null
|
||||
]
|
||||
);
|
||||
|
||||
885
views/charts/index.php
Normal file
885
views/charts/index.php
Normal file
@@ -0,0 +1,885 @@
|
||||
<?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>
|
||||
@@ -37,6 +37,11 @@
|
||||
<input type="number" step="0.01" class="form-control form-control-sm"
|
||||
id="per_house_<?= $month ?>" value="<?= $bill['amount_per_house'] ?? '' ?>">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Monto Real del Recibo</small>
|
||||
<input type="number" step="0.01" class="form-control form-control-sm"
|
||||
id="real_<?= $month ?>" value="<?= $bill['real_amount'] ?? '' ?>">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Fecha de Vencimiento</small>
|
||||
<input type="date" class="form-control form-control-sm"
|
||||
@@ -69,6 +74,7 @@ document.getElementById('yearSelect').addEventListener('change', function() {
|
||||
function saveMonth(month) {
|
||||
const total = document.getElementById('total_' + month).value;
|
||||
const perHouse = document.getElementById('per_house_' + month).value;
|
||||
const realAmount = document.getElementById('real_' + month).value;
|
||||
const dueDate = document.getElementById('due_' + month).value;
|
||||
const year = <?= $year ?>;
|
||||
|
||||
@@ -85,6 +91,7 @@ function saveMonth(month) {
|
||||
month: month,
|
||||
total_amount: parseFloat(total),
|
||||
amount_per_house: parseFloat(perHouse),
|
||||
real_amount: realAmount ? parseFloat(realAmount) : null,
|
||||
due_date: dueDate || null
|
||||
})
|
||||
})
|
||||
|
||||
@@ -53,11 +53,16 @@
|
||||
<i class="bi bi-cash-coin"></i> Finanzas
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $page == 'reportes' ? 'active' : '' ?>" href="/dashboard.php?page=reportes">
|
||||
<i class="bi bi-file-earmark-bar-graph"></i> Reportes
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $page == 'graficos' ? 'active' : '' ?>" href="/dashboard.php?page=graficos">
|
||||
<i class="bi bi-bar-chart-line-fill"></i> Gráficos
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $page == 'reportes' ? 'active' : '' ?>" href="/dashboard.php?page=reportes">
|
||||
<i class="bi bi-file-earmark-bar-graph"></i> Reportes
|
||||
</a>
|
||||
</li>
|
||||
<?php if (Auth::isAdmin()): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?= $page == 'importar' ? 'active' : '' ?>" href="/dashboard.php?page=importar">
|
||||
@@ -115,6 +120,7 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="<?= SITE_URL ?>/assets/js/theme.js"></script>
|
||||
<footer class="footer mt-auto py-3">
|
||||
<div class="container-fluid text-center">
|
||||
|
||||
Reference in New Issue
Block a user