Files
ibiza_sistema/views/payments/index.php
nickpons666 8f2f04951f fix: Corregir cálculo de excedente para casas con consumo_only
- Agregar método getExpectedAmountWithDiscount() que retorna el monto sin descuento de 00
- El excedente ahora se calcula contra el monto original configurado, no contra el monto con descuento
- Casas que pagan exactamente el monto por casa aparecen al corriente (/bin/bash.00)
- Casas que pagan más del monto por casa muestran excedente
2026-01-16 17:18:18 -06:00

592 lines
24 KiB
PHP
Executable File

<div class="row mb-4">
<div class="col-12">
<h2><i class="bi bi-droplet-fill"></i> Pagos de Agua</h2>
<p class="text-muted">Concentrado de pagos mensuales 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;">
<?php for ($y = 2024; $y <= 2030; $y++): ?>
<option value="<?= $y ?>" <?= $y == $year ? 'selected' : '' ?>><?= $y ?></option>
<?php endfor; ?>
</select>
</div>
<div>
<label for="houseFilter" class="form-label me-2">Casa:</label>
<select id="houseFilter" class="form-select d-inline-block" style="width: auto;">
<option value="">Todas</option>
<?php foreach ($houses as $house): ?>
<option value="<?= $house['id'] ?>" data-number="<?= $house['number'] ?>"><?= $house['number'] ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div>
<button onclick="exportToPDF()" class="btn btn-success">
<i class="bi bi-file-earmark-pdf"></i> Exportar PDF
</button>
<?php if (Auth::isAdmin()): ?>
<button onclick="exportToCSV()" class="btn btn-primary">
<i class="bi bi-file-earmark-csv"></i> Exportar CSV
</button>
<?php endif; ?>
<?php if (Auth::isCapturist()): ?>
<button onclick="saveAllChanges()" 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>
<div class="table-responsive no-print">
<table class="table table-bordered table-hover table-sm" id="paymentsTable">
<thead class="table-dark">
<tr>
<th>Casa</th>
<th>Estado</th>
<?php foreach ($months as $month): ?>
<th><?= substr($month, 0, 3) ?></th>
<?php endforeach; ?>
<th>Debe/Excedente</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<?php
$grandTotal = 0;
$grandTotalExpected = 0;
$monthTotals = array_fill_keys($months, 0);
foreach ($houses as $house):
$total = 0;
$totalExpected = 0;
$totalExpectedOriginal = 0;
?>
<tr data-house-id="<?= $house['id'] ?>" data-house-number="<?= $house['number'] ?>" data-status="<?= $house['status'] ?>">
<td><strong><?= $house['number'] ?></strong></td>
<td>
<span class="badge <?= $house['status'] == 'activa' ? 'bg-success' : 'bg-secondary' ?>">
<?= $house['status'] == 'activa' ? 'Activa' : 'Deshabitada' ?>
</span>
<?php if ($house['consumption_only']): ?>
<span class="badge bg-warning" title="Solo consumo">CO</span>
<?php endif; ?>
</td>
<?php foreach ($months as $month):
$payment = $payments[$month][$house['id']] ?? null;
$amount = $payment['amount'] ?? 0;
$monthTotals[$month] += $amount; // Accumulate monthly total
$expected = Payment::getExpectedAmount($house, $year, $month);
$expectedOriginal = Payment::getExpectedAmountWithDiscount($house, $year, $month);
$total += $amount;
$totalExpected += $expected;
$totalExpectedOriginal += $expectedOriginal;
$cellClass = 'pending';
$cellText = '-';
if ($house['status'] == 'deshabitada') {
$cellClass = 'inactive';
$cellText = '-';
} elseif ($amount > 0) {
if ($expected > 0 && $amount >= $expected) {
$cellClass = 'paid';
} else {
$cellClass = 'partial';
}
$cellText = '$' . number_format($amount, 2);
} elseif ($amount == 0) {
$cellClass = 'pending';
if ($expected == 0) {
$cellText = 'Sin monto';
} else {
$cellText = '-';
}
}
$isEditable = Auth::isCapturist() && $house['status'] == 'activa';
?>
<td class="payment-cell text-center <?= $cellClass ?>"
data-house-id="<?= $house['id'] ?>"
data-month="<?= $month ?>"
data-amount="<?= $amount ?>"
data-expected="<?= $expected ?>"
data-status="<?= $house['status'] ?>"
data-is-capturist="<?= Auth::isCapturist() ? '1' : '0' ?>"
<?= $isEditable ? 'contenteditable="true"' : '' ?>>
<?= $cellText ?>
</td>
<?php endforeach; ?>
<?php
$difference = $total - $totalExpectedOriginal;
$diffClass = $difference < 0 ? 'text-danger' : 'text-success';
$diffText = $difference == 0 ? '$0.00' : '$' . number_format($difference, 2);
$grandTotal += $total;
$grandTotalExpected += $totalExpected;
?>
<td class="text-end fw-bold <?= $diffClass ?>"><?= $diffText ?></td>
<td class="text-end fw-bold">$<?= number_format($total, 2) ?></td>
</tr>
<?php endforeach; ?>
<tr class="table-info">
<td colspan="2" class="text-end fw-bold">SUMA MENSUAL:</td>
<?php foreach ($months as $month): ?>
<td class="text-center fw-bold">
$<?= number_format($monthTotals[$month], 2) ?>
</td>
<?php endforeach; ?>
<td colspan="2"></td>
</tr>
<?php
$grandDifference = $grandTotal - $grandTotalExpected;
$grandDiffClass = $grandDifference < 0 ? 'text-danger' : 'text-success';
$grandDiffText = $grandDifference == 0 ? '$0.00' : '$' . number_format($grandDifference, 2);
?>
<tr class="table-primary">
<td colspan="<?= count($months) + 2 ?>" class="text-end fw-bold">TOTALES:</td>
<td class="text-end fw-bold <?= $grandDiffClass ?>"><?= $grandDiffText ?></td>
<td class="text-end fw-bold">$<?= number_format($grandTotal, 2) ?></td>
</tr>
</tbody>
</table>
</div>
<?php if (Auth::isCapturist()): ?>
<div class="d-flex justify-content-end mb-4 no-print mt-3">
<button onclick="saveAllChanges()" 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">
<strong><i class="bi bi-info-circle"></i> Instrucciones:</strong>
<ul class="mb-0 mt-2">
<li><span class="badge bg-success">Verde</span> = Pagado completo</li>
<li><span class="badge bg-warning">Amarillo</span> = Pago parcial o Sin monto configurado</li>
<li><span class="badge bg-danger">Rojo</span> = Sin pago</li>
<li><span class="badge bg-secondary">Gris</span> = Casa deshabitada</li>
</ul>
</div>
</div>
</div>
<?php if (Auth::isAdmin()): ?>
<div id="printArea">
<div class="print-title">Concentrado de Pagos de Agua - <?= $year ?></div>
<div class="print-date">Fecha de generación: <?= date('d/m/Y H:i') ?></div>
<table>
<thead>
<tr>
<th>Casa</th>
<th>Estado</th>
<?php foreach ($months as $month): ?>
<th><?= $month ?></th>
<?php endforeach; ?>
<th>Debe/Excedente</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<?php foreach ($houses as $house):
$total = 0;
$totalExpected = 0;
$totalExpectedOriginal = 0;
?>
<tr>
<td><strong><?= $house['number'] ?></strong></td>
<td><?= $house['status'] == 'activa' ? 'Activa' : 'Deshabitada' ?></td>
<?php foreach ($months as $month):
$payment = $payments[$month][$house['id']] ?? null;
$amount = $payment['amount'] ?? 0;
$expected = Payment::getExpectedAmount($house, $year, $month);
$expectedOriginal = Payment::getExpectedAmountWithDiscount($house, $year, $month);
$total += $amount;
$totalExpected += $expected;
$totalExpectedOriginal += $expectedOriginal;
$bg = '#f8d7da';
if ($amount > 0) {
$bg = $amount >= $expected ? '#d4edda' : '#fff3cd';
} else {
$bg = $house['status'] == 'deshabitada' ? '#e2e3e5' : '#f8d7da';
}
?>
<td style="background-color: <?= $bg ?>;">
<?= $amount > 0 ? '$' . number_format($amount, 2) : '-' ?>
</td>
<?php endforeach; ?>
<?php
$difference = $total - $totalExpectedOriginal;
$diffColor = $difference < 0 ? 'red' : 'green';
$diffText = $difference == 0 ? '$0.00' : '$' . number_format($difference, 2);
?>
<td style="color: <?= $diffColor ?>;"><?= $diffText ?></td>
<td><strong>$<?= number_format($total, 2) ?></strong></td>
</tr>
<?php endforeach; ?>
<tr style="background-color: #bee5eb;">
<td colspan="2" style="text-align: right; font-weight: bold;">SUMA MENSUAL:</td>
<?php foreach ($months as $month): ?>
<td style="text-align: center; font-weight: bold;">
$<?= number_format($monthTotals[$month], 2) ?>
</td>
<?php endforeach; ?>
<td colspan="2"></td>
</tr>
</tbody>
</table>
<div style="margin-top: 20px; font-size: 10px; page-break-inside: avoid;">
<strong>Leyenda:</strong>
<span style="background-color: #d4edda; padding: 2px 8px; margin: 2px;">Verde = Pagado completo</span>
<span style="background-color: #fff3cd; padding: 2px 8px; margin: 2px;">Amarillo = Pago parcial</span>
<span style="background-color: #f8d7da; padding: 2px 8px; margin: 2px;">Rojo = Sin pago</span>
<span style="background-color: #e2e3e5; padding: 2px 8px; margin: 2px;">Gris = Casa deshabitada</span>
</div>
</div>
<?php endif; ?>
<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 meses a exportar:</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAllMonths" checked>
<label class="form-check-label" for="selectAllMonths">Todos</label>
</div>
<hr>
<div class="row">
<?php
$monthList = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
foreach ($monthList as $i => $m):
if ($i % 2 == 0) echo '<div class="col-6">';
?>
<div class="form-check">
<input class="form-check-input month-checkbox" type="checkbox" value="<?= $m ?>" id="month_<?= $i ?>" checked>
<label class="form-check-label" for="month_<?= $i ?>"><?= $m ?></label>
</div>
<?php
if ($i % 2 == 1 || $i == count($monthList) - 1) echo '</div>';
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>
const monthIndex = {
'Enero': 0, 'Febrero': 1, 'Marzo': 2, 'Abril': 3, 'Mayo': 4, 'Junio': 5,
'Julio': 6, 'Agosto': 7, 'Septiembre': 8, 'Octubre': 9, 'Noviembre': 10, 'Diciembre': 11
};
document.getElementById('yearSelect').addEventListener('change', function() {
window.location.href = '/dashboard.php?page=pagos&year=' + this.value;
});
document.getElementById('houseFilter').addEventListener('change', function() {
const houseId = this.value;
const rows = document.querySelectorAll('#paymentsTable tbody tr');
rows.forEach(row => {
if (!houseId) {
row.style.display = '';
} else {
if (row.dataset.houseId == houseId) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
});
});
document.getElementById('selectAllMonths').addEventListener('change', function() {
document.querySelectorAll('.month-checkbox').forEach(cb => {
cb.checked = this.checked;
});
});
document.querySelectorAll('.payment-cell[contenteditable="true"]').forEach(cell => {
cell.addEventListener('focus', function() {
this.classList.add('editing');
const text = this.textContent.trim();
if (text === '-' || text === 'Sin monto' || text.startsWith('$')) {
this.textContent = '';
}
});
cell.addEventListener('click', function() {
const text = this.textContent.trim();
if (text === '-' || text === 'Sin monto' || text.startsWith('$')) {
this.textContent = '';
}
});
cell.addEventListener('blur', function() {
this.classList.remove('editing');
const originalText = this.getAttribute('data-original-text') || '';
let newText = this.textContent.trim();
// Format empty/special values logic
if (newText === '') {
const expected = parseFloat(this.dataset.expected);
if (expected > 0) {
newText = '0';
this.textContent = '0'; // Display 0 for better feedback
} else {
newText = '-';
this.textContent = '-';
}
}
// Check if value actually changed
// We compare sanitized values to avoid false positives with formatting
const originalVal = parseAmount(originalText);
const newVal = parseAmount(newText);
// If content changed
if (originalVal !== newVal) {
trackChange(this, newVal);
}
});
cell.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
this.blur();
}
});
// Store original value on load/focus if not present
if (!cell.hasAttribute('data-original-text')) {
cell.setAttribute('data-original-text', cell.textContent.trim());
}
});
let pendingChanges = new Map();
function parseAmount(text) {
if (text === '-' || text === 'Sin monto' || !text) return 0;
return parseFloat(text.replace(/[^0-9.-]+/g, '')) || 0;
}
function trackChange(cell, newAmount) {
const houseId = cell.dataset.houseId;
const houseNumber = cell.closest('tr').dataset.houseNumber;
const month = cell.dataset.month;
const key = `${houseId}-${month}`;
// Mark cell visually
cell.classList.add('table-warning', 'border-warning');
// Store change
pendingChanges.set(key, {
house_id: houseId,
house_number: houseNumber,
year: <?= $year ?>,
month: month,
amount: newAmount
});
updateSaveButtons();
}
function updateSaveButtons() {
const count = pendingChanges.size;
const btnTop = document.getElementById('btnSaveTop');
const btnBottom = document.getElementById('btnSaveBottom');
const badge = document.getElementById('changesBadge');
if (btnTop) {
btnTop.disabled = count === 0;
if (count > 0) {
badge.style.display = 'block';
badge.textContent = count;
// Prevent leaving page warning could be added here
window.onbeforeunload = () => "Tienes cambios sin guardar. ¿Seguro que quieres salir?";
} else {
badge.style.display = 'none';
window.onbeforeunload = null;
}
}
if (btnBottom) {
btnBottom.disabled = count === 0;
btnBottom.innerHTML = `<i class="bi bi-save"></i> Guardar ${count} Cambios`;
}
}
function saveAllChanges() {
if (pendingChanges.size === 0) return;
Swal.fire({
title: '¿Guardar cambios?',
text: `Se guardarán ${pendingChanges.size} cambios en los pagos.`,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Sí, guardar',
cancelButtonText: 'Cancelar'
}).then((result) => {
if (result.isConfirmed) {
const changes = Array.from(pendingChanges.values());
const btnTop = document.getElementById('btnSaveTop');
// Show loading state
if (btnTop) {
const originalText = btnTop.innerHTML;
btnTop.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Guardando...';
btnTop.disabled = true;
}
fetch('/dashboard.php?page=pagos_actions&action=save_batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ changes: changes })
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.onbeforeunload = null; // Remove warning
Swal.fire('Guardado', data.message, 'success').then(() => {
location.reload();
});
} else {
Swal.fire('Error', data.message || 'Error al guardar', 'error');
if (btnTop) {
btnTop.disabled = false;
updateSaveButtons(); // Restore button state
}
}
})
.catch(error => {
Swal.fire('Error', 'Error de conexión', 'error');
if (btnTop) {
btnTop.disabled = false;
updateSaveButtons();
}
});
}
});
}
function exportToPDF() {
const modal = new bootstrap.Modal(document.getElementById('exportPdfModal'));
modal.show();
}
function generatePDF() {
const checkboxes = document.querySelectorAll('.month-checkbox:checked');
const selectedMonths = Array.from(checkboxes).map(cb => cb.value);
let url = '/dashboard.php?page=pagos&action=export_pdf&year=<?= $year ?>';
selectedMonths.forEach(month => {
url += `&months[]=${encodeURIComponent(month)}`;
});
// 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 exportToCSV() {
const table = document.getElementById('paymentsTable');
const rows = table.querySelectorAll('tr');
let csv = [];
const headerRow = table.querySelector('thead tr');
const headers = headerRow ? headerRow.querySelectorAll('th') : [];
const numCols = headers.length;
rows.forEach(row => {
const isTotalRow = row.classList.contains('table-primary');
const cols = row.querySelectorAll('th, td');
let rowData = [];
if (isTotalRow) {
for (let i = 0; i < numCols - 2; i++) {
rowData.push('');
}
const textDiff = cols[1].textContent.trim().replace(/[\$,]/g, '');
const textTotal = cols[2].textContent.trim().replace(/[\$,]/g, '');
rowData.push(textDiff);
rowData.push(textTotal);
} else {
cols.forEach(col => {
let cellValue = '';
if (col.classList.contains('payment-cell')) {
const amount = parseFloat(col.dataset.amount) || 0;
cellValue = amount.toFixed(2);
} else if (col.tagName === 'TH') {
cellValue = col.textContent.trim();
} else {
let text = col.textContent.trim();
if (text.includes('$')) {
cellValue = text.replace(/[\$,]/g, '');
} else if (text === 'Activa' || text === 'Deshabitada') {
cellValue = text;
} else if (text === '-' || text === 'Sin monto' || text === '0.00') {
cellValue = '0';
} else {
cellValue = text;
}
}
rowData.push(cellValue);
});
}
csv.push(rowData.join(','));
});
const headerTitle = '"Condominio IBIZA-Cto Sierra Morena 152 - Reporte de Pagos de Agua <?= $year ?>"\n\n';
const csvContent = headerTitle + csv.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'pagos_agua_<?= $year ?>.csv';
link.click();
}
</script>