Finalización del módulo Luz Cámara: Corrección de errores JS, exportación profesional a PDF y reportes de deudores

This commit is contained in:
2026-02-14 16:07:25 -06:00
parent 5f90790c7a
commit 9850f1a85e
13 changed files with 2849 additions and 536 deletions

600
views/electricity/index.php Executable file
View File

@@ -0,0 +1,600 @@
<?php
// views/electricity/index.php
$periods = ElectricityBill::getPeriods();
?>
<div class="row mb-4">
<div class="col-12">
<h2><i class="bi bi-lightbulb"></i> Pagos de Luz - Cámara</h2>
<p class="text-muted">Concentrado de pagos bimestrales por casa</p>
</div>
</div>
<div class="mb-4 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex gap-3 align-items-center flex-wrap">
<div>
<label for="yearSelect" class="form-label me-2">Año:</label>
<select id="yearSelect" class="form-select d-inline-block" style="width: auto;"
onchange="window.location.href='dashboard.php?page=luz_camara&year='+this.value">
<?php for ($y = 2024; $y <= date('Y') + 1; $y++): ?>
<option value="<?= $y?>" <?= $y == $year ? 'selected' : ''?>>
<?= $y?>
</option>
<?php
endfor; ?>
</select>
</div>
<div class="btn-group">
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
<i class="bi bi-download"></i> Exportar
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="exportPDF()">PDF</a></li>
<li><a class="dropdown-item" href="#" onclick="exportCSV()">CSV</a></li>
</ul>
</div>
</div>
<div>
<?php if (Auth::isCapturist()): ?>
<button onclick="saveChanges()" id="btnSaveTop" class="btn btn-warning position-relative" disabled>
<i class="bi bi-save"></i> Guardar Cambios
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"
id="changesBadge" style="display: none;">
0
<span class="visually-hidden">cambios pendientes</span>
</span>
</button>
<?php
endif; ?>
</div>
</div>
<!-- Configuración de Recibos Bimestrales -->
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title mb-3"><i class="bi bi-gear"></i> Configuración de Recibos (CFE)</h5>
<div class="row g-3">
<div class="col-md-3">
<label class="form-label small text-muted">Periodo</label>
<select id="configPeriod" class="form-select form-select-sm" onchange="loadConfig()">
<?php foreach ($periods as $period): ?>
<option value="<?= $period?>">
<?= $period?>
</option>
<?php
endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small text-muted">Total CFE</label>
<div class="input-group input-group-sm">
<span class="input-group-text">$</span>
<input type="number" id="configTotal" class="form-control" step="0.01" <?=!Auth::isCapturist()
? 'disabled' : ''?>>
</div>
</div>
<div class="col-md-2">
<label class="form-label small text-muted">Por Casa</label>
<div class="input-group input-group-sm">
<span class="input-group-text">$</span>
<input type="number" id="configPerHouse" class="form-control" step="0.01" <?=!Auth::isCapturist()
? 'disabled' : ''?>>
</div>
</div>
<div class="col-md-3">
<label class="form-label small text-muted">Notas</label>
<input type="text" id="configNotes" class="form-control form-control-sm" placeholder="Opcional..."
<?=!Auth::isCapturist() ? 'disabled' : ''?>>
</div>
<?php if (Auth::isCapturist()): ?>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-success btn-sm w-100" onclick="saveConfig()">
<i class="bi bi-check2"></i> Guardar
</button>
</div>
<?php
endif; ?>
</div>
</div>
</div>
<!-- Tabla de Pagos -->
<div class="card shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-bordered table-hover mb-0" id="electricityTable">
<thead class="table-dark text-center sticky-top">
<tr>
<th style="width: 80px;">Casa</th>
<th style="width: 100px;">Estado</th>
<?php foreach ($periods as $period):
$config = $electricityBills[$period] ?? [];
$amountPerHouse = $config['amount_per_house'] ?? 0;
?>
<th>
<?= $period?><br>
<small class="text-white" style="font-weight: normal;">$
<?= number_format($amountPerHouse, 2)?>
</small>
</th>
<?php
endforeach; ?>
<th>Total</th>
</tr>
</thead>
<tbody>
<?php
$periodTotals = array_fill_keys($periods, 0);
$grandTotal = 0;
foreach ($matrix['houses'] as $house):
// Filtrar visibilidad para usuarios normales
if (!Auth::canViewHouse($house['id']))
continue;
$houseTotal = 0;
?>
<tr data-house-id="<?= $house['id']?>" data-house-number="<?= $house['number']?>">
<td class="text-center fw-bold">
<?= $house['number']?>
</td>
<td class="text-center">
<span class="badge <?= $house['status'] == 'activa' ? 'paid' : 'inactive'?>">
<?= ucfirst($house['status'])?>
</span>
</td>
<?php foreach ($periods as $period):
$payment = $matrix['payments'][$period][$house['id']] ?? null;
$amount = $payment['amount'] ?? 0;
$periodTotals[$period] += $amount;
$houseTotal += $amount;
$config = $electricityBills[$period] ?? [];
$expected = $config['amount_per_house'] ?? 0;
$cellClass = '';
$cellText = '-';
if ($amount > 0) {
if ($expected > 0 && $amount >= $expected) {
$cellClass = 'paid'; // Paid
}
else {
$cellClass = 'partial'; // Partial
}
$cellText = '$' . number_format($amount, 2);
}
else {
if ($expected > 0) {
$cellClass = 'pending'; // Unpaid
$cellText = '-';
}
else {
$cellClass = ''; // No config
$cellText = '-';
}
// Si está deshabitada y no hay pago, gris
if ($house['status'] == 'deshabitada' && $amount == 0) {
$cellClass = 'inactive';
}
}
$isEditable = Auth::isCapturist() && $house['status'] == 'activa';
?>
<td class="payment-cell text-center <?= $cellClass?>" data-house-id="<?= $house['id']?>"
data-period="<?= $period?>" data-amount="<?= $amount?>" data-expected="<?= $expected?>"
<?= $isEditable ? 'contenteditable="true"' : ''?>>
<?= $cellText?>
</td>
<?php
endforeach; ?>
<td class="text-end fw-bold table-active">
$
<?= number_format($houseTotal, 2)?>
</td>
</tr>
<?php $grandTotal += $houseTotal;
endforeach; ?>
</tbody>
<tfoot class="fw-bold">
<tr>
<td colspan="2" class="text-end">Totales:</td>
<?php foreach ($periods as $period): ?>
<td class="text-center">$
<?= number_format($periodTotals[$period], 2)?>
</td>
<?php
endforeach; ?>
<td class="text-end">$
<?= number_format($grandTotal, 2)?>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<?php if (Auth::isCapturist()): ?>
<div class="d-flex justify-content-end mb-4 no-print mt-3">
<button onclick="saveChanges()" id="btnSaveBottom" class="btn btn-warning" disabled>
<i class="bi bi-save"></i> Guardar Cambios
</button>
</div>
<?php
endif; ?>
<div class="row mt-3 no-print">
<div class="col-md-6">
<div class="alert alert-info mb-0 py-2">
<strong><i class="bi bi-info-circle"></i> Instrucciones:</strong>
<ul class="mb-0 mt-1 list-inline">
<li class="list-inline-item"><span class="badge paid">Verde</span> = Pagado</li>
<li class="list-inline-item"><span class="badge partial">Amarillo</span> = Parcial</li>
<li class="list-inline-item"><span class="badge pending">Rojo</span> = Pendiente</li>
<li class="list-inline-item"><span class="badge inactive">Gris</span> = Inactivo/N.A.</li>
</ul>
</div>
</div>
</div>
<!-- Modal Export PDF -->
<div class="modal fade" id="exportPdfModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Exportar a PDF</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<h6>Selecciona los periodos a exportar:</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAllPeriods" checked>
<label class="form-check-label" for="selectAllPeriods">Todos</label>
</div>
<hr>
<div class="row">
<?php foreach ($periods as $i => $period): ?>
<div class="col-6">
<div class="form-check">
<input class="form-check-input period-checkbox" type="checkbox" value="<?= $period?>"
id="period_<?= $i?>" checked>
<label class="form-check-label" for="period_<?= $i?>">
<?= $period?>
</label>
</div>
</div>
<?php
endforeach; ?>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-success" onclick="generatePDF()">
<i class="bi bi-file-earmark-pdf"></i> Generar PDF
</button>
</div>
</div>
</div>
</div>
<script>
// Datos de configuración iniciales
const electricityConfig = <?= json_encode($electricityBills)?>;
let pendingChanges = {};
// Cargar configuración al cambiar select
function loadConfig() {
const period = document.getElementById('configPeriod').value;
const data = electricityConfig[period] || {};
document.getElementById('configTotal').value = data.total_amount || '';
document.getElementById('configPerHouse').value = data.amount_per_house || '';
document.getElementById('configNotes').value = data.notes || '';
}
// Inicializar configuración
document.addEventListener('DOMContentLoaded', function () {
loadConfig();
setupEditableCells();
// Listener para modal
document.getElementById('selectAllPeriods').addEventListener('change', function () {
document.querySelectorAll('.period-checkbox').forEach(cb => {
cb.checked = this.checked;
});
});
});
function setupEditableCells() {
document.querySelectorAll('.payment-cell[contenteditable="true"]').forEach(cell => {
cell.addEventListener('focus', function () {
this.classList.add('editing');
const text = this.textContent.trim();
// Limpiar si es guión, moneda o 0 al enfocar para facilitar edición
if (text === '-' || text.startsWith('$') || parseAmount(text) === 0) {
this.textContent = '';
}
});
cell.addEventListener('blur', function () {
this.classList.remove('editing');
let newText = this.textContent.trim();
let newVal = parseAmount(newText);
// Formatear display
if (newVal === 0) {
const expected = parseFloat(this.dataset.expected);
if (expected > 0) {
this.textContent = '-'; // Pendiente
} else {
this.textContent = '-'; // Nada
}
} else {
this.textContent = '$' + newVal.toFixed(2);
}
trackChange(this, newVal);
});
cell.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
this.blur();
}
});
});
}
function parseAmount(text) {
if (text === '-' || !text) return 0;
return parseFloat(text.replace(/[^0-9.-]+/g, '')) || 0;
}
<?php if (Auth::isCapturist()): ?>
// Guardar configuración
async function saveConfig() {
const period = document.getElementById('configPeriod').value;
const total = document.getElementById('configTotal').value;
const perHouse = document.getElementById('configPerHouse').value;
const notes = document.getElementById('configNotes').value;
try {
const response = await fetch('dashboard.php?page=luz_camara_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
year: <?= $year?>,
period: period,
total_amount: total,
amount_per_house: perHouse,
notes: notes
})
});
const result = await response.json();
if (result.success) {
// Actualizar objeto local
electricityConfig[period] = {
year: <?= $year?>,
period: period,
total_amount: total,
amount_per_house: perHouse,
notes: notes
};
alert('Configuración guardada exitosamente');
location.reload();
} else {
alert('Error al guardar: ' + result.message);
}
} catch (error) {
console.error('Error:', error);
alert('Error de conexión al guardar configuración');
}
}
// Tracking de cambios
function trackChange(cell, newVal) {
const houseId = cell.dataset.houseId;
const period = cell.dataset.period;
const key = `${houseId}_${period}`;
const original = parseFloat(cell.dataset.amount);
// Si el valor cambió respecto al original cargado
if (newVal !== original) {
pendingChanges[key] = {
house_id: houseId,
year: <?= $year?>,
period: period,
amount: newVal
};
cell.classList.add('table-info');
cell.style.fontWeight = 'bold';
} else {
delete pendingChanges[key];
cell.classList.remove('table-info');
cell.style.fontWeight = 'normal';
}
updateSaveButton();
}
function updateSaveButton() {
const count = Object.keys(pendingChanges).length;
const btnTop = document.getElementById('btnSaveTop');
const btnBottom = document.getElementById('btnSaveBottom');
const badge = document.getElementById('changesBadge');
if (btnTop) {
btnTop.disabled = count === 0;
// Duplicar comportamiento: mostrar texto con conteo
if (count > 0) {
btnTop.innerHTML = `<i class="bi bi-save"></i> Guardar ${count} Cambios`;
btnTop.classList.remove('btn-outline-secondary');
btnTop.classList.add('btn-warning');
} else {
btnTop.innerHTML = `<i class="bi bi-save"></i> Guardar Cambios`;
}
}
if (btnBottom) {
btnBottom.disabled = count === 0;
btnBottom.innerHTML = `<i class="bi bi-save"></i> Guardar ${count} Cambios`;
}
// Alerta de navegación si hay cambios
if (count > 0) {
window.onbeforeunload = () => "Tienes cambios sin guardar. ¿Seguro que quieres salir?";
} else {
window.onbeforeunload = null;
}
}
async function saveChanges() {
if (Object.keys(pendingChanges).length === 0) return;
const changes = Object.values(pendingChanges);
const btnTop = document.getElementById('btnSaveTop');
const btnBottom = document.getElementById('btnSaveBottom');
const originalTextTop = btnTop ? btnTop.innerHTML : '';
const originalTextBottom = btnBottom ? btnBottom.innerHTML : '';
if (btnTop) {
btnTop.disabled = true;
btnTop.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Guardando...';
}
if (btnBottom) {
btnBottom.disabled = true;
btnBottom.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Guardando...';
}
try {
const response = await fetch('dashboard.php?page=luz_camara_actions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ changes: changes })
});
const result = await response.json();
if (result.success) {
window.onbeforeunload = null;
alert('Cambios guardados exitosamente');
location.reload();
} else {
alert('Error al guardar: ' + (result.message || result.error));
if (btnTop) {
btnTop.disabled = false;
btnTop.innerHTML = originalTextTop;
}
if (btnBottom) {
btnBottom.disabled = false;
btnBottom.innerHTML = originalTextBottom;
}
}
} catch (error) {
console.error('Error:', error);
alert('Error de conexión');
if (btnTop) {
btnTop.disabled = false;
btnTop.innerHTML = originalTextTop;
}
if (btnBottom) {
btnBottom.disabled = false;
btnBottom.innerHTML = originalTextBottom;
}
}
}
<?php
endif; ?>
function exportPDF() {
const modal = new bootstrap.Modal(document.getElementById('exportPdfModal'));
modal.show();
}
function generatePDF() {
const checkboxes = document.querySelectorAll('.period-checkbox:checked');
const selectedPeriods = Array.from(checkboxes).map(cb => cb.value);
let url = 'dashboard.php?page=luz_camara&action=export_electricity_pdf&year=<?= $year?>';
selectedPeriods.forEach(period => {
url += `&periods[]=${encodeURIComponent(period)}`;
});
// Cerrar el modal antes de redirigir
const exportPdfModal = bootstrap.Modal.getInstance(document.getElementById('exportPdfModal'));
if (exportPdfModal) {
exportPdfModal.hide();
}
// Redirigir al usuario para iniciar la descarga del PDF
window.location.href = url;
}
function exportCSV() {
let csv = [];
const rows = document.querySelectorAll("#electricityTable tr");
for (const row of rows) {
const cols = row.querySelectorAll("td,th");
const rowData = [];
for (const col of cols) {
rowData.push('"' + col.innerText.replace(/(\r\n|\n|\r)/gm, " ").trim() + '"');
}
csv.push(rowData.join(","));
}
const csvFile = new Blob([csv.join("\n")], { type: "text/csv" });
const downloadLink = document.createElement("a");
downloadLink.download = "Pagos_Luz_Camara_<?= $year?>.csv";
downloadLink.href = window.URL.createObjectURL(csvFile);
downloadLink.style.display = "none";
document.body.appendChild(downloadLink);
downloadLink.click();
}
</script>
<style>
.payment-cell {
cursor: pointer;
}
.payment-cell:focus {
outline: 2px solid #0d6efd;
background-color: #fff !important;
color: #000;
z-index: 5;
position: relative;
}
@media print {
.btn-group,
#btnSaveTop,
#btnSaveBottom,
.card-header,
.form-label,
.input-group,
.no-print {
display: none !important;
}
.card {
border: none !important;
shadow: none !important;
}
.badge {
border: 1px solid #000;
color: #000 !important;
}
}
</style>

View File

@@ -0,0 +1,153 @@
<style>
table {
width: 100%;
border-collapse: collapse;
font-size: 8pt;
}
th,
td {
border: 1px solid #000;
padding: 4px;
text-align: center;
}
th {
background-color: #eee;
}
.print-title {
text-align: center;
font-size: 14pt;
margin-bottom: 10px;
}
.print-date {
text-align: right;
font-size: 8pt;
margin-bottom: 10px;
}
.text-danger {
color: red;
}
.text-success {
color: green;
}
</style>
<div class="print-title">Concentrado de Pagos de Luz - Cámara -
<?= $year?>
</div>
<div class="print-date">Fecha de generación:
<?= date('d/m/Y H:i')?>
</div>
<table>
<thead>
<tr>
<th width="10%">Casa</th>
<th width="10%">Estado</th>
<?php foreach ($periods as $period):
$config = $electricityBills[$period] ?? [];
$amountPerHouse = $config['amount_per_house'] ?? 0;
?>
<th>
<?= $period?><br><small>$
<?= number_format($amountPerHouse, 2)?>
</small>
</th>
<?php
endforeach; ?>
<th width="15%">Total</th>
</tr>
</thead>
<tbody>
<?php
$grandTotal = 0;
$periodTotals = array_fill_keys($periods, 0);
foreach ($matrix['houses'] as $house):
// Filtrar solo casas permitidas (aunque el controlador ya debió filtrar)
if (!Auth::canViewHouse($house['id']))
continue;
$houseTotal = 0;
?>
<tr>
<td><strong>
<?= $house['number']?>
</strong></td>
<td>
<?= $house['status'] == 'activa' ? 'Activa' : 'Deshabitada'?>
</td>
<?php foreach ($periods as $period):
$payment = $matrix['payments'][$period][$house['id']] ?? null;
$amount = $payment['amount'] ?? 0;
$periodTotals[$period] += $amount;
$houseTotal += $amount;
$config = $electricityBills[$period] ?? [];
$expected = $config['amount_per_house'] ?? 0;
$bg_color = '#FFFFFF';
// Lógica de colores idéntica a la vista web para consistencia
if ($amount > 0) {
if ($expected > 0 && $amount >= $expected) {
$bg_color = '#d4edda'; // Verde (paid)
}
else {
$bg_color = '#fff3cd'; // Amarillo (partial)
}
}
else {
if ($expected > 0) {
$bg_color = '#f8d7da'; // Rojo (pending)
}
elseif ($house['status'] == 'deshabitada') {
$bg_color = '#e2e3e5'; // Gris (inactive)
}
}
?>
<td style="background-color: <?= $bg_color?>;">
<?= $amount > 0 ? '$' . number_format($amount, 2) : '-'?>
</td>
<?php
endforeach; ?>
<td><strong>$
<?= number_format($houseTotal, 2)?>
</strong></td>
</tr>
<?php
$grandTotal += $houseTotal;
endforeach;
?>
<tr style="background-color: #bee5eb;">
<td colspan="2" style="text-align: right; font-weight: bold;">TOTALES:</td>
<?php foreach ($periods as $period): ?>
<td style="text-align: center; font-weight: bold;">
$
<?= number_format($periodTotals[$period], 2)?>
</td>
<?php
endforeach; ?>
<td style="text-align: center; font-weight: bold;">$
<?= number_format($grandTotal, 2)?>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 20px; font-size: 8pt; page-break-inside: avoid;">
<strong>Leyenda:</strong>
<span style="background-color: #d4edda; padding: 2px 8px; margin: 2px; border: 1px solid #ccc;">Verde =
Pagado</span>
<span style="background-color: #fff3cd; padding: 2px 8px; margin: 2px; border: 1px solid #ccc;">Amarillo =
Parcial</span>
<span style="background-color: #f8d7da; padding: 2px 8px; margin: 2px; border: 1px solid #ccc;">Rojo =
Pendiente</span>
<span style="background-color: #e2e3e5; padding: 2px 8px; margin: 2px; border: 1px solid #ccc;">Gris =
Inactivo</span>
</div>

View File

@@ -1,17 +1,19 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IBIZA CEA - Sistema de Gestión</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🏠</text></svg>">
<link href="<?= SITE_URL ?>/assets/css/theme.css" rel="stylesheet">
<link rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🏠</text></svg>">
<link href="<?= SITE_URL?>/assets/css/theme.css" rel="stylesheet">
<script>
// Prevenir FOUC (Flash of Unstyled Content)
(function() {
(function () {
const theme = localStorage.getItem('theme') || 'light';
if (theme === 'dark') {
document.documentElement.classList.add('dark-mode');
@@ -19,8 +21,9 @@
})();
</script>
</head>
<body>
<?php if (Auth::check()): ?>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary sticky-top">
@@ -34,42 +37,54 @@
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link <?= $page == 'dashboard' ? 'active' : '' ?>" href="/dashboard.php?page=dashboard">
<a class="nav-link <?= $page == 'dashboard' ? 'active' : ''?>"
href="/dashboard.php?page=dashboard">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $page == 'pagos' ? 'active' : '' ?>" href="/dashboard.php?page=pagos">
<a class="nav-link <?= $page == 'pagos' ? 'active' : ''?>" href="/dashboard.php?page=pagos">
<i class="bi bi-droplet-fill"></i> Pagos de Agua
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $page == 'casas' ? 'active' : '' ?>" href="/dashboard.php?page=casas">
<a class="nav-link <?= $page == 'luz_camara' ? 'active' : ''?>"
href="/dashboard.php?page=luz_camara">
<i class="bi bi-lightbulb-fill"></i> Luz Cámara
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $page == 'casas' ? 'active' : ''?>" href="/dashboard.php?page=casas">
<i class="bi bi-building"></i> Casas
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $page == 'finanzas' ? 'active' : '' ?>" href="/dashboard.php?page=finanzas">
<a class="nav-link <?= $page == 'finanzas' ? 'active' : ''?>"
href="/dashboard.php?page=finanzas">
<i class="bi bi-cash-coin"></i> Finanzas
</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>
<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">
<a class="nav-link <?= $page == 'importar' ? 'active' : ''?>"
href="/dashboard.php?page=importar">
<i class="bi bi-file-earmark-arrow-up"></i> Importar
</a>
</li>
<?php endif; ?>
<?php
endif; ?>
</ul>
<ul class="navbar-nav">
<li class="nav-item d-flex align-items-center">
@@ -78,55 +93,80 @@
</button>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i>
<?= htmlspecialchars(Auth::user()['first_name'] ?? 'Usuario') ?>
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i>
<?= htmlspecialchars(Auth::user()['first_name'] ?? 'Usuario')?>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<?php if (Auth::isAdmin()): ?>
<li><a class="dropdown-item" href="/dashboard.php?page=usuarios"><i class="bi bi-people"></i> Usuarios</a></li>
<li><a class="dropdown-item" href="/dashboard.php?page=configurar"><i class="bi bi-gear"></i> Configurar</a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="/dashboard.php?page=usuarios"><i
class="bi bi-people"></i> Usuarios</a></li>
<li><a class="dropdown-item" href="/dashboard.php?page=configurar"><i
class="bi bi-gear"></i> Configurar</a></li>
<li>
<hr class="dropdown-divider">
</li>
<?php
endif; ?>
<?php if (Auth::isAdmin()): ?>
<li><span class="dropdown-item text-muted small"><i class="bi bi-server"></i> <?= DB_HOST ?>:<?= DB_PORT ?></span></li>
<li><span class="dropdown-item text-muted small"><i class="bi bi-database"></i> DB: <?= DB_NAME ?></span></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<li><a class="dropdown-item" href="/dashboard.php?page=profile"><i class="bi bi-person"></i> Perfil</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="/logout.php"><i class="bi bi-box-arrow-right"></i> Cerrar Sesión</a></li>
<li><span class="dropdown-item text-muted small"><i class="bi bi-server"></i>
<?= DB_HOST?>:
<?= DB_PORT?>
</span></li>
<li><span class="dropdown-item text-muted small"><i class="bi bi-database"></i> DB:
<?= DB_NAME?>
</span></li>
<li>
<hr class="dropdown-divider">
</li>
<?php
endif; ?>
<li><a class="dropdown-item" href="/dashboard.php?page=profile"><i class="bi bi-person"></i>
Perfil</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item text-danger" href="/logout.php"><i
class="bi bi-box-arrow-right"></i> Cerrar Sesión</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<?php endif; ?>
<?php
endif; ?>
<div class="container-fluid py-4">
<?php
$viewPath = __DIR__ . '/../' . $view . '.php';
if (isset($view) && file_exists($viewPath)):
?>
<?php include $viewPath; ?>
<?php else: ?>
<div class="alert alert-danger">
Vista no encontrada: <?= htmlspecialchars($view ?? '') ?><br>
Ruta: <?= htmlspecialchars($viewPath ?? '') ?><br>
Existe: <?= isset($view) && file_exists($viewPath) ? 'Sí' : 'No' ?>
</div>
<?php endif; ?>
<?php
$viewPath = __DIR__ . '/../' . $view . '.php';
if (isset($view) && file_exists($viewPath)):
?>
<?php include $viewPath; ?>
<?php
else: ?>
<div class="alert alert-danger">
Vista no encontrada:
<?= htmlspecialchars($view ?? '')?><br>
Ruta:
<?= htmlspecialchars($viewPath ?? '')?><br>
Existe:
<?= isset($view) && file_exists($viewPath) ? 'Sí' : 'No'?>
</div>
<?php
endif; ?>
</div>
<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>
<script src="<?= SITE_URL?>/assets/js/theme.js"></script>
<footer class="footer mt-auto py-3">
<div class="container-fluid text-center">
<span class="text-muted">Condominio IBIZA - Derechos reservados Miguel Pons casa 11</span>
</div>
</footer>
</body>
</html>
</html>

View File

@@ -19,6 +19,9 @@
<a href="/dashboard.php?page=reportes&type=concepts" class="btn btn-outline-info <?= ($_GET['type'] ?? '') == 'concepts' ? 'active' : '' ?>">
<i class="bi bi-collection"></i> Conceptos Especiales
</a>
<a href="/dashboard.php?page=reportes&type=electricity-debtors" class="btn btn-outline-warning <?= ($_GET['type'] ?? '') == 'electricity-debtors' ? 'active' : '' ?>">
<i class="bi bi-lightbulb-fill"></i> Deudores Luz
</a>
</div>
</div>
@@ -745,6 +748,233 @@ function exportConceptsCSV() {
}
</script>
</script>
<?php elseif ($reportType == 'electricity-debtors' && isset($electricityDebtors)): ?>
<?php
$hasFilters = !empty($electricityDebtors['filters']['year']) || !empty($electricityDebtors['filters']['periods']) || !empty($electricityDebtors['filters']['house_id']);
$filterText = [];
if (!empty($electricityDebtors['filters']['year'])) {
$filterText[] = "Año: " . $electricityDebtors['filters']['year'];
}
if (!empty($electricityDebtors['filters']['periods'])) {
$filterText[] = "Periodos: " . implode(', ', $electricityDebtors['filters']['periods']);
}
if (!empty($electricityDebtors['filters']['house_id'])) {
require_once __DIR__ . '/../../models/House.php';
$house = House::findById($electricityDebtors['filters']['house_id']);
$filterText[] = "Casa: " . ($house['number'] ?? 'N/A');
}
?>
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="bi bi-funnel"></i> Filtros de Deudores de Luz
<?php if ($hasFilters): ?>
<span class="badge bg-warning text-dark ms-2"><?= implode(' | ', $filterText) ?></span>
<?php endif; ?>
</h5>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="collapse" data-bs-target="#filtersCollapse">
<i class="bi bi-chevron-down"></i>
</button>
</div>
<div class="collapse <?php echo $hasFilters ? '' : 'show'; ?>" id="filtersCollapse">
<div class="card-body">
<form id="electricityDebtorsFilter">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Año</label>
<select name="filter_year" class="form-select">
<option value="">Todos los años</option>
<?php for ($y = 2024; $y <= date('Y') + 1; $y++): ?>
<option value="<?= $y ?>" <?= ($_GET['filter_year'] ?? '') == $y ? 'selected' : '' ?>><?= $y ?></option>
<?php endfor; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Casa</label>
<select name="filter_house" class="form-select">
<option value="">Todas las casas</option>
<?php
require_once __DIR__ . '/../../models/House.php';
$allHouses = House::getAccessible();
foreach ($allHouses as $h): ?>
<option value="<?= $h['id'] ?>" <?= ($_GET['filter_house'] ?? '') == $h['id'] ? 'selected' : '' ?>><?= $h['number'] ?> - <?= htmlspecialchars($h['owner_name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Periodos</label>
<div class="d-flex flex-wrap gap-2">
<?php
require_once __DIR__ . '/../../models/ElectricityBill.php';
$allPeriods = ElectricityBill::getPeriods();
$selectedPeriods = explode(',', $_GET['filter_periods'] ?? '');
foreach ($allPeriods as $p): ?>
<div class="form-check">
<input type="checkbox" name="filter_periods[]" value="<?= $p ?>"
class="form-check-input period-checkbox"
id="period_<?= str_replace(' ', '', $p) ?>"
<?= in_array($p, $selectedPeriods) ? 'checked' : '' ?>>
<label class="form-check-label" for="period_<?= str_replace(' ', '', $p) ?>"><?= $p ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<button type="submit" class="btn btn-primary">
<i class="bi bi-search"></i> Aplicar Filtros
</button>
<a href="/dashboard.php?page=reportes&type=electricity-debtors" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Limpiar Filtros
</a>
</div>
</div>
</form>
</div>
</div>
</div>
<script>
document.getElementById('electricityDebtorsFilter').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const params = new URLSearchParams();
if (formData.get('filter_year')) {
params.append('filter_year', formData.get('filter_year'));
}
if (formData.get('filter_house')) {
params.append('filter_house', formData.get('filter_house'));
}
const selectedPeriods = formData.getAll('filter_periods[]');
if (selectedPeriods.length > 0) {
params.append('filter_periods', selectedPeriods.join(','));
}
window.location.href = '/dashboard.php?page=reportes&type=electricity-debtors&' + params.toString();
});
</script>
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card border-warning">
<div class="card-body">
<h6 class="text-muted">Total Adeudado (Luz)</h6>
<h3 class="text-warning">$<?= number_format($electricityDebtors['total_due'], 2) ?></h3>
<small class="text-muted">Total general de deudas</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-info">
<div class="card-body">
<h6 class="text-muted">Total Esperado</h6>
<h3 class="text-info">$<?= number_format($electricityDebtors['total_expected'], 2) ?></h3>
<small class="text-muted">Total configurado</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-success">
<div class="card-body">
<h6 class="text-muted">Total Pagado</h6>
<h3 class="text-success">$<?= number_format($electricityDebtors['total_paid'], 2) ?></h3>
<small class="text-muted">Total recaudado</small>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0"><i class="bi bi-exclamation-triangle"></i> Deudores de Luz - Cámara</h5>
<button onclick="exportElectricityDebtorsPDF()" class="btn btn-outline-warning btn-sm">
<i class="bi bi-file-earmark-pdf"></i> Exportar PDF
</button>
</div>
<div class="card-body">
<?php if (empty($electricityDebtors['debtors'])): ?>
<p class="text-muted">No hay deudores registrados</p>
<?php else: ?>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="table-warning">
<tr>
<th>Casa</th>
<th>Propietario</th>
<th>Periodos Adeudados</th>
<th>Total Debe</th>
</tr>
</thead>
<tbody>
<?php foreach ($electricityDebtors['debtors'] as $debtor): ?>
<tr>
<td><strong><?= $debtor['house_number'] ?></strong></td>
<td><?= htmlspecialchars($debtor['owner_name'] ?? '-') ?></td>
<td>
<table class="table table-sm mb-0">
<?php foreach ($debtor['periods_due'] as $period): ?>
<tr>
<td><?= $period['year'] ?> - <?= $period['period'] ?></td>
<td class="text-end">$<?= number_format($period['due'], 2) ?></td>
</tr>
<?php endforeach; ?>
</table>
</td>
<td class="text-end fw-bold text-danger">$<?= number_format($debtor['total_due'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="table-dark">
<tr>
<th colspan="3" class="text-end">TOTAL GENERAL:</th>
<th class="text-end">$<?= number_format($electricityDebtors['total_due'], 2) ?></th>
</tr>
</tfoot>
</table>
</div>
<?php endif; ?>
</div>
</div>
<script>
function exportElectricityDebtorsPDF() {
const form = document.getElementById('electricityDebtorsFilter');
const params = new URLSearchParams();
params.append('action', 'export_pdf_report');
params.append('type', 'electricity-debtors');
if (form) {
const formData = new FormData(form);
if (formData.get('filter_year')) {
params.append('filter_year', formData.get('filter_year'));
}
if (formData.get('filter_house')) {
params.append('filter_house', formData.get('filter_house'));
}
const selectedPeriods = formData.getAll('filter_periods[]');
if (selectedPeriods.length > 0) {
params.append('filter_periods', selectedPeriods.join(','));
}
} else {
// Fallback si no hay formulario (por ejemplo si se ocultó), usar parámetros de URL actual
const currentParams = new URLSearchParams(window.location.search);
if (currentParams.get('filter_year')) params.append('filter_year', currentParams.get('filter_year'));
if (currentParams.get('filter_house')) params.append('filter_house', currentParams.get('filter_house'));
if (currentParams.get('filter_periods')) params.append('filter_periods', currentParams.get('filter_periods'));
}
window.open('/dashboard.php?page=reportes_actions&' + params.toString(), '_blank');
}
</script>
<?php else: ?>
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">

View File

@@ -0,0 +1,123 @@
<style>
table {
width: 100%;
border-collapse: collapse;
font-size: 8pt;
}
th,
td {
border: 1px solid #000;
padding: 4px;
text-align: center;
}
th {
background-color: #eee;
}
.print-title {
text-align: center;
font-size: 14pt;
margin-bottom: 10px;
}
.print-date {
text-align: right;
font-size: 8pt;
margin-bottom: 10px;
}
.text-danger {
color: red;
}
.text-warning {
color: orange;
}
</style>
<div class="print-title">Condominio IBIZA-Cto Sierra Morena 152 - Reporte de Deudores de Luz - Energía Cámara</div>
<div class="print-date">Fecha de generación:
<?= date('d/m/Y H:i')?>
</div>
<?php
$hasFilters = !empty($electricityDebtors['filters']['year']) || !empty($electricityDebtors['filters']['periods']) || !empty($electricityDebtors['filters']['house_id']);
$filterText = [];
if (!empty($electricityDebtors['filters']['year'])) {
$filterText[] = "Año: " . $electricityDebtors['filters']['year'];
}
if (!empty($electricityDebtors['filters']['periods'])) {
$filterText[] = "Periodos: " . implode(', ', $electricityDebtors['filters']['periods']);
}
if (!empty($electricityDebtors['filters']['house_id'])) {
require_once __DIR__ . '/../../models/House.php';
$house = House::findById($electricityDebtors['filters']['house_id']);
$filterText[] = "Casa: " . ($house['number'] ?? 'N/A');
}
if ($hasFilters):
?>
<div style="font-size: 9pt; margin-bottom: 10px;">
<strong>Filtros aplicados:</strong>
<?= implode(' | ', $filterText)?>
</div>
<?php
endif; ?>
<?php if (empty($electricityDebtors['debtors'])): ?>
<p>No hay deudores de luz registrados con los filtros actuales.</p>
<?php
else: ?>
<table>
<thead>
<tr style="background-color: #ffc107;">
<th>Casa</th>
<th>Propietario</th>
<th>Periodos Adeudados</th>
<th>Total Debe</th>
</tr>
</thead>
<tbody>
<?php foreach ($electricityDebtors['debtors'] as $debtor): ?>
<tr>
<td><strong>
<?= $debtor['house_number']?>
</strong></td>
<td>
<?= htmlspecialchars($debtor['owner_name'] ?? '-')?>
</td>
<td>
<table style="width:100%; border: none;">
<?php foreach ($debtor['periods_due'] as $period): ?>
<tr>
<td style="border: none; text-align: left;">
<?= $period['year']?> -
<?= $period['period']?>
</td>
<td style="border: none; text-align: right;">$
<?= number_format($period['due'], 2)?>
</td>
</tr>
<?php
endforeach; ?>
</table>
</td>
<td class="text-end fw-bold text-danger">$
<?= number_format($debtor['total_due'], 2)?>
</td>
</tr>
<?php
endforeach; ?>
</tbody>
<tfoot>
<tr style="background-color: #343a40; color: #fff;">
<th colspan="3" style="text-align: right;">TOTAL GENERAL:</th>
<th style="text-align: right;">$
<?= number_format($electricityDebtors['total_due'], 2)?>
</th>
</tr>
</tfoot>
</table>
<?php
endif; ?>