Primer version funcional

This commit is contained in:
Administrador Ibiza
2025-12-29 23:37:11 -06:00
commit 5289fd4133
294 changed files with 111418 additions and 0 deletions

48
views/auth/login.php Executable file
View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - IBIZA CEA</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.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; position: relative; padding-bottom: 60px; }
.login-container { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.login-card { max-width: 400px; width: 100%; }
.login-card .card-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; }
footer { position: fixed; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.2); }
</style>
</head>
<body>
<div class="login-container">
<div class="card login-card shadow">
<div class="card-header text-center py-4">
<h3><i class="bi bi-house-door-fill"></i> IBIZA CEA</h3>
<p class="mb-0">Sistema de Gestión</p>
</div>
<div class="card-body p-4">
<?php if (isset($error)): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label"><i class="bi bi-person"></i> Usuario</label>
<input type="text" class="form-control" id="username" name="username" required autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label"><i class="bi bi-lock"></i> Contraseña</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Iniciar Sesión</button>
</div>
</form>
</div>
</div>
</div>
<footer class="text-center text-white py-3">
<p class="mb-0 small">Condiminio IBIZA - Derechos Reservados Miguel Pons casa 11</p>
</footer>
</body>
</html>

108
views/configurar/index.php Executable file
View File

@@ -0,0 +1,108 @@
<div class="row mb-4">
<div class="col-12">
<h2><i class="bi bi-gear"></i> Configuración Mensual</h2>
<p class="text-muted">Configurar montos esperados de agua por mes</p>
</div>
</div>
<div class="mb-4">
<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 class="row">
<?php foreach ($months as $month):
$bill = $monthlyBills[$month] ?? null;
?>
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><?= $month ?></h6>
<?php if ($bill): ?>
<span class="badge bg-success">$<?= number_format($bill['amount_per_house'], 2) ?>/casa</span>
<?php endif; ?>
</div>
<div class="card-body">
<div class="mb-2">
<small class="text-muted">Monto Total del Mes</small>
<input type="number" step="0.01" class="form-control form-control-sm"
id="total_<?= $month ?>" value="<?= $bill['total_amount'] ?? '' ?>">
</div>
<div class="mb-2">
<small class="text-muted">Monto por Casa</small>
<input type="number" step="0.01" class="form-control form-control-sm"
id="per_house_<?= $month ?>" value="<?= $bill['amount_per_house'] ?? '' ?>">
</div>
<div class="mb-2">
<small class="text-muted">Fecha de Vencimiento</small>
<input type="date" class="form-control form-control-sm"
id="due_<?= $month ?>" value="<?= $bill['due_date'] ?? '' ?>">
</div>
<button onclick="saveMonth('<?= $month ?>')" class="btn btn-primary btn-sm w-100">
<i class="bi bi-save"></i> Guardar
</button>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="card mt-4">
<div class="card-header">
<h5 class="card-title mb-0"><i class="bi bi-info-circle"></i> Información</h5>
</div>
<div class="card-body">
<p class="mb-2">El <strong>Monto por Casa</strong> se usa para determinar si un pago está completo, parcial o pendiente en la tabla de pagos.</p>
<p class="mb-0">Casas marcadas como "consumo_only" reciben un descuento de $100 (desde 2025)</p>
</div>
</div>
<script>
document.getElementById('yearSelect').addEventListener('change', function() {
window.location.href = '/dashboard.php?page=configurar&year=' + this.value;
});
function saveMonth(month) {
const total = document.getElementById('total_' + month).value;
const perHouse = document.getElementById('per_house_' + month).value;
const dueDate = document.getElementById('due_' + month).value;
const year = <?= $year ?>;
if (total === '' || perHouse === '') {
Swal.fire('Error', 'Debe ingresar el monto total y el monto por casa', 'error');
return;
}
fetch('/dashboard.php?page=config_actions&action=save_monthly_bill', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
year: year,
month: month,
total_amount: parseFloat(total),
amount_per_house: parseFloat(perHouse),
due_date: dueDate || null
})
})
.then(r => {
console.log('Response status:', r.status);
return r.json();
})
.then(data => {
console.log('Response data:', data);
if (data.success) {
Swal.fire('Éxito', 'Configuración guardada', 'success').then(() => location.reload());
} else {
Swal.fire('Error', data.message, 'error');
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire('Error', 'Error al guardar: ' + error.message, 'error');
});
}
</script>

263
views/dashboard/index.php Executable file
View File

@@ -0,0 +1,263 @@
<div class="row mb-4">
<div class="col-12">
<h2>Dashboard</h2>
<p class="text-muted">Vista general del sistema</p>
</div>
</div>
<div class="mb-4 d-flex justify-content-between align-items-center flex-wrap gap-2">
<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 class="input-group" style="width: 300px;">
<input type="text" class="form-control" id="globalSearch" placeholder="Buscar casa o propietario...">
<button class="btn btn-outline-secondary" onclick="globalSearch()">
<i class="bi bi-search"></i>
</button>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-2">
<div class="card stat-card border-primary">
<div class="card-body text-center">
<h6 class="text-muted small">Casas</h6>
<h4 class="text-primary mb-0"><?= $stats['total_houses'] ?></h4>
<small class="text-muted">Total</small>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stat-card border-success">
<div class="card-body text-center">
<h6 class="text-muted small">Activas</h6>
<h4 class="text-success mb-0"><?= $stats['active_houses'] ?></h4>
<small class="text-muted">Operativas</small>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stat-card border-info">
<div class="card-body text-center">
<h6 class="text-muted small">Ingresos</h6>
<h4 class="text-info mb-0">$<?= number_format($stats['total_payments'], 0) ?></h4>
<small class="text-muted">Año <?= $year ?></small>
</div>
</div>
</div>
<?php if (!Auth::isLector()): ?>
<div class="col-md-2">
<div class="card stat-card border-danger">
<div class="card-body text-center">
<h6 class="text-muted small">Gastos</h6>
<h4 class="text-danger mb-0">$<?= number_format($stats['total_expenses'], 0) ?></h4>
<small class="text-muted">Año <?= $year ?></small>
</div>
</div>
</div>
<div class="col-md-2">
<div class="card stat-card border-warning">
<div class="card-body text-center">
<h6 class="text-muted small">Balance</h6>
<h4 class="<?= $stats['balance'] >= 0 ? 'text-success' : 'text-danger' ?> mb-0">
$<?= number_format($stats['balance'], 0) ?>
</h4>
<small class="text-muted">Neto</small>
</div>
</div>
</div>
<?php endif; ?>
<div class="col-md-2">
<div class="card stat-card border-secondary">
<div class="card-body text-center">
<h6 class="text-muted small">Conceptos</h6>
<h4 class="text-secondary mb-0"><?= $stats['active_concepts'] ?></h4>
<small class="text-muted">Activos</small>
</div>
</div>
</div>
</div>
<div class="row g-4">
<?php if (Auth::isAdmin() || Auth::isCapturist()): ?>
<div class="col-lg-<?= Auth::isCapturist() && !Auth::isAdmin() ? '8' : '12' ?>">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"><i class="bi bi-activity"></i> Actividad Reciente</h5>
<?php if (Auth::isAdmin()): ?>
<div>
<select id="userActivityFilter" class="form-select form-select-sm d-inline-block" style="width: 200px;">
<option value="">Todos los usuarios</option>
<?php foreach ($distinctUsers as $user): ?>
<option value="<?= $user['id'] ?>" <?= ($_GET['user'] ?? '') == $user['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($user['first_name'] . ' ' . $user['last_name']) ?> (<?= htmlspecialchars($user['username']) ?>)
</option>
<?php endforeach; ?>
</select>
<button onclick="clearActivityHistory()" class="btn btn-sm btn-outline-danger ms-2">
<i class="bi bi-trash"></i> Limpiar Historial
</button>
</div>
<?php endif; ?>
</div>
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Fecha</th>
<th>Usuario</th>
<th>Acción</th>
<th>Detalles</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentActivity as $log): ?>
<tr>
<td><small><?= date('d/m/Y H:i', strtotime($log['created_at'])) ?></small></td>
<td><?= htmlspecialchars($log['username'] ?? '-') ?></td>
<td>
<span class="badge bg-secondary"><?= htmlspecialchars($log['action']) ?></span>
</td>
<td><small><?= htmlspecialchars($log['details'] ?? '-') ?></small></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php endif; ?>
<?php if (Auth::isCapturist() && !Auth::isAdmin()): ?>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0"><i class="bi bi-grid"></i> Acciones Rápidas</h5>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-6">
<a href="/dashboard.php?page=pagos" class="btn btn-outline-primary w-100">
<i class="bi bi-droplet-fill d-block mb-1"></i>
<small>Pagos</small>
</a>
</div>
<div class="col-6">
<a href="/dashboard.php?page=finanzas" class="btn btn-outline-danger w-100">
<i class="bi bi-receipt d-block mb-1"></i>
<small>Gastos</small>
</a>
</div>
<div class="col-6">
<a href="/dashboard.php?page=casas" class="btn btn-outline-info w-100">
<i class="bi bi-building d-block mb-1"></i>
<small>Casas</small>
</a>
</div>
<div class="col-6">
<a href="/dashboard.php?page=reportes" class="btn btn-outline-secondary w-100">
<i class="bi bi-file-earmark-bar-graph d-block mb-1"></i>
<small>Reportes</small>
</a>
</div>
<?php if (Auth::isAdmin()): ?>
<div class="col-6">
<a href="/dashboard.php?page=configurar" class="btn btn-outline-warning w-100">
<i class="bi bi-gear d-block mb-1"></i>
<small>Configurar</small>
</a>
</div>
<div class="col-6">
<a href="/dashboard.php?page=importar" class="btn btn-outline-success w-100">
<i class="bi bi-file-earmark-arrow-up d-block mb-1"></i>
<small>Importar</small>
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5 class="card-title mb-0"><i class="bi bi-info-circle"></i> Información</h5>
</div>
<div class="card-body">
<p class="mb-1"><small><strong>Usuario:</strong> <?= htmlspecialchars((Auth::user()['first_name'] ?? '') . ' ' . (Auth::user()['last_name'] ?? '')) ?></small></p>
<p class="mb-1"><small><strong>Rol:</strong> <?= htmlspecialchars(Auth::role()) ?></small></p>
<p class="mb-0"><small><strong>Año Fiscal:</strong> <?= $year ?></small></p>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<script>
document.getElementById('yearSelect').addEventListener('change', function() {
window.location.href = '/dashboard.php?page=dashboard&year=' + this.value;
});
function globalSearch() {
const query = document.getElementById('globalSearch').value.trim();
if (query) {
fetch(`/dashboard.php?page=search_global&q=${encodeURIComponent(query)}`)
.then(r => r.json())
.then(data => {
if (data.success && data.results.length > 0) {
const results = data.results.map(r =>
`<a href="/dashboard.php?page=house_view&id=${r.id}" class="dropdown-item">
Casa ${r.number} - ${r.owner_name || 'Sin propietario'}
</a>`
).join('');
Swal.fire({
title: 'Resultados',
html: `<div style="max-height:300px;overflow-y:auto">${results}</div>`,
showConfirmButton: false
});
} else {
Swal.fire('Sin resultados', 'No se encontraron casas', 'info');
}
});
}
}
document.getElementById('userActivityFilter')?.addEventListener('change', function() {
const userId = this.value;
if (userId) {
window.location.href = '/dashboard.php?page=dashboard&user=' + userId;
} else {
window.location.href = '/dashboard.php?page=dashboard';
}
});
function clearActivityHistory() {
Swal.fire({
title: '¿Limpiar historial?',
text: 'Esta acción eliminará TODO el historial de actividad y no se puede deshacer.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
confirmButtonText: 'Sí, eliminar todo'
}).then((result) => {
if (result.isConfirmed) {
fetch('/dashboard.php?page=activity_logs&action=clear', {
method: 'DELETE'
}).then(r => r.json()).then(data => {
if (data.success) {
Swal.fire('Eliminado', data.message, 'success').then(() => location.reload());
} else {
Swal.fire('Error', data.message, 'error');
}
}).catch(() => {
Swal.fire('Error', 'Error de conexión', 'error');
});
}
});
}
</script>

415
views/finance/concept_view.php Executable file
View File

@@ -0,0 +1,415 @@
<div class="row mb-4">
<div class="col-12">
<a href="/dashboard.php?page=finanzas" class="btn btn-outline-secondary mb-2">
<i class="bi bi-arrow-left"></i> Volver a Finanzas
</a>
<h2><i class="bi bi-collection"></i> <?= htmlspecialchars($concept['name']) ?></h2>
<p class="text-muted"><?= htmlspecialchars($concept['description'] ?? '') ?></p>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card border-primary">
<div class="card-body text-center">
<h6 class="text-muted">Monto por Casa</h6>
<h4 class="text-primary">$<?= number_format($concept['amount_per_house'], 2) ?></h4>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-success">
<div class="card-body text-center">
<h6 class="text-muted">Recaudado</h6>
<h4 class="text-success">$<?= number_format($status['total_collected'], 2) ?></h4>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-danger">
<div class="card-body text-center">
<h6 class="text-muted">Gastado</h6>
<h4 class="text-danger">$<?= number_format($status['total_expenses'], 2) ?></h4>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card <?= $status['balance'] >= 0 ? 'border-success' : 'border-danger' ?>">
<div class="card-body text-center">
<h6 class="text-muted">Balance Neto</h6>
<h4 class="<?= $status['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">$<?= number_format($status['balance'], 2) ?></h4>
<small class="text-muted">(Recaudado - Gastado)</small>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Pagos por Casa</h5>
</div>
<div class="card-body">
<?php if (empty($payments)): ?>
<div class="alert alert-info text-center">
<i class="bi bi-info-circle"></i>
No hay pagos inicializados para este concepto.
<br><br>
<?php if (Auth::isCapturist()): ?>
<button id="init-payments-btn" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Inicializar Pagos
</button>
<?php endif; ?>
</div>
<?php else: ?>
<?php if (Auth::isCapturist()): ?>
<div class="alert alert-warning text-center mb-3">
<i class="bi bi-arrow-clockwise"></i>
<button id="reinit-payments-btn" class="btn btn-warning me-2">
<i class="bi bi-arrow-clockwise"></i> Reinicializar Pagos
</button>
<small>Esto actualizará todos los pagos a $0.00</small>
</div>
<?php endif; ?>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Casa</th>
<th>Propietario</th>
<th>Monto</th>
<th>Fecha de Pago</th>
<?php if (Auth::isCapturist()): ?>
<th>Acciones</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($payments as $payment):
$cellClass = $payment['amount'] > 0 ? 'paid' : 'pending';
?>
<tr data-payment-id="<?= $payment['id'] ?>" data-house-id="<?= $payment['house_id'] ?>">
<td><strong><?= $payment['house_number'] ?></strong></td>
<td><?= htmlspecialchars($payment['owner_name'] ?? '-') ?></td>
<td class="payment-cell <?= $cellClass ?>"
data-amount="<?= $payment['amount'] ?>"
data-payment-date="<?= $payment['payment_date'] ?: '' ?>"
<?= Auth::isCapturist() ? 'contenteditable="true"' : '' ?>>
<?= $payment['amount'] > 0 ? '$' . number_format($payment['amount'], 2) : '-' ?>
</td>
<td>
<?php if (Auth::isCapturist()): ?>
<input type="date" class="form-control form-control-sm payment-date-input"
value="<?= $payment['payment_date'] ? date('Y-m-d', strtotime($payment['payment_date'])) : '' ?>">
<?php else: ?>
<?= $payment['payment_date'] ? date('d/m/Y', strtotime($payment['payment_date'])) : '-' ?>
<?php endif; ?>
</td>
<?php if (Auth::isCapturist()): ?>
<td>
<button class="btn btn-sm btn-primary" onclick="saveConceptPayment(this)">
<i class="bi bi-check"></i>
</button>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<?php
require_once __DIR__ . '/../../models/Expense.php';
$db = Database::getInstance();
$conceptExpenses = $db->fetchAll(
"SELECT e.*, ec.amount as allocated_amount
FROM expense_concept_allocations ec
JOIN expenses e ON ec.expense_id = e.id
WHERE ec.concept_id = ?
ORDER BY e.expense_date DESC",
[$concept['id']]
);
?>
<div class="card mt-4">
<div class="card-header">
<h5 class="card-title mb-0">Gastos Asociados</h5>
</div>
<div class="card-body">
<?php if (!empty($conceptExpenses)): ?>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Fecha</th>
<th>Descripción</th>
<th>Categoría</th>
<th>Asignado</th>
<th>Total Gasto</th>
</tr>
</thead>
<tbody>
<?php foreach ($conceptExpenses as $exp): ?>
<tr>
<td><?= date('d/m/Y', strtotime($exp['expense_date'])) ?></td>
<td><?= htmlspecialchars($exp['description']) ?></td>
<td><?= htmlspecialchars($exp['category'] ?? '-') ?></td>
<td class="text-end text-info">$<?= number_format($exp['allocated_amount'], 2) ?></td>
<td class="text-end">$<?= number_format($exp['amount'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr class="table-secondary">
<th colspan="3" class="text-end">Total:</th>
<th class="text-end text-danger">$<?= number_format($status['total_expenses'], 2) ?></th>
<th>-</th>
</tr>
</tfoot>
</table>
</div>
<?php else: ?>
<p class="text-muted mb-0">No hay gastos asociados a este concepto.</p>
<?php endif; ?>
</div>
</div>
<script>
const conceptId = '<?= $concept["id"] ?>';
window.initializeConceptPayments = function() {
console.log('=== Inicializando pagos ===');
console.log('Concept ID:', conceptId);
if (typeof Swal === 'undefined') {
alert('Error: SweetAlert no está cargado');
return;
}
Swal.fire({
title: '¿Inicializar pagos?',
text: 'Esto creará registros de pago para todas las casas activas con monto $0.00. ¿Continuar?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Sí, inicializar',
confirmButtonColor: '#3085d6',
cancelButtonText: 'Cancelar'
}).then(function(result) {
console.log('=== Resultado del sweetalert ===', result);
if (result.isConfirmed) {
console.log('=== Iniciando fetch ===');
var url = '/dashboard.php?page=concept_view_actions&action=initialize_concept_payments&concept_id=' + conceptId;
console.log('URL:', url);
fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(function(response) {
console.log('=== Respuesta del servidor ===', response);
console.log('Status:', response.status);
console.log('OK:', response.ok);
if (!response.ok) {
return response.text().then(function(text) {
console.log('Error response text:', text);
throw new Error('HTTP ' + response.status + ': ' + text);
});
}
return response.text().then(function(text) {
console.log('Response text:', text);
try {
return JSON.parse(text);
} catch (e) {
console.error('JSON parse error:', e);
console.error('Response text:', text);
throw new Error('Error al parsear JSON: ' + e.message);
}
});
})
.then(function(data) {
console.log('=== Datos recibidos ===', data);
if (data.success) {
Swal.fire('Éxito', data.message, 'success').then(function() {
console.log('Recargando página...');
location.reload();
});
} else {
Swal.fire('Error', data.message || 'Error desconocido', 'error');
}
})
.catch(function(error) {
console.error('=== Error en fetch ===', error);
console.error('Error message:', error.message);
Swal.fire('Error', 'Ocurrió un error: ' + error.message, 'error');
});
}
});
};
<?php if (Auth::isCapturist()): ?>
document.querySelectorAll('.payment-cell[contenteditable="true"]').forEach(function(cell) {
let originalValue = '';
cell.addEventListener('focus', function() {
originalValue = this.textContent.trim();
this.classList.add('editing');
const currentValue = this.textContent.trim();
if (currentValue === '-' || currentValue === '') {
this.textContent = '';
} else if (currentValue.includes('$')) {
this.textContent = currentValue.replace(/[^0-9.]/g, '');
} else {
this.textContent = currentValue;
}
setTimeout(() => {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(this);
selection.removeAllRanges();
selection.addRange(range);
}, 0);
});
cell.addEventListener('blur', function() {
this.classList.remove('editing');
const newValue = this.textContent.trim();
if (newValue === '') {
this.textContent = originalValue;
} else {
let value = parseFloat(newValue.replace(/[^0-9.-]+/g, '')) || 0;
if (value < 0 || isNaN(value)) {
this.textContent = originalValue;
} else if (value === 0) {
this.textContent = '$0.00';
} else {
this.textContent = '$' + value.toFixed(2);
}
}
});
cell.addEventListener('keydown', function(e) {
if (!((e.key >= '0' && e.key <= '9') ||
e.key === '.' ||
e.key === 'Backspace' ||
e.key === 'Delete' ||
e.key === 'Tab' ||
e.key === 'Enter' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowRight')) {
e.preventDefault();
}
if (e.key === 'Enter') {
e.preventDefault();
const btn = this.parentElement.querySelector('.btn-primary');
if (btn) {
btn.click();
}
}
});
cell.addEventListener('blur', function() {
this.classList.remove('editing');
const newValue = this.textContent.trim();
if (newValue === '') {
// If cell is empty on blur, restore the original value (cancel edit)
this.textContent = originalValue;
} else {
let value = parseFloat(newValue.replace(/[^0-g.-]+/g, '')) || 0;
if (value <= 0) {
this.textContent = '-';
} else {
this.textContent = '$' + value.toFixed(2);
}
}
});
cell.addEventListener('keydown', function(e) {
if (!((e.key >= '0' && e.key <= '9') ||
e.key === '.' ||
e.key === 'Backspace' ||
e.key === 'Delete' ||
e.key === 'Tab' ||
e.key === 'Enter' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowRight')) {
e.preventDefault();
}
if (e.key === 'Enter') {
e.preventDefault();
this.blur();
}
});
});
window.saveConceptPayment = function(btn) {
const row = btn.closest('tr');
const cell = row.querySelector('.payment-cell');
const dateInput = row.querySelector('.payment-date-input');
const houseId = row.dataset.houseId;
const value = cell.textContent.trim();
const amount = parseFloat(value.replace(/[^0-9.-]+/g, '')) || 0;
const paymentDate = dateInput ? dateInput.value : null;
console.log('=== Guardando pago ===');
console.log('House ID:', houseId);
console.log('Amount:', amount);
console.log('Payment Date:', paymentDate);
cell.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
fetch('/dashboard.php?page=concept_view_actions&action=save_concept_payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
concept_id: '<?= $concept["id"] ?>',
house_id: houseId,
amount: amount,
payment_date: paymentDate
})
})
.then(function(r) {
console.log('Response status:', r.status);
return r.json();
})
.then(function(data) {
console.log('Save payment data:', data);
if (data.success) {
location.reload();
} else {
Swal.fire('Error', data.message, 'error');
}
})
.catch(function(error) {
console.error('Save payment error:', error);
Swal.fire('Error', 'Ocurrió un error: ' + error.message, 'error');
});
};
// Attach event listener to button
const initButton = document.getElementById('init-payments-btn');
if (initButton) {
initButton.addEventListener('click', window.initializeConceptPayments);
}
// Attach event listener to reinitialize button
const reinitButton = document.getElementById('reinit-payments-btn');
if (reinitButton) {
reinitButton.addEventListener('click', window.initializeConceptPayments);
}
<?php endif; ?>
</script>

View File

@@ -0,0 +1,258 @@
<div class="row mb-4">
<div class="col-12">
<a href="/dashboard.php?page=finanzas" class="btn btn-outline-secondary mb-2">
<i class="bi bi-arrow-left"></i> Volver a Finanzas
</a>
<h2><i class="bi bi-collection"></i> <?= htmlspecialchars($concept['name']) ?></h2>
<p class="text-muted"><?= htmlspecialchars($concept['description'] ?? '') ?></p>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card border-primary">
<div class="card-body text-center">
<h6 class="text-muted">Monto por Casa</h6>
<h4 class="text-primary">$<?= number_format($concept['amount_per_house'], 2) ?></h4>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-success">
<div class="card-body text-center">
<h6 class="text-muted">Recaudado</h6>
<h4 class="text-success">$<?= number_format($status['total_collected'], 2) ?></h4>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-danger">
<div class="card-body text-center">
<h6 class="text-muted">Gastado</h6>
<h4 class="text-danger">$<?= number_format($status['total_expenses'], 2) ?></h4>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card <?= $status['balance'] >= 0 ? 'border-success' : 'border-danger' ?>">
<div class="card-body text-center">
<h6 class="text-muted">Balance Neto</h6>
<h4 class="<?= $status['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">$<?= number_format($status['balance'], 2) ?></h4>
<small class="text-muted">(Recaudado - Gastado)</small>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Pagos por Casa</h5>
</div>
<div class="card-body">
<?php if (empty($payments)): ?>
<div class="alert alert-info text-center">
<i class="bi bi-info-circle"></i>
No hay pagos inicializados para este concepto.
<br><br>
<?php if (Auth::isCapturist()): ?>
<button class="btn btn-primary" onclick="initializeConceptPayments()">
<i class="bi bi-plus-circle"></i> Inicializar Pagos
</button>
<?php endif; ?>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Casa</th>
<th>Propietario</th>
<th>Monto</th>
<th>Fecha de Pago</th>
<?php if (Auth::isCapturist()): ?>
<th>Acciones</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($payments as $payment):
$cellClass = $payment['amount'] > 0 ? 'paid' : 'pending';
?>
<tr data-payment-id="<?= $payment['id'] ?>" data-house-id="<?= $payment['house_id'] ?>">
<td><strong><?= $payment['house_number'] ?></strong></td>
<td><?= htmlspecialchars($payment['owner_name'] ?? '-') ?></td>
<td class="payment-cell <?= $cellClass ?>"
data-amount="<?= $payment['amount'] ?>"
<?= Auth::isCapturist() ? 'contenteditable="true"' : '' ?>>
<?= $payment['amount'] > 0 ? '$' . number_format($payment['amount'], 2) : '-' ?>
</td>
<td><?= $payment['payment_date'] ? date('d/m/Y', strtotime($payment['payment_date'])) : '-' ?></td>
<?php if (Auth::isCapturist()): ?>
<td>
<button class="btn btn-sm btn-primary" onclick="saveConceptPayment(this)">
<i class="bi bi-check"></i>
</button>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<?php
require_once __DIR__ . '/../../models/Expense.php';
$conceptExpenses = $db->fetchAll(
"SELECT e.*, ec.amount as allocated_amount
FROM expense_concept_collections ec
JOIN expenses e ON ec.expense_id = e.id
WHERE ec.concept_id = ?
ORDER BY e.expense_date DESC",
[$concept['id']]
);
?>
<div class="card mt-4">
<div class="card-header">
<h5 class="card-title mb-0">Gastos Asociados</h5>
</div>
<div class="card-body">
<?php if (!empty($conceptExpenses)): ?>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Fecha</th>
<th>Descripción</th>
<th>Categoría</th>
<th>Asignado</th>
<th>Total Gasto</th>
</tr>
</thead>
<tbody>
<?php foreach ($conceptExpenses as $exp): ?>
<tr>
<td><?= date('d/m/Y', strtotime($exp['expense_date'])) ?></td>
<td><?= htmlspecialchars($exp['description']) ?></td>
<td><?= htmlspecialchars($exp['category'] ?? '-') ?></td>
<td class="text-end text-info">$<?= number_format($exp['allocated_amount'], 2) ?></td>
<td class="text-end">$<?= number_format($exp['amount'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr class="table-secondary">
<th colspan="3" class="text-end">Total:</th>
<th class="text-end text-danger">$<?= number_format($status['total_expenses'], 2) ?></th>
<th>-</th>
</tr>
</tfoot>
</table>
</div>
<?php else: ?>
<p class="text-muted mb-0">No hay gastos asociados a este concepto.</p>
<?php endif; ?>
</div>
</div>
<script>
const conceptId = '<?= $concept['id'] ?>';
function initializeConceptPayments() {
console.log('Inicializando pagos...');
console.log('Concept ID:', conceptId);
if (typeof Swal === 'undefined') {
alert('Error: SweetAlert no está cargado');
return;
}
Swal.fire({
title: '¿Inicializar pagos?',
text: 'Esto creará registros de pago para todas las casas activas con monto $0.00. ¿Continuar?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Sí, inicializar',
confirmButtonColor: '#3085d6',
cancelButtonText: 'Cancelar'
}).then(function(result) {
console.log('Resultado del sweetalert:', result);
if (result.isConfirmed) {
console.log('Iniciando fetch...');
console.log('URL:', '/api/initialize_concept_payments.php?concept_id=' + conceptId);
fetch('/api/initialize_concept_payments.php?concept_id=' + conceptId)
.then(function(r) {
console.log('Respuesta del servidor:', r);
return r.json();
})
.then(function(data) {
console.log('Datos recibidos:', data);
if (data.success) {
Swal.fire('Éxito', data.message, 'success').then(function() {
location.reload();
});
} else {
Swal.fire('Error', data.message, 'error');
}
})
.catch(function(error) {
console.error('Error:', error);
Swal.fire('Error', 'Ocurrió un error: ' + error.message, 'error');
});
}
});
}
<?php if (Auth::isCapturist()): ?>
document.querySelectorAll('.payment-cell[contenteditable="true"]').forEach(function(cell) {
cell.addEventListener('focus', function() {
this.classList.add('editing');
});
cell.addEventListener('blur', function() {
this.classList.remove('editing');
});
cell.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
}
});
});
function saveConceptPayment(btn) {
const row = btn.closest('tr');
const cell = row.querySelector('.payment-cell');
const houseId = row.dataset.houseId;
const value = cell.textContent.trim();
const amount = parseFloat(value.replace(/[^0-9.-]+/g, '')) || 0;
cell.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
fetch('/api/save_concept_payment.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
concept_id: '<?= $concept['id'] ?>',
house_id: houseId,
amount: amount
})
})
.then(function(r) {
return r.json();
})
.then(function(data) {
if (data.success) {
location.reload();
} else {
Swal.fire('Error', data.message, 'error');
}
});
}
<?php endif; ?>
</script>

560
views/finance/index.php Executable file
View File

@@ -0,0 +1,560 @@
<div class="row mb-4">
<div class="col-12">
<h2><i class="bi bi-cash-coin"></i> Finanzas</h2>
<p class="text-muted">Gestión de gastos y conceptos especiales</p>
</div>
</div>
<ul class="nav nav-tabs mb-4" id="financeTabs">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#concepts">
<i class="bi bi-collection"></i> Conceptos Especiales
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#expenses">
<i class="bi bi-receipt"></i> Gastos
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="concepts">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Conceptos Especiales</h5>
<?php if (Auth::isCapturist()): ?>
<button id="newConceptBtn" class="btn btn-primary btn-sm">
<i class="bi bi-plus"></i> Nuevo Concepto
</button>
<?php endif; ?>
</div>
<div class="card-body">
<div class="row">
<?php foreach ($concepts as $concept):
$status = CollectionConcept::getCollectionStatus($concept['id']);
?>
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h6 class="card-title">[ID: <?= $concept['id'] ?>] <?= htmlspecialchars($concept['name']) ?></h6>
<p class="card-text small text-muted"><?= htmlspecialchars($concept['description'] ?? '') ?></p>
<div class="mb-2">
<small>Monto Total:</small> <strong>$<?= number_format($concept['total_amount'], 2) ?></strong>
</div>
<div class="mb-2">
<small>Monto por casa:</small> <strong>$<?= number_format($concept['amount_per_house'], 2) ?></strong>
</div>
<div class="progress mb-2" style="height: 20px;">
<div class="progress-bar bg-success" style="width: <?= $status['percentage'] ?>%">
<?= $status['percentage'] ?>%
</div>
</div>
<div class="mb-2">
<small class="text-muted">Recaudado:</small> <strong>$<?= number_format($status['total_collected'], 2) ?></strong>
</div>
<div class="mb-2">
<small class="text-muted">Gastado:</small> <strong class="text-danger">$<?= number_format($status['total_expenses'], 2) ?></strong>
</div>
<div class="mb-2">
<small class="text-muted">Balance Neto:</small> <strong class="<?= $status['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">$<?= number_format($status['balance'], 2) ?></strong>
</div>
<div class="mt-2">
<a href="/dashboard.php?page=concept_view&id=<?= $concept['id'] ?>" class="btn btn-sm btn-outline-primary">
Ver Detalles
</a>
<?php if (Auth::isCapturist()): ?>
<button class="btn btn-sm btn-outline-secondary btn-edit-concept" data-concept-id="<?= $concept['id'] ?>">
<i class="bi bi-pencil"></i> Editar
</button>
<?php endif; ?>
<?php if (Auth::isAdmin()): ?>
<button class="btn btn-sm btn-outline-danger btn-delete-concept" data-concept-id="<?= $concept['id'] ?>">
<i class="bi bi-trash"></i> Eliminar
</button>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="expenses">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Gastos Registrados</h5>
<?php if (Auth::isCapturist()): ?>
<button id="newExpenseBtn" class="btn btn-primary btn-sm">
<i class="bi bi-plus"></i> Nuevo Gasto
</button>
<?php endif; ?>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Fecha</th>
<th>Descripción</th>
<th>Conceptos</th>
<th>Categoría</th>
<th>Comprobante</th>
<th>Monto</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($expenses as $expense):
$expenseConcepts = Expense::getConcepts($expense['id']);
?>
<tr>
<td><?= date('d/m/Y', strtotime($expense['expense_date'])) ?></td>
<td><?= htmlspecialchars($expense['description']) ?></td>
<td>
<?php if (!empty($expenseConcepts)): ?>
<div>
<?php foreach ($expenseConcepts as $ec): ?>
<span class="badge bg-info" title="Monto: $<?= number_format($ec['amount'], 2) ?>">
<?= htmlspecialchars($ec['concept_name']) ?>
</span>
<?php endforeach; ?>
</div>
<?php else: ?>
<span class="text-muted">Sin concepto</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($expense['category'] ?? '-') ?></td>
<td>
<?php if (!empty($expense['receipt_path'])): ?>
<a href="<?= htmlspecialchars($expense['receipt_path']) ?>" target="_blank" class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i> Ver
</a>
<?php else: ?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<td class="text-end">$<?= number_format($expense['amount'], 2) ?></td>
<td>
<?php if (Auth::isCapturist()): ?>
<button class="btn btn-sm btn-outline-primary btn-edit-expense" data-expense-id="<?= $expense['id'] ?>">
<i class="bi bi-pencil"></i> Editar
</button>
<?php endif; ?>
<?php if (Auth::isAdmin()): ?>
<button class="btn btn-sm btn-danger btn-delete-expense" data-expense-id="<?= $expense['id'] ?>">
<i class="bi bi-trash"></i>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<?php if (Auth::isCapturist()): ?>
<div class="modal fade" id="conceptModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="conceptModalTitle">Nuevo Concepto Especial</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="conceptForm">
<div class="modal-body">
<input type="hidden" name="id" id="conceptIdInput">
<div class="mb-3">
<label class="form-label">Nombre del Concepto</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="mb-3">
<label class="form-label">Descripción</label>
<textarea class="form-control" name="description" rows="2"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Monto Total a Recaudar ($)</label>
<input type="number" step="0.01" class="form-control" name="total_amount" id="totalAmountInput" placeholder="Ej: 20000.00">
</div>
<div class="mb-3">
<label class="form-label">Monto por Casa ($)</label>
<input type="number" step="0.01" class="form-control" name="amount_per_house" id="perHouseAmountInput" placeholder="Ej: 200.00">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Fecha del Concepto</label>
<input type="date" class="form-control" name="concept_date" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Fecha de Vencimiento (opcional)</label>
<input type="date" class="form-control" name="due_date">
</div>
</div>
<div class="mb-3">
<label class="form-label">Categoría</label>
<input type="text" class="form-control" name="category" placeholder="Ej: Mantenimiento, Mejora">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary" id="conceptFormSubmitBtn">Guardar</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="expenseModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="expenseModalTitle">Registrar Gasto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="expenseForm">
<input type="hidden" name="id" id="expenseIdInput">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Descripción</label>
<input type="text" class="form-control" name="description" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Monto del Gasto</label>
<input type="number" step="0.01" class="form-control" name="amount" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Fecha</label>
<input type="date" class="form-control" name="expense_date" required>
</div>
</div>
<div class="mb-3">
<label class="form-label">Comprobante (Imagen o PDF)</label>
<input type="file" class="form-control" name="receipt" accept=".jpg,.jpeg,.png,.pdf" id="expenseReceiptInput">
<small class="text-muted" id="existingReceiptHint"></small>
</div>
<div class="mb-3">
<label class="form-label">Asignar Gasto a Conceptos</label>
<div id="expense-concept-allocations" class="p-2 border rounded" style="max-height: 250px; overflow-y: auto;">
<?php if (empty($concepts)): ?>
<p class="text-muted">No hay conceptos especiales para asignar.</p>
<?php else: ?>
<?php foreach ($concepts as $c):
$status = CollectionConcept::getCollectionStatus($c['id']);
?>
<div class="allocation-row mb-2">
<div class="form-check">
<input class="form-check-input concept-checkbox" type="checkbox" value="<?= $c['id'] ?>" id="alloc_concept_<?= $c['id'] ?>">
<label class="form-check-label" for="alloc_concept_<?= $c['id'] ?>">
<?= htmlspecialchars($c['name']) ?>
<small class="text-success">(Balance: $<?= number_format($status['balance'], 2) ?>)</small>
</label>
</div>
<div class="input-group input-group-sm mt-1">
<span class="input-group-text">$</span>
<input type="number" step="0.01" class="form-control allocation-amount" name="allocations[<?= $c['id'] ?>]" placeholder="0.00" disabled>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<div class="alert alert-secondary mt-2">
<div class="d-flex justify-content-between"><span>Monto del Gasto:</span> <strong id="summary-expense-amount">$0.00</strong></div>
<div class="d-flex justify-content-between"><span>Total Asignado:</span> <strong id="summary-allocated-amount">$0.00</strong></div>
<hr class="my-1">
<div class="d-flex justify-content-between fw-bold"><span>Restante:</span> <strong id="summary-remaining-amount">$0.00</strong></div>
</div>
<div class="mb-3">
<label class="form-label">Categoría</label>
<input type="text" class="form-control" name="category" placeholder="Ej: Mantenimiento, Limpieza">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar Gasto</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (!<?= Auth::isCapturist() ? 'true' : 'false' ?>) return;
const expenseModalEl = document.getElementById('expenseModal');
if (!expenseModalEl) return;
// --- Concept Modal ---
const conceptModalEl = document.getElementById('conceptModal');
const conceptModal = conceptModalEl ? new bootstrap.Modal(conceptModalEl) : null;
const conceptForm = document.getElementById('conceptForm');
const openConceptModal = (id = null) => {
conceptForm.reset();
conceptForm.elements['id'].value = '';
if (id) {
conceptModalEl.querySelector('#conceptModalTitle').textContent = 'Editar Concepto Especial';
conceptModalEl.querySelector('#conceptFormSubmitBtn').textContent = 'Guardar Cambios';
fetch(`/dashboard.php?page=finanzas&action=get_concept&id=${id}`).then(r => r.json()).then(res => {
if (res.success && res.data) {
const c = res.data;
conceptForm.elements['id'].value = c.id;
conceptForm.elements['name'].value = c.name || '';
conceptForm.elements['description'].value = c.description || '';
conceptForm.elements['total_amount'].value = c.total_amount || '';
conceptForm.elements['amount_per_house'].value = c.amount_per_house || '';
conceptForm.elements['concept_date'].value = c.concept_date || '';
conceptForm.elements['due_date'].value = c.due_date || '';
conceptForm.elements['category'].value = c.category || '';
conceptModal.show();
} else {
Swal.fire('Error', res.message || 'No se pudo cargar el concepto.', 'error');
}
});
} else {
conceptModalEl.querySelector('#conceptModalTitle').textContent = 'Nuevo Concepto Especial';
conceptModalEl.querySelector('#conceptFormSubmitBtn').textContent = 'Crear Concepto';
conceptModal.show();
}
};
const deleteConcept = (id) => {
if (!<?= Auth::isAdmin() ? 'true' : 'false' ?>) {
Swal.fire('Acceso Denegado', 'No tienes permisos para eliminar conceptos', 'error');
return;
}
Swal.fire({
title: '¿Eliminar Concepto?',
text: 'Esta acción eliminará el concepto, sus pagos y asignaciones de gastos.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sí, eliminar'
}).then(res => {
if (res.isConfirmed) {
fetch(`/dashboard.php?page=finanzas&action=delete_concept&id=${id}`, { method: 'GET' }).then(r => r.json()).then(data => {
if (data.success) {
Swal.fire('Eliminado', data.message, 'success').then(() => location.reload());
} else {
Swal.fire('Error', data.message, 'error');
}
});
}
});
};
document.getElementById('newConceptBtn')?.addEventListener('click', () => openConceptModal());
document.getElementById('concepts').addEventListener('click', e => {
const editBtn = e.target.closest('.btn-edit-concept');
if (editBtn) openConceptModal(editBtn.dataset.conceptId);
const deleteBtn = e.target.closest('.btn-delete-concept');
if (deleteBtn) deleteConcept(deleteBtn.dataset.conceptId);
});
conceptForm.addEventListener('submit', e => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.target).entries());
data.total_amount = data.total_amount ? parseFloat(data.total_amount) : null;
data.amount_per_house = data.amount_per_house ? parseFloat(data.amount_per_house) : null;
fetch('/dashboard.php?page=finanzas&action=save_concept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(r => r.json()).then(res => {
if (res.success) {
conceptModal.hide();
Swal.fire('Éxito', res.message, 'success').then(() => location.reload());
} else {
Swal.fire('Error', res.message || 'Error desconocido.', 'error');
}
});
});
// --- Expense Modal ---
const expenseModal = new bootstrap.Modal(expenseModalEl);
const expenseForm = document.getElementById('expenseForm');
const expenseAmountInput = expenseForm.querySelector('input[name="amount"]');
const allocationContainer = document.getElementById('expense-concept-allocations');
const updateAllocationSummary = () => {
const expenseAmount = parseFloat(expenseAmountInput.value) || 0;
let allocatedAmount = 0;
allocationContainer.querySelectorAll('.allocation-row').forEach(row => {
const amountInput = row.querySelector('.allocation-amount');
if (!amountInput.disabled) {
allocatedAmount += parseFloat(amountInput.value) || 0;
}
});
const remainingAmount = expenseAmount - allocatedAmount;
document.getElementById('summary-expense-amount').textContent = `$${expenseAmount.toFixed(2)}`;
document.getElementById('summary-allocated-amount').textContent = `$${allocatedAmount.toFixed(2)}`;
const remainingEl = document.getElementById('summary-remaining-amount');
remainingEl.textContent = `$${remainingAmount.toFixed(2)}`;
remainingEl.classList.toggle('text-danger', remainingAmount < 0 || (remainingAmount > 0 && allocatedAmount > 0));
};
document.getElementById('newExpenseBtn')?.addEventListener('click', () => {
openExpenseModal();
});
const openExpenseModal = (id = null) => {
expenseForm.reset();
expenseForm.elements['id'].value = '';
document.getElementById('existingReceiptHint').textContent = '';
allocationContainer.querySelectorAll('.allocation-row').forEach(row => {
row.querySelector('.concept-checkbox').checked = false;
const amountInput = row.querySelector('.allocation-amount');
amountInput.value = '';
amountInput.disabled = true;
});
updateAllocationSummary();
if (id) {
expenseModalEl.querySelector('#expenseModalTitle').textContent = 'Editar Gasto';
fetch(`/dashboard.php?page=finanzas&action=get_expense&id=${id}`).then(r => r.json()).then(res => {
if (res.success && res.data) {
const e = res.data;
expenseForm.elements['id'].value = e.id;
expenseForm.elements['description'].value = e.description || '';
expenseForm.elements['amount'].value = e.amount || '';
expenseForm.elements['expense_date'].value = e.expense_date || '';
expenseForm.elements['category'].value = e.category || '';
if (e.receipt_path) {
document.getElementById('existingReceiptHint').textContent = 'Comprobante existente: ' + e.receipt_path.split('/').pop();
}
// Load allocations
if (e.allocations && e.allocations.length > 0) {
allocationContainer.querySelectorAll('.allocation-row').forEach(row => {
const checkbox = row.querySelector('.concept-checkbox');
const amountInput = row.querySelector('.allocation-amount');
const alloc = e.allocations.find(a => a.concept_id == checkbox.value);
if (alloc) {
checkbox.checked = true;
amountInput.value = alloc.amount;
amountInput.disabled = false;
}
});
updateAllocationSummary();
}
expenseModal.show();
} else {
Swal.fire('Error', res.message || 'No se pudo cargar el gasto.', 'error');
}
});
} else {
expenseModalEl.querySelector('#expenseModalTitle').textContent = 'Registrar Gasto';
expenseModal.show();
}
};
expenseAmountInput.addEventListener('input', updateAllocationSummary);
allocationContainer.addEventListener('change', e => {
if (e.target.classList.contains('concept-checkbox')) {
e.target.closest('.allocation-row').querySelector('.allocation-amount').disabled = !e.target.checked;
if (!e.target.checked) e.target.closest('.allocation-row').querySelector('.allocation-amount').value = '';
}
updateAllocationSummary();
});
allocationContainer.addEventListener('input', e => {
if (e.target.classList.contains('allocation-amount')) updateAllocationSummary();
});
expenseForm.addEventListener('submit', e => {
e.preventDefault();
const formData = new FormData(expenseForm);
formData.append('id', expenseForm.elements['id'].value);
formData.append('description', expenseForm.elements['description'].value);
formData.append('amount', expenseForm.elements['amount'].value);
formData.append('expense_date', expenseForm.elements['expense_date'].value);
formData.append('category', expenseForm.elements['category'].value);
const allocations = [];
allocationContainer.querySelectorAll('.allocation-row').forEach(row => {
const checkbox = row.querySelector('.concept-checkbox');
const amountInput = row.querySelector('.allocation-amount');
if (checkbox.checked) {
const amount = parseFloat(amountInput.value) || 0;
if (amount > 0) {
allocations.push({ concept_id: checkbox.value, amount: amount });
}
}
});
formData.append('allocations', JSON.stringify(allocations));
const allocatedTotal = allocations.reduce((sum, a) => sum + a.amount, 0);
const expenseAmount = parseFloat(expenseForm.elements['amount'].value);
if (allocations.length > 0 && Math.abs(expenseAmount - allocatedTotal) > 0.01) {
Swal.fire('Error', 'El total asignado a los conceptos no coincide con el monto del gasto.', 'error');
return;
}
Swal.fire({
title: 'Guardando...',
text: 'Procesando gasto y comprobante',
allowOutsideClick: false,
didOpen: () => Swal.showLoading()
});
fetch('/dashboard.php?page=finanzas&action=save_expense', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(res => {
if (res.success) {
expenseModal.hide();
Swal.fire('Éxito', res.message, 'success').then(() => location.reload());
} else {
Swal.fire('Error', res.message || 'Error desconocido.', 'error');
}
})
.catch(error => {
Swal.fire('Error', 'Error de conexión', 'error');
});
});
// Delegate deleteExpense and editExpense events from main table
document.getElementById('expenses')?.addEventListener('click', e => {
const deleteBtn = e.target.closest('.btn-delete-expense');
if(deleteBtn) deleteExpense(deleteBtn.dataset.expenseId);
const editBtn = e.target.closest('.btn-edit-expense');
if(editBtn) openExpenseModal(editBtn.dataset.expenseId);
});
const deleteExpense = (id) => {
if (!<?= Auth::isAdmin() ? 'true' : 'false' ?>) {
Swal.fire('Acceso Denegado', 'No tienes permisos para eliminar gastos', 'error');
return;
}
Swal.fire({
title: '¿Eliminar Gasto?',
text: 'Esta acción no se puede deshacer.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sí, eliminar'
}).then(res => {
if (res.isConfirmed) {
fetch(`/dashboard.php?page=finanzas&action=delete_expense&id=${id}`, { method: 'GET' }).then(r => r.json()).then(data => {
if (data.success) location.reload();
});
}
});
}
});
</script>
<?php endif; ?>

152
views/houses/index.php Executable file
View File

@@ -0,0 +1,152 @@
<div class="row mb-4">
<div class="col-12">
<h2><i class="bi bi-building"></i> Gestión de Casas</h2>
<p class="text-muted">Administración del registro de propietarios</p>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-dark">
<tr>
<th>Número</th>
<th>Estado</th>
<th>Consumo Only</th>
<th>Propietario</th>
<th>Email</th>
<th>Teléfono</th>
<th>Ver</th>
<?php if (Auth::isAdmin()): ?>
<th>Acciones</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($houses as $house): ?>
<tr>
<td><strong><?= $house['number'] ?></strong></td>
<td>
<span class="badge <?= $house['status'] == 'activa' ? 'bg-success' : 'bg-secondary' ?>">
<?= $house['status'] == 'activa' ? 'Activa' : 'Deshabitada' ?>
</span>
</td>
<td>
<?php if ($house['consumption_only']): ?>
<span class="badge bg-warning" title="Solo consumo">Sí</span>
<?php else: ?>
<span class="text-muted">No</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($house['owner_name'] ?? '-') ?></td>
<td><?= htmlspecialchars($house['owner_email'] ?? '-') ?></td>
<td><?= htmlspecialchars($house['owner_phone'] ?? '-') ?></td>
<td>
<a href="/dashboard.php?page=house_view&id=<?= $house['id'] ?>" class="btn btn-sm btn-info">
<i class="bi bi-eye"></i>
</a>
</td>
<?php if (Auth::isAdmin()): ?>
<td>
<button class="btn btn-sm btn-primary" onclick="editHouse(<?= $house['id'] ?>)">
<i class="bi bi-pencil"></i>
</button>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php if (Auth::isAdmin()): ?>
<div class="modal fade" id="houseModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Editar Casa</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="houseForm">
<div class="modal-body">
<input type="hidden" id="houseId" name="id">
<div class="mb-3">
<label class="form-label">Estado</label>
<select class="form-select" name="status" required>
<option value="activa">Activa</option>
<option value="deshabitada">Deshabitada</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Solo Consumo</label>
<select class="form-select" name="consumption_only">
<option value="0">No</option>
<option value="1">Sí (Descuento $100)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Propietario</label>
<input type="text" class="form-control" name="owner_name">
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="owner_email">
</div>
<div class="mb-3">
<label class="form-label">Teléfono</label>
<input type="text" class="form-control" name="owner_phone">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar</button>
</div>
</form>
</div>
</div>
</div>
<script>
let housesData = <?= json_encode($houses) ?>;
function editHouse(id) {
const house = housesData.find(h => h.id === id);
if (house) {
document.getElementById('houseId').value = house.id;
document.querySelector('[name="status"]').value = house.status;
document.querySelector('[name="consumption_only"]').value = house.consumption_only;
document.querySelector('[name="owner_name"]').value = house.owner_name || '';
document.querySelector('[name="owner_email"]').value = house.owner_email || '';
document.querySelector('[name="owner_phone"]').value = house.owner_phone || '';
new bootstrap.Modal(document.getElementById('houseModal')).show();
}
}
document.getElementById('houseForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData.entries());
data.consumption_only = parseInt(data.consumption_only);
data.consumption_only = parseInt(data.consumption_only);
fetch('/dashboard.php?page=house_actions&action=save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(r => r.json())
.then(result => {
if (result.success) {
Swal.fire('Éxito', 'Casa actualizada', 'success').then(() => location.reload());
} else {
Swal.fire('Error', result.message, 'error');
}
});
});
</script>
<?php endif; ?>

164
views/houses/view.php Executable file
View File

@@ -0,0 +1,164 @@
<div class="row mb-4">
<div class="col-12">
<a href="/dashboard.php?page=casas" class="btn btn-outline-secondary mb-2">
<i class="bi bi-arrow-left"></i> Volver a Casas
</a>
<h2><i class="bi bi-building"></i> Casa <?= $house['number'] ?></h2>
<p class="text-muted">Vista unificada de pagos y finanzas</p>
</div>
</div>
<div class="row mb-4">
<div class="col-md-3">
<div class="card">
<div class="card-header">Información</div>
<div class="card-body">
<p><strong>Número:</strong> <?= $house['number'] ?></p>
<p><strong>Estado:</strong>
<span class="badge <?= $house['status'] == 'activa' ? 'bg-success' : 'bg-secondary' ?>">
<?= $house['status'] == 'activa' ? 'Activa' : 'Deshabitada' ?>
</span>
</p>
<p><strong>Consumo Only:</strong>
<?= $house['consumption_only'] ? '<span class="badge bg-warning">Sí</span>' : 'No' ?>
</p>
<p><strong>Propietario:</strong> <?= htmlspecialchars($house['owner_name'] ?? '-') ?></p>
<p><strong>Email:</strong> <?= htmlspecialchars($house['owner_email'] ?? '-') ?></p>
<p><strong>Teléfono:</strong> <?= htmlspecialchars($house['owner_phone'] ?? '-') ?></p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-success">
<div class="card-body text-center">
<h6 class="text-muted">Total Pagado Agua (Histórico)</h6>
<h3 class="text-success">$<?= number_format($totalWaterPayments, 2) ?></h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-info">
<div class="card-body text-center">
<h6 class="text-muted">Total Conceptos</h6>
<h3 class="text-info">$<?= number_format($totalConceptPayments, 2) ?></h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-primary">
<div class="card-body text-center">
<h6 class="text-muted">Total General</h6>
<h3 class="text-primary">$<?= number_format($totalWaterPayments + $totalConceptPayments, 2) ?></h3>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs mb-4" id="houseTabs">
<li class="nav-item">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#waterPayments">
<i class="bi bi-droplet-fill"></i> Pagos de Agua
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#conceptPayments">
<i class="bi bi-collection"></i> Conceptos Especiales
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#activityLog">
<i class="bi bi-clock-history"></i> Historial
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="waterPayments">
<div class="card">
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Mes</th>
<th>Año</th>
<th>Monto</th>
<th>Fecha de Pago</th>
<th>Método</th>
</tr>
</thead>
<tbody>
<?php foreach ($waterPayments as $payment):
$expected = Payment::getExpectedAmount($house, $payment['year'], $payment['month']);
$statusClass = $payment['amount'] >= $expected ? 'paid' : 'pending';
?>
<tr>
<td><?= $payment['month'] ?></td>
<td><?= $payment['year'] ?></td>
<td class="<?= $statusClass ?>">$<?= number_format($payment['amount'], 2) ?></td>
<td><?= $payment['payment_date'] ? date('d/m/Y', strtotime($payment['payment_date'])) : '-' ?></td>
<td><?= htmlspecialchars($payment['payment_method'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade" id="conceptPayments">
<div class="card">
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Concepto</th>
<th>Monto</th>
<th>Fecha de Pago</th>
<th>Notas</th>
</tr>
</thead>
<tbody>
<?php foreach ($conceptPayments as $payment): ?>
<tr>
<td><?= htmlspecialchars($payment['concept_name']) ?></td>
<td>$<?= number_format($payment['amount'], 2) ?></td>
<td><?= $payment['payment_date'] ? date('d/m/Y', strtotime($payment['payment_date'])) : '-' ?></td>
<td><?= htmlspecialchars($payment['notes'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="tab-pane fade" id="activityLog">
<div class="card">
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Fecha</th>
<th>Acción</th>
<th>Detalles</th>
<th>Usuario</th>
</tr>
</thead>
<tbody>
<?php foreach ($activityLogs as $log): ?>
<tr>
<td><?= date('d/m/Y H:i', strtotime($log['created_at'])) ?></td>
<td><?= htmlspecialchars($log['action']) ?></td>
<td><?= htmlspecialchars($log['details'] ?? '-') ?></td>
<td><?= htmlspecialchars($log['username'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>

269
views/import/index.php Executable file
View File

@@ -0,0 +1,269 @@
<div class="row mb-4">
<div class="col-12">
<h2><i class="bi bi-file-earmark-arrow-up"></i> Importación de Datos</h2>
<p class="text-muted">Importación masiva de datos al sistema (formato CSV)</p>
</div>
</div>
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Seleccionar Tipo de Importación</h5>
</div>
<div class="card-body">
<div class="mb-4">
<label class="form-label">¿Qué deseas importar?</label>
<select id="importTypeSelector" class="form-select form-select-lg">
<option value="">-- Selecciona una opción --</option>
<option value="houses">Casas</option>
<option value="payments">Pagos de Agua</option>
<option value="expenses">Gastos</option>
<option value="concept_payments">Pagos por Concepto Especial</option>
</select>
</div>
<div id="importArea" style="display: none;">
<form id="importForm">
<input type="hidden" name="type" id="importTypeInput">
<div id="dynamicFields"></div>
<div class="mb-3">
<label class="form-label">Archivo CSV</label>
<input type="file" class="form-control" name="file" accept=".csv" required>
</div>
<div id="formatInfo" class="alert alert-info small mb-3"></div>
<div class="d-flex gap-2">
<button type="button" id="downloadTemplateBtn" class="btn btn-outline-primary">
<i class="bi bi-download"></i> Descargar Plantilla
</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-upload"></i> Importar
</button>
</div>
</form>
</div>
<div id="placeholder" class="text-center py-5 text-muted">
<i class="bi bi-arrow-up-circle display-4"></i>
<p class="mt-3">Selecciona un tipo de importación para continuar</p>
</div>
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5 class="card-title mb-0"><i class="bi bi-clock-history"></i> Historial de Importaciones</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Fecha</th>
<th>Tipo</th>
<th>Registros</th>
<th>Estado</th>
<th>Usuario</th>
</tr>
</thead>
<tbody id="importHistory">
<tr><td colspan="5" class="text-muted">No hay importaciones recientes</td></tr>
</tbody>
</table>
</div>
</div>
<?php
$conceptsJson = json_encode($concepts);
?>
<script>
const conceptsData = <?= $conceptsJson ?>;
const importTypes = {
houses: {
title: 'Importar Casas',
description: 'Importa o actualiza la información de las casas del condominio.',
fieldsHtml: '',
format: 'number,status,consumption_only,owner_name,owner_email,owner_phone',
examples: '001,activa,0,Juan Pérez,juan@email.com,555-1234\n002,activa,1,Maria García,maria@email.com,555-5678\n003,deshabitada,0,,,',
template: 'number,status,consumption_only,owner_name,owner_email,owner_phone\n001,activa,0,Juan Pérez,juan@email.com,555-1234\n002,activa,1,Maria García,maria@email.com,555-5678\n003,deshabitada,0,,,',
filename: 'plantilla_importar_casas.csv'
},
payments: {
title: 'Importar Pagos de Agua',
description: 'Importa pagos mensuales de agua para las casas.',
fieldsHtml: '',
format: 'year,house_number,month,amount,payment_date,payment_method,notes',
examples: '2025,001,Enero,250.00,2025-01-15,Efectivo,Pago mensual\n2025,002,Enero,250.00,2025-01-16,Transferencia,Pago transferencia\n2024,003,Diciembre,250.00,2024-12-20,Efectivo,Pago diciembre 2024',
template: 'year,house_number,month,amount,payment_date,payment_method,notes\n2025,001,Enero,250.00,2025-01-15,Efectivo,Pago mensual\n2025,002,Enero,250.00,2025-01-16,Transferencia,Pago transferencia\n2024,003,Diciembre,250.00,2024-12-20,Efectivo,Pago diciembre 2024',
filename: 'plantilla_importar_pagos_agua.csv'
},
expenses: {
title: 'Importar Gastos',
description: 'Importa gastos del condominio.',
fieldsHtml: '',
format: 'description,amount,expense_date,category,notes',
examples: 'Mantenimiento jardín,150.00,2025-01-10,Mantenimiento,Pago jardinero\nCompra insumos,75.50,2025-01-12,Insumos,Tornillos y clavos',
template: 'description,amount,expense_date,category,notes\nMantenimiento jardín,150.00,2025-01-10,Mantenimiento,Pago jardinero\nCompra insumos,75.50,2025-01-12,Insumos,Tornillos y clavos',
filename: 'plantilla_importar_gastos.csv'
},
concept_payments: {
title: 'Importar Pagos por Concepto Especial',
description: 'Importa pagos para uno o varios conceptos especiales.',
format: 'concept_id,house_number,amount,payment_date,notes',
examples: '1,001,200.00,2025-01-15,Pago cuota\n1,002,200.00,2025-01-16,Pago cuota\n2,003,300.00,2025-01-17,Pago fondo',
template: 'concept_id,house_number,amount,payment_date,notes\n1,001,200.00,2025-01-15,Pago cuota\n1,002,200.00,2025-01-16,Pago cuota\n2,003,300.00,2025-01-17,Pago fondo',
filename: 'plantilla_importar_pagos_concepto.csv'
}
};
document.getElementById('importTypeSelector').addEventListener('change', function() {
const type = this.value;
const importArea = document.getElementById('importArea');
const placeholder = document.getElementById('placeholder');
const dynamicFields = document.getElementById('dynamicFields');
const formatInfo = document.getElementById('formatInfo');
if (!type) {
importArea.style.display = 'none';
placeholder.style.display = 'block';
return;
}
const config = importTypes[type];
document.getElementById('importTypeInput').value = type;
let fieldsHtml = '';
let conceptHelp = '';
if (type === 'concept_payments') {
conceptHelp = '<div class="alert alert-secondary mb-3"><strong>Conceptos disponibles (ID - Nombre):</strong><br>';
conceptsData.forEach(function(c, index) {
const separator = index === conceptsData.length - 1 ? '' : ' | ';
conceptHelp += '<code>' + c.id + ' - ' + c.name + '</code>' + separator;
});
conceptHelp += '</div>';
fieldsHtml += conceptHelp;
}
dynamicFields.innerHTML = fieldsHtml;
formatInfo.innerHTML = '<strong>Formato esperado:</strong><br>' + config.format + '<br><br>' +
'<em>Ejemplo:</em><br><code>' + config.examples.replace(/\n/g, '<br>') + '</code><br><br>' +
'<small class="text-muted">' + config.description + '</small>';
importArea.style.display = 'block';
placeholder.style.display = 'none';
});
document.getElementById('downloadTemplateBtn').addEventListener('click', function() {
const type = document.getElementById('importTypeSelector').value;
if (type) {
downloadTemplate(type);
}
});
function downloadTemplate(type) {
const config = importTypes[type];
const csv = config.template;
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = config.filename;
link.click();
}
document.getElementById('importForm').addEventListener('submit', importData);
function importData(e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const file = formData.get('file');
if (!file) {
Swal.fire('Error', 'Debe seleccionar un archivo', 'error');
return;
}
Swal.fire({
title: 'Importando...',
text: 'Procesando archivo, por favor espere',
allowOutsideClick: false,
didOpen: () => Swal.showLoading()
});
fetch('/dashboard.php?page=import_actions&action=import', {
method: 'POST',
body: formData
})
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire('Éxito', data.message, 'success').then(() => location.reload());
} else {
Swal.fire('Error', data.message || 'Error al importar', 'error');
}
})
.catch(error => {
Swal.fire('Error', 'Error de conexión', 'error');
});
}
function loadImportHistory() {
fetch('/dashboard.php?page=import_actions&action=history')
.then(r => r.json())
.then(data => {
if (data.success && data.data.length > 0) {
const tbody = document.getElementById('importHistory');
tbody.innerHTML = '';
data.data.forEach(log => {
const row = document.createElement('tr');
// Extraer tipo y registros de los detalles
const details = log.details || '';
let type = 'Desconocido';
let records = '0';
if (details.includes('Importación houses')) {
type = 'Casas';
} else if (details.includes('Importación payments')) {
type = 'Pagos de Agua';
} else if (details.includes('Importación expenses')) {
type = 'Gastos';
} else if (details.includes('Importación concept_payments')) {
type = 'Pagos por Concepto';
}
const recordsMatch = details.match(/(\d+)\s+registros/);
if (recordsMatch) {
records = recordsMatch[1];
}
row.innerHTML = `
<td>${new Date(log.created_at).toLocaleString('es-MX')}</td>
<td>${type}</td>
<td>${records}</td>
<td><span class="badge bg-success">Exitoso</span></td>
<td>${log.username || 'N/A'}</td>
`;
tbody.appendChild(row);
});
}
})
.catch(error => {
console.error('Error al cargar historial:', error);
});
}
// Cargar historial al iniciar la página
document.addEventListener('DOMContentLoaded', loadImportHistory);
</script>

125
views/layout/base.php Executable file
View File

@@ -0,0 +1,125 @@
<!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">
<script>
// Prevenir FOUC (Flash of Unstyled Content)
(function() {
const theme = localStorage.getItem('theme') || 'light';
if (theme === 'dark') {
document.documentElement.classList.add('dark-mode');
}
})();
</script>
</head>
<body>
<?php if (Auth::check()): ?>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="/dashboard.php">
<i class="bi bi-house-door-fill"></i> IBIZA CEA
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<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">
<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">
<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">
<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">
<i class="bi bi-cash-coin"></i> Finanzas
</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">
<i class="bi bi-file-earmark-arrow-up"></i> Importar
</a>
</li>
<?php endif; ?>
</ul>
<ul class="navbar-nav">
<li class="nav-item d-flex align-items-center">
<button id="theme-toggle" class="nav-link" style="background: none; border: none;">
<i id="theme-icon" class="bi bi-sun-fill"></i>
</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>
<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; ?>
<?php if (Auth::isAdmin()): ?>
<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; ?>
<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; ?>
</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="<?= 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>

560
views/payments/index.php Executable file
View File

@@ -0,0 +1,560 @@
<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>
<button onclick="exportToCSV()" class="btn btn-primary">
<i class="bi bi-file-earmark-csv"></i> Exportar CSV
</button>
<?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;
foreach ($houses as $house):
$total = 0;
$totalExpected = 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;
$expected = Payment::getExpectedAmount($house, $year, $month);
$total += $amount;
$totalExpected += $expected;
$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 - $totalExpected;
$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; ?>
<?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;
?>
<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);
$total += $amount;
$totalExpected += $expected;
$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 - $totalExpected;
$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; ?>
</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>

117
views/payments/pdf_template.php Executable file
View File

@@ -0,0 +1,117 @@
<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;
}
.bg-success { background-color: #d4edda; }
.bg-warning { background-color: #fff3cd; }
.bg-danger { background-color: #f8d7da; }
.bg-secondary { background-color: #e2e3e5; }
</style>
<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><?= substr($month, 0, 3) ?></th>
<?php endforeach; ?>
<th>Debe/Excedente</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<?php
$grandTotal = 0;
$grandTotalExpected = 0;
foreach ($houses as $house):
$total = 0;
$totalExpected = 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);
$total += $amount;
$totalExpected += $expected;
$bg_color = '#FFFFFF'; // Default white
if ($house['status'] == 'deshabitada') {
$bg_color = '#e2e3e5'; // Gris
} elseif ($amount > 0) {
if ($expected > 0 && $amount >= $expected) {
$bg_color = '#d4edda'; // Verde
} else {
$bg_color = '#fff3cd'; // Amarillo
}
} elseif ($amount == 0 && $expected > 0) {
$bg_color = '#f8d7da'; // Rojo
}
?>
<td style="background-color: <?= $bg_color ?>;">
<?= $amount > 0 ? '$' . number_format($amount, 2) : '-' ?>
</td>
<?php endforeach; ?>
<?php
$difference = $total - $totalExpected;
$diffColor = $difference < 0 ? 'red' : 'green';
$diffText = $difference == 0 ? '$0.00' : '$' . number_format($difference, 2);
$grandTotal += $total;
$grandTotalExpected += $totalExpected;
?>
<td style="color: <?= $diffColor ?>;"><?= $diffText ?></td>
<td><strong>$<?= number_format($total, 2) ?></strong></td>
</tr>
<?php endforeach; ?>
<?php
$grandDifference = $grandTotal - $grandTotalExpected;
$grandDiffClass = $grandDifference < 0 ? 'text-danger' : 'text-success';
$grandDiffText = $grandDifference == 0 ? '$0.00' : '$' . number_format($grandDifference, 2);
?>
<tr>
<td colspan="<?= count($months) + 2 ?>" style="text-align: right; font-weight: bold;">TOTALES:</td>
<td style="text-align: center; font-weight: bold; color: <?= $grandDiffClass == 'text-danger' ? 'red' : 'green' ?>;"><?= $grandDiffText ?></td>
<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 completo</span>
<span style="background-color: #fff3cd; padding: 2px 8px; margin: 2px; border: 1px solid #ccc;">Amarillo = Pago parcial</span>
<span style="background-color: #f8d7da; padding: 2px 8px; margin: 2px; border: 1px solid #ccc;">Rojo = Sin pago</span>
<span style="background-color: #e2e3e5; padding: 2px 8px; margin: 2px; border: 1px solid #ccc;">Gris = Casa deshabitada</span>
</div>

467
views/reports/index.php Executable file
View File

@@ -0,0 +1,467 @@
<div class="row mb-4">
<div class="col-12">
<h2><i class="bi bi-file-earmark-bar-graph"></i> Reportes</h2>
<p class="text-muted">Balance general y reportes financieros</p>
</div>
</div>
<div class="mb-4">
<div class="btn-group" role="group">
<a href="/dashboard.php?page=reportes" class="btn btn-outline-primary <?= ($_GET['type'] ?? 'general') == 'general' ? 'active' : '' ?>">
<i class="bi bi-bar-chart"></i> Balance General
</a>
<a href="/dashboard.php?page=reportes&type=water-debtors" class="btn btn-outline-danger <?= ($_GET['type'] ?? '') == 'water-debtors' ? 'active' : '' ?>">
<i class="bi bi-droplet-fill"></i> Deudores de Agua
</a>
<a href="/dashboard.php?page=reportes&type=concept-debtors" class="btn btn-outline-warning <?= ($_GET['type'] ?? '') == 'concept-debtors' ? 'active' : '' ?>">
<i class="bi bi-cash-coin"></i> Deudores de Conceptos
</a>
</div>
</div>
<?php $reportType = $_GET['type'] ?? 'general'; ?>
<?php if ($reportType == 'water-debtors' && isset($waterDebtors)): ?>
<?php
$hasFilters = !empty($waterDebtors['filters']['year']) || !empty($waterDebtors['filters']['months']) || !empty($waterDebtors['filters']['house_id']);
$filterText = [];
if (!empty($waterDebtors['filters']['year'])) {
$filterText[] = "Año: " . $waterDebtors['filters']['year'];
}
if (!empty($waterDebtors['filters']['months'])) {
$filterText[] = "Meses: " . implode(', ', $waterDebtors['filters']['months']);
}
if (!empty($waterDebtors['filters']['house_id'])) {
require_once __DIR__ . '/../../models/House.php';
$house = House::findById($waterDebtors['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 Agua
<?php if ($hasFilters): ?>
<span class="badge bg-info 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="waterDebtorsFilter">
<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 <= 2025; $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">Meses</label>
<div class="d-flex flex-wrap gap-2">
<?php
$allMonths = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
$selectedMonths = explode(',', $_GET['filter_months'] ?? '');
foreach ($allMonths as $m): ?>
<div class="form-check">
<input type="checkbox" name="filter_months[]" value="<?= $m ?>"
class="form-check-input month-checkbox"
id="month_<?= $m ?>"
<?= in_array($m, $selectedMonths) ? 'checked' : '' ?>>
<label class="form-check-label" for="month_<?= $m ?>"><?= substr($m,0,3) ?></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=water-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('waterDebtorsFilter').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 selectedMonths = formData.getAll('filter_months[]');
if (selectedMonths.length > 0) {
params.append('filter_months', selectedMonths.join(','));
}
window.location.href = '/dashboard.php?page=reportes&type=water-debtors&' + params.toString();
});
</script>
<?php if ($reportType == 'water-debtors' && isset($waterDebtors)): ?>
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card border-danger">
<div class="card-body">
<h6 class="text-muted">Total Adeudado (Agua - <?= $year ?>)</h6>
<h3 class="text-danger">$<?= number_format($waterDebtors['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($waterDebtors['total_expected'], 2) ?></h3>
<small class="text-muted">Total de cuotas mensuales</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($waterDebtors['total_paid'], 2) ?></h3>
<small class="text-muted">Total de pagos realizados</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 Pago de Agua</h5>
<button onclick="exportWaterDebtorsPDF()" class="btn btn-outline-danger btn-sm">
<i class="bi bi-file-earmark-pdf"></i> Exportar PDF
</button>
</div>
<div class="card-body">
<?php if (empty($waterDebtors['debtors'])): ?>
<p class="text-muted">No hay deudores registrados</p>
<?php else: ?>
<table class="table table-sm table-bordered">
<thead class="table-danger">
<tr>
<th>Casa</th>
<th>Propietario</th>
<th>Meses Adeudados</th>
<th>Total Debe</th>
</tr>
</thead>
<tbody>
<?php foreach ($waterDebtors['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['months_due'] as $month): ?>
<tr>
<td><?= $month['year'] ?> - <?= $month['month'] ?></td>
<td class="text-end">$<?= number_format($month['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($waterDebtors['total_due'], 2) ?></th>
</tr>
</tfoot>
</table>
<?php endif; ?>
</div>
</div>
<script>
function exportWaterDebtorsPDF() {
const params = new URLSearchParams();
<?php if (!empty($waterDebtors['filters']['year'])): ?>
params.append('filter_year', <?= $waterDebtors['filters']['year'] ?>);
<?php endif; ?>
<?php if (!empty($waterDebtors['filters']['months'])): ?>
params.append('filter_months', '<?= implode(',', $waterDebtors['filters']['months']) ?>');
<?php endif; ?>
<?php if (!empty($waterDebtors['filters']['house_id'])): ?>
params.append('filter_house', <?= $waterDebtors['filters']['house_id'] ?>);
<?php endif; ?>
window.open('/dashboard.php?page=reportes_actions&action=export_pdf_report&type=water-debtors&' + params.toString(), '_blank');
}
</script>
<?php endif; ?>
<?php elseif ($reportType == 'concept-debtors' && isset($conceptDebtors)): ?>
<div class="row g-4 mb-4">
<div class="col-md-12">
<div class="card border-warning">
<div class="card-body">
<h6 class="text-muted">Total Adeudado (Conceptos)</h6>
<h3 class="text-warning">$<?= number_format($conceptDebtors['total_due'], 2) ?></h3>
<small class="text-muted">Total general de deudas de conceptos especiales</small>
</div>
</div>
</div>
</div>
<?php foreach ($conceptDebtors['debtors'] as $concept): ?>
<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-collection"></i> <?= htmlspecialchars($concept['concept_name']) ?>
<span class="badge bg-warning ms-2">$<?= number_format($concept['total_due'], 2) ?> adeudado</span>
</h5>
<small class="text-muted">
Esperado: $<?= number_format($concept['total_expected'], 2) ?> |
Recaudado: $<?= number_format($concept['total_collected'], 2) ?> |
Pendiente: $<?= number_format($concept['total_due'], 2) ?>
</small>
</div>
<div class="card-body">
<table class="table table-sm table-bordered">
<thead class="table-warning">
<tr>
<th>Casa</th>
<th>Propietario</th>
<th>Monto Esperado</th>
<th>Pagado</th>
<th>Adeuda</th>
</tr>
</thead>
<tbody>
<?php foreach ($concept['house_debtors'] as $house): ?>
<tr>
<td><strong><?= $house['house_number'] ?></strong></td>
<td><?= htmlspecialchars($house['owner_name'] ?? '-') ?></td>
<td class="text-end">$<?= number_format($house['expected'], 2) ?></td>
<td class="text-end">$<?= number_format($house['paid'], 2) ?></td>
<td class="text-end fw-bold text-warning">$<?= number_format($house['due'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="table-dark">
<tr>
<th colspan="4" class="text-end">Total:</th>
<th class="text-end">$<?= number_format($concept['total_due'], 2) ?></th>
</tr>
</tfoot>
</table>
</div>
</div>
<?php endforeach; ?>
<div class="card border-warning">
<div class="card-body">
<div class="row">
<div class="col-md-12 text-center">
<h5>Total General Adeudado en Todos los Conceptos</h5>
<h3 class="text-warning">$<?= number_format($conceptDebtors['total_due'], 2) ?></h3>
<button onclick="exportConceptDebtorsPDF()" class="btn btn-outline-warning">
<i class="bi bi-file-earmark-pdf"></i> Exportar PDF
</button>
</div>
</div>
</div>
</div>
<script>
function exportConceptDebtorsPDF() {
window.open('/dashboard.php?page=reportes_actions&action=export_pdf_report&type=concept-debtors', '_blank');
}
</script>
<?php else: ?>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card border-success">
<div class="card-body">
<h6 class="text-muted">Ingresos Totales</h6>
<h4 class="text-success">$<?= number_format($balance['total_incomes'], 2) ?></h4>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-primary">
<div class="card-body">
<h6 class="text-muted">Conceptos</h6>
<h4 class="text-primary">$<?= number_format($balance['concept_incomes'], 2) ?></h4>
</div>
</div>
</div>
<?php if (!Auth::isLector()): ?>
<div class="col-md-3">
<div class="card border-danger">
<div class="card-body">
<h6 class="text-muted">Egresos</h6>
<h4 class="text-danger">$<?= number_format($balance['total_expenses'], 2) ?></h4>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card <?= $balance['balance'] >= 0 ? 'border-success' : 'border-danger' ?>">
<div class="card-body">
<h6 class="text-muted">Balance Neto</h6>
<h4 class="<?= $balance['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">
$<?= number_format($balance['balance'], 2) ?>
</h4>
</div>
</div>
</div>
<?php endif; ?>
</div>
<div class="row">
<?php if (!Auth::isLector()): ?>
<div class="col-md-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Gastos por Categoría</h5>
<button onclick="exportExpensesPDF()" class="btn btn-outline-danger btn-sm">
<i class="bi bi-file-earmark-pdf"></i> PDF
</button>
</div>
<div class="card-body">
<?php if (empty($expensesByCategory)): ?>
<p class="text-muted">No hay gastos registrados</p>
<?php else: ?>
<table class="table table-sm">
<thead>
<tr>
<th>Categoría</th>
<th class="text-end">Monto</th>
</tr>
</thead>
<tbody>
<?php foreach ($expensesByCategory as $cat): ?>
<tr>
<td><?= htmlspecialchars($cat['category'] ?? 'Sin categoría') ?></td>
<td class="text-end">$<?= number_format($cat['total'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
<div class="col-md-<?= Auth::isLector() ? '12' : '6' ?>">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Resumen Financiero</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>
</div>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5 class="card-title mb-0">Exportar Reportes</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<button onclick="exportBalancePDF()" class="btn btn-outline-primary w-100">
<i class="bi bi-file-earmark-bar-graph"></i> Balance General (PDF)
</button>
</div>
<div class="col-md-3">
<button onclick="exportBalanceCSV()" class="btn btn-outline-success w-100">
<i class="bi bi-file-earmark-csv"></i> Balance General (CSV)
</button>
</div>
<?php if (!Auth::isLector()): ?>
<div class="col-md-3">
<button onclick="exportExpensesPDF()" class="btn btn-outline-danger w-100">
<i class="bi bi-receipt"></i> Gastos (PDF)
</button>
</div>
<div class="col-md-3">
<button onclick="exportExpensesCSV()" class="btn btn-outline-secondary w-100">
<i class="bi bi-file-earmark-csv"></i> Gastos (CSV)
</button>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
function exportBalancePDF() {
window.open('/dashboard.php?page=reportes_actions&action=export_pdf_report&type=balance', '_blank');
}
function exportBalanceCSV() {
window.open('/dashboard.php?page=reportes_actions&action=export_csv_balance', '_blank');
}
function exportExpensesPDF() {
window.open('/dashboard.php?page=reportes_actions&action=export_pdf_report&type=expenses', '_blank');
}
function exportExpensesCSV() {
window.open('/dashboard.php?page=reportes_actions&action=export_csv_expenses', '_blank');
}
</script>
<?php endif; ?>

60
views/reports/pdf_balance.php Executable file
View File

@@ -0,0 +1,60 @@
<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">Condominio IBIZA-Cto Sierra Morena 152 - Reporte de Balance General</div>
<div class="print-date">Fecha de generación: <?= date('d/m/Y H:i') ?></div>
<table>
<thead>
<tr style="background-color: #d4edda;">
<th colspan="2">Resumen Financiero</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left;">Total Ingresos (Conceptos):</td>
<td class="text-end text-success">$<?= number_format($balance['total_incomes'], 2) ?></td>
</tr>
<?php if (!Auth::isLector()): ?>
<tr>
<td style="text-align: left;">Total Egresos:</td>
<td class="text-end text-danger">$<?= number_format($balance['total_expenses'], 2) ?></td>
</tr>
<tr style="background-color: #343a40; color: #fff;">
<td style="text-align: left; font-weight: bold;">Balance Neto:</td>
<td class="text-end fw-bold <?= $balance['balance'] >= 0 ? 'text-success' : 'text-danger' ?>">
$<?= number_format($balance['balance'], 2) ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>

View File

@@ -0,0 +1,86 @@
<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;
}
.text-warning {
color: orange;
}
</style>
<div class="print-title">Condominio IBIZA-Cto Sierra Morena 152 - Reporte de Deudores de Conceptos Especiales</div>
<div class="print-date">Fecha de generación: <?= date('d/m/Y H:i') ?></div>
<?php if (empty($conceptDebtors['debtors'])): ?>
<p>No hay deudores de conceptos especiales registrados.</p>
<?php else: ?>
<?php foreach ($conceptDebtors['debtors'] as $concept): ?>
<h5 style="font-size: 10pt; margin-top: 15px; margin-bottom: 5px;">
<?= htmlspecialchars($concept['concept_name']) ?>
<span style="color: orange; margin-left: 10px;">$<?= number_format($concept['total_due'], 2) ?> adeudado</span>
</h5>
<small style="font-size: 7pt; margin-bottom: 5px;">
Esperado: $<?= number_format($concept['total_expected'], 2) ?> |
Recaudado: $<?= number_format($concept['total_collected'], 2) ?> |
Pendiente: $<?= number_format($concept['total_due'], 2) ?>
</small>
<table>
<thead>
<tr style="background-color: #fff3cd;">
<th>Casa</th>
<th>Propietario</th>
<th>Monto Esperado</th>
<th>Pagado</th>
<th>Adeuda</th>
</tr>
</thead>
<tbody>
<?php foreach ($concept['house_debtors'] as $house): ?>
<tr>
<td><strong><?= $house['house_number'] ?></strong></td>
<td><?= htmlspecialchars($house['owner_name'] ?? '-') ?></td>
<td class="text-end">$<?= number_format($house['expected'], 2) ?></td>
<td class="text-end">$<?= number_format($house['paid'], 2) ?></td>
<td class="text-end fw-bold text-warning">$<?= number_format($house['due'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background-color: #343a40; color: #fff;">
<th colspan="4" style="text-align: right;">Total Concepto:</th>
<th style="text-align: right;">$<?= number_format($concept['total_due'], 2) ?></th>
</tr>
</tfoot>
</table>
<?php endforeach; ?>
<div style="margin-top: 20px; text-align: right; font-size: 10pt;">
<h5 style="margin-bottom: 5px;">Total General Adeudado en Todos los Conceptos</h5>
<h3 style="color: orange; margin-top: 0px;">$<?= number_format($conceptDebtors['total_due'], 2) ?></h3>
</div>
<?php endif; ?>

82
views/reports/pdf_expenses.php Executable file
View File

@@ -0,0 +1,82 @@
<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">Condominio IBIZA-Cto Sierra Morena 152 - Reporte de Gastos</div>
<div class="print-date">Fecha de generación: <?= date('d/m/Y H:i') ?></div>
<?php if (empty($expenses)): ?>
<p>No hay gastos registrados.</p>
<?php else: ?>
<table>
<thead>
<tr style="background-color: #f8d7da;">
<th>Fecha</th>
<th>Descripción</th>
<th>Categoría</th>
<th>Monto</th>
</tr>
</thead>
<tbody>
<?php
$totalExpenses = 0;
foreach ($expenses as $exp):
$totalExpenses += $exp['amount'];
?>
<tr>
<td><?= date('d/m/Y', strtotime($exp['expense_date'])) ?></td>
<td style="text-align: left;"><?= htmlspecialchars($exp['description']) ?></td>
<td><?= htmlspecialchars($exp['category'] ?? '-') ?></td>
<td class="text-end">$<?= number_format($exp['amount'], 2) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background-color: #343a40; color: #fff;">
<th colspan="3" style="text-align: right;">TOTAL GENERAL DE GASTOS:</th>
<th style="text-align: right;">$<?= number_format($totalExpenses, 2) ?></th>
</tr>
<?php if (!empty($expensesByCategory)): ?>
<tr>
<td colspan="4" style="text-align: left; background-color: #fff; border: none; padding-top: 10px; padding-bottom: 0px;">
<strong style="font-size: 9pt;">Gastos por Categoría:</strong>
</td>
</tr>
<?php foreach ($expensesByCategory as $cat): ?>
<tr>
<td colspan="3" style="text-align: right; border: none;"><?= htmlspecialchars($cat['category'] ?? 'Sin categoría') ?>:</td>
<td class="text-end" style="border: none;">$<?= number_format($cat['total'], 2) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tfoot>
</table>
<?php endif; ?>

View File

@@ -0,0 +1,95 @@
<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">Condominio IBIZA-Cto Sierra Morena 152 - Reporte de Deudores de Agua <?= $year ?></div>
<div class="print-date">Fecha de generación: <?= date('d/m/Y H:i') ?></div>
<?php
$hasFilters = !empty($waterDebtors['filters']['year']) || !empty($waterDebtors['filters']['months']) || !empty($waterDebtors['filters']['house_id']);
$filterText = [];
if (!empty($waterDebtors['filters']['year'])) {
$filterText[] = "Año: " . $waterDebtors['filters']['year'];
}
if (!empty($waterDebtors['filters']['months'])) {
$filterText[] = "Meses: " . implode(', ', $waterDebtors['filters']['months']);
}
if (!empty($waterDebtors['filters']['house_id'])) {
require_once __DIR__ . '/../../models/House.php';
$house = House::findById($waterDebtors['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($waterDebtors['debtors'])): ?>
<p>No hay deudores de agua registrados con los filtros actuales.</p>
<?php else: ?>
<table>
<thead>
<tr style="background-color: #f8d7da;">
<th>Casa</th>
<th>Propietario</th>
<th>Meses Adeudados</th>
<th>Total Debe</th>
</tr>
</thead>
<tbody>
<?php foreach ($waterDebtors['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['months_due'] as $month): ?>
<tr>
<td style="border: none; text-align: left;"><?= $month['year'] ?> - <?= $month['month'] ?></td>
<td style="border: none; text-align: right;">$<?= number_format($month['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($waterDebtors['total_due'], 2) ?></th>
</tr>
</tfoot>
</table>
<?php endif; ?>

324
views/users/index.php Executable file
View File

@@ -0,0 +1,324 @@
<div class="row mb-4">
<div class="col-12">
<h2><i class="bi bi-people"></i> Gestión de Usuarios</h2>
<p class="text-muted">Crear y administrar usuarios del sistema</p>
</div>
</div>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Usuarios del Sistema</h5>
<?php if (Auth::isAdmin()): ?>
<button class="btn btn-primary btn-sm" onclick="openUserModal()">
<i class="bi bi-plus"></i> Nuevo Usuario
</button>
<?php endif; ?>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>ID</th>
<th>Usuario</th>
<th>Email</th>
<th>Nombre</th>
<th>Rol</th>
<th>Estado</th>
<th>Último Acceso</th>
<?php if (Auth::isAdmin()): ?>
<th>Acciones</th>
<?php endif; ?>
</tr>
</thead>
<tbody id="usersTable">
<tr><td colspan="8" class="text-center">Cargando...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<?php if (Auth::isAdmin()): ?>
<div class="modal fade" id="userModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="userModalTitle">Nuevo Usuario</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="userForm" novalidate>
<div class="modal-body">
<input type="hidden" id="userId" name="id">
<div class="mb-3">
<label class="form-label">Usuario</label>
<input type="text" class="form-control" name="username" required>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email" required>
</div>
<div class="mb-3">
<label class="form-label">Nombre</label>
<input type="text" class="form-control" name="first_name" required>
</div>
<div class="mb-3">
<label class="form-label">Apellido</label>
<input type="text" class="form-control" name="last_name" required>
</div>
<div class="mb-3">
<label class="form-label">Contraseña</label>
<input type="password" class="form-control" name="password" id="userPassword">
<small class="text-muted">Dejar vacío para no cambiar</small>
</div>
<div class="mb-3">
<label class="form-label">Rol</label>
<select class="form-select" name="role" id="userRole" required>
<option value="VIEWER">Viewer (Solo lectura)</option>
<option value="CAPTURIST">Capturista (Puede editar)</option>
<option value="ADMIN">Admin (Acceso total)</option>
<option value="LECTOR">Lector (Solo casas asignadas)</option>
</select>
</div>
<div class="mb-3" id="houseSelectionDiv" style="display:none;">
<label class="form-label">Casas Permitidas</label>
<select class="form-select" id="houseSelect" name="house_ids[]" multiple style="height: 200px;" required>
<?php
require_once __DIR__ . '/../../models/House.php';
$allHouses = House::all();
foreach ($allHouses as $house): ?>
<option value="<?= $house['id'] ?>">Casa <?= $house['number'] ?> - <?= htmlspecialchars($house['owner_name'] ?: 'Sin nombre') ?></option>
<?php endforeach; ?>
</select>
<small class="text-muted">Seleccionar una o más casas (Ctrl+Click para múltiples)</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar</button>
</div>
</form>
</div>
</div>
</div>
<script>
let usersData = [];
function loadUsers() {
fetch('/dashboard.php?page=users_actions&action=list')
.then(r => r.json())
.then(data => {
usersData = data.users;
renderUsers();
});
}
function renderUsers() {
const tbody = document.getElementById('usersTable');
if (usersData.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center">No hay usuarios</td></tr>';
return;
}
tbody.innerHTML = usersData.map(u => {
let roleClass = u.role === 'ADMIN' ? 'danger' : (u.role === 'CAPTURIST' ? 'warning' : (u.role === 'LECTOR' ? 'secondary' : 'info'));
return `
<tr>
<td>${u.id}</td>
<td>${u.username}</td>
<td>${u.email}</td>
<td>${u.first_name} ${u.last_name}</td>
<td><span class="badge bg-${roleClass}">${u.role}</span></td>
<td>${u.is_active ? '<span class="badge bg-success">Activo</span>' : '<span class="badge bg-secondary">Inactivo</span>'}</td>
<td>${u.last_login ? new Date(u.last_login).toLocaleString() : '-'}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="editUser(${u.id})">
<i class="bi bi-pencil"></i>
</button>
${u.id !== 1 ? `<button class="btn btn-sm btn-danger" onclick="deleteUser(${u.id})">
<i class="bi bi-trash"></i>
</button>` : ''}
</td>
</tr>
`;
}).join('');
}
document.getElementById('userRole').addEventListener('change', function() {
const houseDiv = document.getElementById('houseSelectionDiv');
const houseSelect = document.getElementById('houseSelect');
if (this.value === 'LECTOR') {
houseDiv.style.display = 'block';
houseSelect.required = true;
} else {
houseDiv.style.display = 'none';
houseSelect.required = false;
Array.from(houseSelect.options).forEach(opt => opt.selected = false);
}
});
function openUserModal(id = null) {
const modal = new bootstrap.Modal(document.getElementById('userModal'));
const title = document.getElementById('userModalTitle');
const passwordField = document.getElementById('userPassword');
const roleSelect = document.getElementById('userRole');
const houseDiv = document.getElementById('houseSelectionDiv');
const houseSelect = document.getElementById('houseSelect');
document.getElementById('userForm').reset();
document.getElementById('userId').value = '';
Array.from(houseSelect.options).forEach(opt => opt.selected = false);
if (id) {
const user = usersData.find(u => u.id === id);
if (user) {
title.textContent = 'Editar Usuario';
document.getElementById('userId').value = user.id;
document.querySelector('[name="username"]').value = user.username;
document.querySelector('[name="email"]').value = user.email;
document.querySelector('[name="first_name"]').value = user.first_name;
document.querySelector('[name="last_name"]').value = user.last_name;
roleSelect.value = user.role;
passwordField.placeholder = 'Dejar vacío para no cambiar';
passwordField.required = false;
if (user.role === 'LECTOR') {
houseDiv.style.display = 'block';
houseSelect.required = true;
fetch(`/dashboard.php?page=users_actions&action=get_user_houses&user_id=${id}`)
.then(r => r.json())
.then(data => {
// ...
}); }
}
} else {
title.textContent = 'Nuevo Usuario';
passwordField.placeholder = 'Contraseña requerida';
passwordField.required = true;
houseDiv.style.display = 'none';
houseSelect.required = false;
}
modal.show();
}
function editUser(id) {
openUserModal(id);
}
function deleteUser(id) {
Swal.fire({
title: '¿Eliminar usuario?',
text: 'Esta acción no se puede deshacer',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sí, eliminar'
}).then(result => {
if (result.isConfirmed) {
fetch(`/dashboard.php?page=users_actions&action=delete&id=${id}`, { method: 'GET' }) // Cambiado a GET
.then(r => r.json())
.then(data => {
if (data.success) {
Swal.fire('Eliminado', '', 'success').then(loadUsers);
} else {
Swal.fire('Error', data.message || 'Error al eliminar', 'error'); // Mostrar mensaje de error del backend
}
});
}
});
}
document.getElementById('userForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData.entries());
const id = data.id;
const houseSelect = document.getElementById('houseSelect');
const selectedHouses = Array.from(houseSelect.selectedOptions).map(opt => parseInt(opt.value));
// **NUEVA VALIDACIÓN:** Si el rol es LECTOR y no hay casas seleccionadas
if (data.role === 'LECTOR' && selectedHouses.length === 0) {
Swal.fire('Error de Validación', 'Debe seleccionar al menos una casa para el rol Lector.', 'error');
return; // Detener el envío del formulario
}
// Si no hay password y es actualización, eliminar del data para no cambiarla
if (id && !data.password) {
delete data.password;
}
const apiUrl = id ? '/dashboard.php?page=users_actions&action=update' : '/dashboard.php?page=users_actions&action=create';
console.log('--- Iniciando submit de formulario de usuario ---');
console.log('Action URL:', apiUrl);
console.log('Data sent:', data);
fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(r => {
console.log('Response from user save:', r);
if (!r.ok) { // Check if response is not OK (e.g., 404, 500)
return r.text().then(text => { throw new Error(text || r.statusText); });
}
return r.json();
})
.then(res => {
console.log('Result from user save:', res);
if (res.success) {
const currentUserId = id || res.user_id;
const previousRole = usersData.find(u => u.id === parseInt(id))?.role; // parseInt(id)
if (data.role === 'LECTOR' || (id && previousRole === 'LECTOR' && data.role !== 'LECTOR')) {
const housesToAssign = (data.role === 'LECTOR') ? selectedHouses : [];
console.log('Attempting to assign houses:', housesToAssign);
fetch('/dashboard.php?page=users_actions&action=assign_houses', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: currentUserId, house_ids: housesToAssign })
})
.then(r => {
console.log('Response from assign houses:', r);
if (!r.ok) {
return r.text().then(text => { throw new Error(text || r.statusText); });
}
return r.json();
})
.then(assignRes => {
console.log('Result from assign houses:', assignRes);
if (assignRes.success) {
Swal.fire('Guardado', res.message || 'Usuario y permisos guardados exitosamente.', 'success').then(() => {
bootstrap.Modal.getInstance(document.getElementById('userModal')).hide();
loadUsers();
});
} else {
Swal.fire('Error', assignRes.message || 'Error desconocido al asignar casas.', 'error');
}
}).catch(error => {
console.error('Error assigning houses:', error);
Swal.fire('Error', 'Error de conexión al asignar casas: ' + error.message, 'error');
});
} else {
Swal.fire('Guardado', res.message || 'Usuario guardado exitosamente.', 'success').then(() => {
bootstrap.Modal.getInstance(document.getElementById('userModal')).hide();
loadUsers();
});
}
} else {
Swal.fire('Error', res.message || 'Error desconocido al guardar usuario.', 'error');
}
})
.catch(error => {
console.error('Error saving user:', error);
Swal.fire('Error', 'Error de conexión al guardar usuario: ' + error.message, 'error');
});
});
loadUsers();
</script>
<?php endif; ?>

135
views/users/profile.php Executable file
View File

@@ -0,0 +1,135 @@
<div class="row mb-4">
<div class="col-12">
<h2><i class="bi bi-person"></i> Mi Perfil</h2>
<p class="text-muted">Actualiza tu información personal y contraseña</p>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Información Personal</h5>
</div>
<div class="card-body">
<form id="profileForm">
<div class="mb-3">
<label class="form-label">Usuario</label>
<input type="text" class="form-control" value="<?= Auth::user()['username'] ?>" disabled>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email" value="<?= Auth::user()['email'] ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Nombre</label>
<input type="text" class="form-control" name="first_name" value="<?= Auth::user()['first_name'] ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Apellido</label>
<input type="text" class="form-control" name="last_name" value="<?= Auth::user()['last_name'] ?>" required>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check"></i> Guardar Cambios
</button>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Cambiar Contraseña</h5>
</div>
<div class="card-body">
<form id="passwordForm">
<div class="mb-3">
<label class="form-label">Contraseña Actual</label>
<input type="password" class="form-control" name="current_password" required>
</div>
<div class="mb-3">
<label class="form-label">Nueva Contraseña</label>
<input type="password" class="form-control" name="new_password" id="newPassword" required>
</div>
<div class="mb-3">
<label class="form-label">Confirmar Contraseña</label>
<input type="password" class="form-control" name="confirm_password" id="confirmPassword" required>
</div>
<button type="submit" class="btn btn-warning">
<i class="bi bi-lock"></i> Cambiar Contraseña
</button>
</form>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5 class="card-title mb-0">Información de Cuenta</h5>
</div>
<div class="card-body">
<p><strong>Rol:</strong> <span class="badge bg-<?= (Auth::isAdmin() ? 'danger' : (Auth::isCapturist() ? 'warning' : 'info')) ?>"><?= Auth::user()['role'] ?></span></p>
<p><strong>Estado:</strong> <span class="badge bg-success">Activo</span></p>
<p><strong>Último acceso:</strong> <?= Auth::user()['last_login'] ? date('d/m/Y H:i', strtotime(Auth::user()['last_login'])) : 'No disponible' ?></p>
</div>
</div>
</div>
</div>
<script>
document.getElementById('profileForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData.entries());
fetch('/dashboard.php?page=users_actions&action=update_profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => {
if (res.success) {
Swal.fire('Guardado', 'Tu perfil ha sido actualizado', 'success').then(() => {
location.reload(); // Recargar para ver los cambios en la información de sesión
});
} else {
Swal.fire('Error', res.message || 'Error al guardar', 'error');
}
})
.catch(() => Swal.fire('Error', 'Error de conexión', 'error'));
});
document.getElementById('passwordForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData.entries());
if (data.new_password !== data.confirm_password) {
Swal.fire('Error', 'Las contraseñas no coinciden', 'error');
return;
}
if (data.new_password.length < 6) {
Swal.fire('Error', 'La contraseña debe tener al menos 6 caracteres', 'error');
return;
}
fetch('/dashboard.php?page=users_actions&action=change_password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => {
if (res.success) {
Swal.fire('Éxito', 'Contraseña actualizada', 'success').then(() => {
document.getElementById('passwordForm').reset();
});
} else {
Swal.fire('Error', res.message || 'Error al cambiar contraseña', 'error');
}
})
.catch(() => Swal.fire('Error', 'Error de conexión', 'error'));
});
</script>