Primer version funcional
This commit is contained in:
48
views/auth/login.php
Executable file
48
views/auth/login.php
Executable 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
108
views/configurar/index.php
Executable 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
263
views/dashboard/index.php
Executable 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
415
views/finance/concept_view.php
Executable 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>
|
||||
258
views/finance/concept_view.php.backup
Executable file
258
views/finance/concept_view.php.backup
Executable 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
560
views/finance/index.php
Executable 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
152
views/houses/index.php
Executable 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
164
views/houses/view.php
Executable 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
269
views/import/index.php
Executable 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
125
views/layout/base.php
Executable 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
560
views/payments/index.php
Executable 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
117
views/payments/pdf_template.php
Executable 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
467
views/reports/index.php
Executable 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
60
views/reports/pdf_balance.php
Executable 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>
|
||||
86
views/reports/pdf_concept_debtors.php
Executable file
86
views/reports/pdf_concept_debtors.php
Executable 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
82
views/reports/pdf_expenses.php
Executable 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; ?>
|
||||
95
views/reports/pdf_water_debtors.php
Executable file
95
views/reports/pdf_water_debtors.php
Executable 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
324
views/users/index.php
Executable 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
135
views/users/profile.php
Executable 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>
|
||||
Reference in New Issue
Block a user