Files

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>