Mejorar Balance General con detalle de conceptos especiales y filtro por año

This commit is contained in:
Administrador Ibiza
2025-12-30 23:36:51 -06:00
parent d629526485
commit 5e714ebae9
4 changed files with 327 additions and 32 deletions

View File

@@ -533,6 +533,8 @@ switch ($page) {
$balance = Report::getGeneralBalance();
$expensesByCategory = Report::getExpensesByCategory();
$accessibleHouseIds = Auth::getAccessibleHouseIds();
$filterYear = $_GET['filter_year'] ?? null;
$conceptDetails = Report::getConceptDetailsByYear($filterYear);
if ($reportType == 'water-debtors') {
$filters = [
@@ -754,6 +756,8 @@ switch ($page) {
// Requerimos el modelo Report
require_once __DIR__ . '/models/Report.php';
$balance = Report::getGeneralBalance();
$filterYear = $_GET['filter_year'] ?? null;
$conceptDetails = Report::getConceptDetailsByYear($filterYear);
include __DIR__ . '/views/reports/pdf_balance.php';
break;
}
@@ -763,25 +767,63 @@ switch ($page) {
exit;
case 'export_csv_balance':
// Lógica para exportar CSV de balance
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="Balance_General_IBIZA_' . date('Y') . '.csv"');
fputs(fopen('php://output', 'w'), $bom = (chr(0xEF) . chr(0xBB) . chr(0xBF))); // BOM for UTF-8 in Excel
// Requerimos el modelo Report
Auth::requireAdmin();
require_once __DIR__ . '/models/Report.php';
$filterYear = $_GET['filter_year'] ?? null;
$balance = Report::getGeneralBalance();
$conceptDetails = Report::getConceptDetailsByYear($filterYear);
header('Content-Type: text/csv');
$filename = 'Balance_General_IBIZA';
if ($filterYear) {
$filename .= '_' . $filterYear;
}
header('Content-Disposition: attachment; filename="' . $filename . '.csv"');
fputs(fopen('php://output', 'w'), $bom = (chr(0xEF) . chr(0xBB) . chr(0xBF)));
$output = fopen('php://output', 'w');
fputcsv($output, ['Condominio IBIZA-Cto Sierra Morena 152 - Balance General ' . date('Y')]);
fputcsv($output, ['Condominio IBIZA-Cto Sierra Morena 152 - Balance General']);
if ($filterYear) {
fputcsv($output, ['Año: ' . $filterYear]);
}
fputcsv($output, ['']); // Blank line
fputcsv($output, ['Descripción', 'Monto']);
fputcsv($output, ['Total Ingresos (Conceptos)', $balance['total_incomes']]);
if (!Auth::isLector()) {
fputcsv($output, ['Total Egresos', $balance['total_expenses']]);
fputcsv($output, ['Balance Neto', $balance['balance']]);
fputcsv($output, ['Total Egresos', $balance['total_expenses']]);
fputcsv($output, ['Balance Neto', $balance['balance']]);
fputcsv($output, ['']); // Blank line
if (!empty($conceptDetails['concepts'])) {
fputcsv($output, ['Detalle de Conceptos Especiales']);
fputcsv($output, ['']); // Blank line
fputcsv($output, ['Concepto', 'Descripción', 'Esperado', 'Recaudado', 'Pendiente', 'Gastos', 'Balance']);
foreach ($conceptDetails['concepts'] as $cd) {
fputcsv($output, [
$cd['concept']['name'],
$cd['concept']['description'] ?? '-',
$cd['expected'],
$cd['collected'],
$cd['pending'],
$cd['expenses'],
$cd['balance']
]);
}
fputcsv($output, ['']); // Blank line
fputcsv($output, [
'TOTALES',
'',
$conceptDetails['totals']['expected'],
$conceptDetails['totals']['collected'],
$conceptDetails['totals']['pending'],
$conceptDetails['totals']['expenses'],
$conceptDetails['totals']['balance']
]);
}
fclose($output);
exit;

View File

@@ -66,6 +66,92 @@ class Report {
];
}
public static function getConceptDetailsByYear($year = null) {
$db = Database::getInstance();
$concepts = $db->fetchAll(
"SELECT c.id, c.name, c.amount_per_house, c.concept_date, c.description
FROM finance_collection_concepts c
WHERE c.is_active = 1
ORDER BY c.concept_date DESC"
);
$conceptsData = [];
$totalExpected = 0;
$totalCollected = 0;
$totalExpenses = 0;
foreach ($concepts as $concept) {
$conceptYear = date('Y', strtotime($concept['concept_date']));
if ($year && $conceptYear != $year) {
continue;
}
$activeHousesCount = $db->fetchOne(
"SELECT COUNT(*) as count FROM houses WHERE status = 'activa'"
);
$activeHousesCount = $activeHousesCount['count'] ?? 0;
$expected = $concept['amount_per_house'] * $activeHousesCount;
$whereClause = "WHERE cp.concept_id = ?";
$params = [$concept['id']];
if ($year) {
$whereClause .= " AND YEAR(cp.payment_date) = ?";
$params[] = $year;
}
$collected = $db->fetchOne(
"SELECT COALESCE(SUM(cp.amount), 0) as total
FROM finance_collection_payments cp
$whereClause",
$params
);
$collectedAmount = $collected['total'] ?? 0;
$expenses = $db->fetchOne(
"SELECT COALESCE(SUM(ec.amount), 0) as total
FROM expense_concept_allocations ec
JOIN expenses e ON ec.expense_id = e.id
WHERE ec.concept_id = ?
" . ($year ? "AND YEAR(e.expense_date) = ?" : ""),
$year ? [$concept['id'], $year] : [$concept['id']]
);
$expensesAmount = $expenses['total'] ?? 0;
$balance = $collectedAmount - $expensesAmount;
$conceptsData[] = [
'concept' => $concept,
'year' => $conceptYear,
'expected' => $expected,
'collected' => $collectedAmount,
'expenses' => $expensesAmount,
'balance' => $balance,
'pending' => max(0, $expected - $collectedAmount)
];
$totalExpected += $expected;
$totalCollected += $collectedAmount;
$totalExpenses += $expensesAmount;
}
$totalBalance = $totalCollected - $totalExpenses;
return [
'concepts' => $conceptsData,
'totals' => [
'expected' => $totalExpected,
'collected' => $totalCollected,
'expenses' => $totalExpenses,
'balance' => $totalBalance,
'pending' => max(0, $totalExpected - $totalCollected)
],
'year' => $year
];
}
public static function getHouseStatement($houseId, $year = null) {
$db = Database::getInstance();
$house = House::findById($houseId);

View File

@@ -521,6 +521,60 @@ function exportConceptsCSV() {
</script>
<?php else: ?>
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="bi bi-funnel"></i> Filtro por Año
</h5>
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="collapse" data-bs-target="#yearFilterCollapse">
<i class="bi bi-chevron-down"></i>
</button>
</div>
<div class="collapse show" id="yearFilterCollapse">
<div class="card-body">
<form id="yearFilter">
<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
$years = range(2024, date('Y') + 1);
foreach ($years as $y): ?>
<option value="<?= $y ?>" <?= ($_GET['filter_year'] ?? '') == $y ? 'selected' : '' ?>><?= $y ?></option>
<?php endforeach; ?>
</select>
</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 Filtro
</button>
<a href="/dashboard.php?page=reportes" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Limpiar Filtro
</a>
</div>
</div>
</form>
</div>
</div>
</div>
<script>
document.getElementById('yearFilter').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'));
}
window.location.href = '/dashboard.php?page=reportes&' + params.toString();
});
</script>
<div class="row g-4 mb-4">
<div class="col-md-3">
@@ -596,33 +650,75 @@ function exportConceptsCSV() {
</div>
</div>
<?php endif; ?>
<div class="col-md-<?= Auth::isLector() ? '12' : '6' ?>">
<div class="col-md-<?= Auth::isLector() ? '12' : '12' ?>">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Resumen Financiero</h5>
<h5 class="card-title mb-0">
Resumen Financiero
<?php if (!empty($conceptDetails['year'])): ?>
<small class="text-muted">(<?= $conceptDetails['year'] ?>)</small>
<?php endif; ?>
</h5>
<button onclick="exportBalancePDF()" class="btn btn-outline-primary btn-sm">
<i class="bi bi-file-earmark-pdf"></i> PDF
</button>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<td>Total Ingresos (Conceptos):</td>
<td class="text-end text-success">$<?= number_format($balance['total_incomes'], 2) ?></td>
</tr>
<?php if (!Auth::isLector()): ?>
<tr class="table-light">
<td>Total Egresos:</td>
<td class="text-end text-danger">$<?= number_format($balance['total_expenses'], 2) ?></td>
</tr>
<tr class="table-dark">
<td><strong>Balance:</strong></td>
<td class="text-end fw-bold <?= $balance['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">
$<?= number_format($balance['balance'], 2) ?>
</td>
</tr>
<?php endif; ?>
</table>
<?php if (empty($conceptDetails['concepts'])): ?>
<p class="text-muted">No hay conceptos especiales registrados para el año seleccionado.</p>
<?php else: ?>
<table class="table table-sm table-bordered">
<thead class="table-primary">
<tr>
<th>Concepto</th>
<th class="text-end">Esperado</th>
<th class="text-end">Recaudado</th>
<th class="text-end">Pendiente</th>
<th class="text-end">Gastos Asociados</th>
<th class="text-end">Balance</th>
</tr>
</thead>
<tbody>
<?php foreach ($conceptDetails['concepts'] as $cd): ?>
<tr>
<td>
<strong><?= htmlspecialchars($cd['concept']['name']) ?></strong>
<?php if (!empty($cd['concept']['description'])): ?>
<small class="text-muted d-block"><?= htmlspecialchars($cd['concept']['description']) ?></small>
<?php endif; ?>
</td>
<td class="text-end">$<?= number_format($cd['expected'], 2) ?></td>
<td class="text-end text-success">$<?= number_format($cd['collected'], 2) ?></td>
<td class="text-end text-warning">$<?= number_format($cd['pending'], 2) ?></td>
<?php if (!Auth::isLector()): ?>
<td class="text-end text-danger">$<?= number_format($cd['expenses'], 2) ?></td>
<td class="text-end fw-bold <?= $cd['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">
$<?= number_format($cd['balance'], 2) ?>
</td>
<?php else: ?>
<td class="text-end" colspan="2">-</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="table-dark">
<tr>
<th><strong>TOTALES:</strong></th>
<th class="text-end">$<?= number_format($conceptDetails['totals']['expected'], 2) ?></th>
<th class="text-end">$<?= number_format($conceptDetails['totals']['collected'], 2) ?></th>
<th class="text-end">$<?= number_format($conceptDetails['totals']['pending'], 2) ?></th>
<?php if (!Auth::isLector()): ?>
<th class="text-end">$<?= number_format($conceptDetails['totals']['expenses'], 2) ?></th>
<th class="text-end fw-bold <?= $conceptDetails['totals']['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">
$<?= number_format($conceptDetails['totals']['balance'], 2) ?>
</th>
<?php else: ?>
<th class="text-end" colspan="2">-</th>
<?php endif; ?>
</tr>
</tfoot>
</table>
<?php endif; ?>
</div>
</div>
</div>
@@ -666,11 +762,21 @@ function exportConceptsCSV() {
<script>
function exportBalancePDF() {
window.open('/dashboard.php?page=reportes_actions&action=export_pdf_report&type=balance', '_blank');
const filterYear = '<?= $_GET['filter_year'] ?? '' ?>';
let url = '/dashboard.php?page=reportes_actions&action=export_pdf_report&type=balance';
if (filterYear) {
url += '&filter_year=' + filterYear;
}
window.open(url, '_blank');
}
function exportBalanceCSV() {
window.open('/dashboard.php?page=reportes_actions&action=export_csv_balance', '_blank');
const filterYear = '<?= $_GET['filter_year'] ?? '' ?>';
let url = '/dashboard.php?page=reportes_actions&action=export_csv_balance';
if (filterYear) {
url += '&filter_year=' + filterYear;
}
window.open(url, '_blank');
}
function exportExpensesPDF() {

View File

@@ -58,3 +58,64 @@
<?php endif; ?>
</tbody>
</table>
<br>
<?php if (!empty($conceptDetails['concepts'])): ?>
<h3 style="text-align: center; margin-bottom: 10px;">Detalle de Conceptos Especiales</h3>
<?php if (!empty($conceptDetails['year'])): ?>
<p style="text-align: center; font-size: 9pt; margin-bottom: 15px;">Año: <?= $conceptDetails['year'] ?></p>
<?php endif; ?>
<table style="margin-bottom: 10px;">
<thead style="background-color: #0d6efd; color: white;">
<tr>
<th>Concepto</th>
<th>Esperado</th>
<th>Recaudado</th>
<th>Pendiente</th>
<th>Gastos</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
<?php foreach ($conceptDetails['concepts'] as $cd): ?>
<tr>
<td style="text-align: left;">
<strong><?= htmlspecialchars($cd['concept']['name']) ?></strong>
<?php if (!empty($cd['concept']['description'])): ?>
<br><small style="color: #666;"><?= htmlspecialchars($cd['concept']['description']) ?></small>
<?php endif; ?>
</td>
<td>$<?= number_format($cd['expected'], 2) ?></td>
<td class="text-success">$<?= number_format($cd['collected'], 2) ?></td>
<td class="text-warning">$<?= number_format($cd['pending'], 2) ?></td>
<?php if (!Auth::isLector()): ?>
<td class="text-danger">$<?= number_format($cd['expenses'], 2) ?></td>
<td class="text-end fw-bold <?= $cd['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">
$<?= number_format($cd['balance'], 2) ?>
</td>
<?php else: ?>
<td colspan="2">-</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot style="background-color: #343a40; color: white; font-weight: bold;">
<tr>
<td style="text-align: left;">TOTALES:</td>
<td>$<?= number_format($conceptDetails['totals']['expected'], 2) ?></td>
<td>$<?= number_format($conceptDetails['totals']['collected'], 2) ?></td>
<td>$<?= number_format($conceptDetails['totals']['pending'], 2) ?></td>
<?php if (!Auth::isLector()): ?>
<td>$<?= number_format($conceptDetails['totals']['expenses'], 2) ?></td>
<td class="text-end <?= $conceptDetails['totals']['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">
$<?= number_format($conceptDetails['totals']['balance'], 2) ?>
</td>
<?php else: ?>
<td colspan="2">-</td>
<?php endif; ?>
</tr>
</tfoot>
</table>
<?php endif; ?>