600 lines
22 KiB
PHP
Executable File
600 lines
22 KiB
PHP
Executable File
<?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>
|