445 lines
25 KiB
PHP
445 lines
25 KiB
PHP
<?php
|
|
session_start();
|
|
// Check if user is Super User
|
|
if (!isset($_SESSION["user_id"]) || ($_SESSION["user_role"] ?? '') !== 'Super User') {
|
|
header("Location: login.php");
|
|
exit;
|
|
}
|
|
require_once 'db/config.php';
|
|
$project_name = $_SERVER['PROJECT_NAME'] ?? 'LPA Online';
|
|
|
|
$users_count = 0;
|
|
$lpas_count = 0;
|
|
$users = [];
|
|
$lpas = [];
|
|
$migration_history = [];
|
|
$backups = [];
|
|
|
|
try {
|
|
$db = db();
|
|
// Get stats
|
|
$users_count = $db->query("SELECT COUNT(*) FROM users")->fetchColumn();
|
|
$lpas_count = $db->query("SELECT COUNT(*) FROM lpa_applications")->fetchColumn();
|
|
|
|
// Get all users
|
|
$users = $db->query("SELECT * FROM users ORDER BY created_at DESC LIMIT 50")->fetchAll();
|
|
|
|
// Get all LPAs
|
|
$lpas = $db->query("SELECT * FROM lpa_applications ORDER BY created_at DESC LIMIT 50")->fetchAll();
|
|
|
|
// Get migration history
|
|
try {
|
|
$migration_history = $db->query("SELECT * FROM migration_history ORDER BY executed_at DESC")->fetchAll();
|
|
} catch (PDOException $e) {
|
|
$migration_history = [];
|
|
}
|
|
|
|
// Get backups from filesystem
|
|
$backups_dir = __DIR__ . '/backups';
|
|
if (is_dir($backups_dir)) {
|
|
$backup_files = glob($backups_dir . '/backup_*.sql');
|
|
foreach ($backup_files as $file) {
|
|
$backups[] = [
|
|
'filename' => basename($file),
|
|
'size' => round(filesize($file) / 1024, 2) . ' KB',
|
|
'created_at' => filemtime($file)
|
|
];
|
|
}
|
|
// Sort by created_at descending
|
|
usort($backups, function($a, $b) {
|
|
return $b['created_at'] - $a['created_at'];
|
|
});
|
|
}
|
|
} catch (PDOException $e) {
|
|
error_log($e->getMessage());
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin Dashboard — <?php echo htmlspecialchars($project_name); ?></title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="assets/css/custom.css" rel="stylesheet">
|
|
</head>
|
|
<body class="bg-light">
|
|
<nav class="navbar navbar-expand-lg bg-white border-bottom shadow-sm">
|
|
<div class="container">
|
|
<a class="navbar-brand d-flex align-items-center" href="/">
|
|
<img src="assets/pasted-20260228-235417-eedda424.png" alt="<?php echo htmlspecialchars($project_name); ?>" height="40">
|
|
</a>
|
|
<div class="d-flex align-items-center">
|
|
<span class="me-3 d-none d-md-inline text-muted small">Logged in as Admin: <?php echo htmlspecialchars($_SESSION['user_name'] ?? $_SESSION['user_email']); ?></span>
|
|
<a href="/logout.php" class="btn btn-outline-secondary btn-sm px-3 rounded-pill">Logout</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container py-5">
|
|
<div class="row mb-5 align-items-center">
|
|
<div class="col-md-6">
|
|
<h1 class="h3 fw-bold mb-1">System Administration</h1>
|
|
<p class="text-muted small mb-0">Overview of all users and applications in the system.</p>
|
|
</div>
|
|
<div class="col-md-6 text-md-end mt-3 mt-md-0">
|
|
<div class="dropdown d-inline-block">
|
|
<button class="btn btn-white border shadow-sm rounded-pill px-4 dropdown-toggle" type="button" id="maintenanceDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-1"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
|
|
System Tools
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-lg rounded-3" aria-labelledby="maintenanceDropdown">
|
|
<li><h6 class="dropdown-header">Database Maintenance</h6></li>
|
|
<li>
|
|
<button class="dropdown-item py-2" type="button" onclick="runMigrations()">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-2 text-primary"><polyline points="16 16 12 12 8 16"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path><polyline points="16 16 12 12 8 16"></polyline></svg>
|
|
Run Pending Migrations
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="migration-alert" style="display: none;">
|
|
<div class="alert alert-info alert-dismissible fade show rounded-3 shadow-sm border-0 mb-4" role="alert">
|
|
<span id="migration-message"></span>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-5">
|
|
<div class="col-md-6">
|
|
<div class="card border-0 shadow-sm p-4">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-primary-subtle text-primary p-3 rounded-3 me-3">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
|
</div>
|
|
<div>
|
|
<h6 class="text-muted small text-uppercase fw-bold mb-1">Total Users</h6>
|
|
<h2 class="fw-bold mb-0"><?php echo number_format($users_count); ?></h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="card border-0 shadow-sm p-4">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-success-subtle text-success p-3 rounded-3 me-3">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
|
</div>
|
|
<div>
|
|
<h6 class="text-muted small text-uppercase fw-bold mb-1">Total Applications</h6>
|
|
<h2 class="fw-bold mb-0"><?php echo number_format($lpas_count); ?></h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
<div class="col-lg-12">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white py-3 border-bottom-0">
|
|
<h5 class="fw-bold mb-0">Recent Users</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light">
|
|
<tr class="small text-uppercase tracking-wider">
|
|
<th class="ps-4">Name</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Credits</th>
|
|
<th>Verified</th>
|
|
<th class="text-end pe-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($users as $u): ?>
|
|
<tr>
|
|
<td class="ps-4 fw-medium"><?php echo htmlspecialchars($u['name'] ?? 'N/A'); ?></td>
|
|
<td><?php echo htmlspecialchars($u['email']); ?></td>
|
|
<td>
|
|
<span class="badge rounded-pill <?php echo ($u['role'] ?? '') === 'Super User' ? 'bg-danger-subtle text-danger' : 'bg-secondary-subtle text-secondary'; ?>">
|
|
<?php echo htmlspecialchars($u['role'] ?? 'Standard User'); ?>
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="fw-bold text-primary"><?php echo (int)($u['credits'] ?? 0); ?></span>
|
|
</td>
|
|
<td>
|
|
<?php if ($u['is_verified']): ?>
|
|
<span class="text-success small">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-1"><polyline points="20 6 9 17 4 12"></polyline></svg>Yes
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="text-muted small">No</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="text-end pe-4">
|
|
<button onclick="showCreditModal(<?php echo $u['id']; ?>, '<?php echo htmlspecialchars($u['name'] ?? $u['email']); ?>', <?php echo (int)($u['credits'] ?? 0); ?>)" class="btn btn-sm btn-outline-primary px-3 rounded-pill">Adjust Credits</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-12 mt-5">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white py-3 border-bottom-0">
|
|
<h5 class="fw-bold mb-0">Recent Applications</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light">
|
|
<tr class="small text-uppercase tracking-wider">
|
|
<th class="ps-4">Type</th>
|
|
<th>Donor</th>
|
|
<th>Progress</th>
|
|
<th>Status</th>
|
|
<th class="text-end pe-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (count($lpas) > 0): ?>
|
|
<?php foreach ($lpas as $lpa): ?>
|
|
<tr id="lpa-row-<?php echo $lpa['id']; ?>">
|
|
<td class="ps-4">
|
|
<div class="fw-bold mb-0"><?php echo htmlspecialchars($lpa['lpa_type']); ?></div>
|
|
<div class="text-muted small">ID: #<?php echo $lpa['id']; ?></div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-medium"><?php echo htmlspecialchars($lpa['donor_name']); ?></div>
|
|
<div class="text-muted small"><?php echo htmlspecialchars($lpa['customer_email']); ?></div>
|
|
</td>
|
|
<td>
|
|
<div class="progress" style="height: 6px; width: 100px;">
|
|
<?php $percent = round(($lpa['step_reached'] / 14) * 100); ?>
|
|
<div class="progress-bar bg-primary" role="progressbar" style="width: <?php echo $percent; ?>%"></div>
|
|
</div>
|
|
<span class="small text-muted"><?php echo $percent; ?>%</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge rounded-pill bg-info-subtle text-info"><?php echo ucfirst($lpa['status']); ?></span>
|
|
</td>
|
|
<td class="text-end pe-4">
|
|
<a href="api/generate_pdf.php?id=<?php echo $lpa['id']; ?>" class="btn btn-sm btn-outline-primary px-3 rounded-pill me-2">PDF</a>
|
|
<button onclick="deleteLPA(<?php echo $lpa['id']; ?>)" class="btn btn-sm btn-outline-danger px-3 rounded-pill">Delete</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<tr><td colspan="5" class="text-center py-4">No applications found.</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-6 mt-5">
|
|
<div class="card border-0 shadow-sm mb-5 h-100">
|
|
<div class="card-header bg-white py-3 border-bottom-0">
|
|
<h5 class="fw-bold mb-0">Migration History</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light">
|
|
<tr class="small text-uppercase tracking-wider">
|
|
<th class="ps-4">File Name</th>
|
|
<th class="text-end pe-4">Executed At</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (count($migration_history) > 0): ?>
|
|
<?php foreach ($migration_history as $m): ?>
|
|
<tr>
|
|
<td class="ps-4 fw-medium"><?php echo htmlspecialchars($m['filename']); ?></td>
|
|
<td class="text-end pe-4 text-muted small"><?php echo date('M d, Y H:i', strtotime($m['executed_at'])); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<tr><td colspan="2" class="text-center py-4 text-muted small">No migration history found. Run migrations to initialize.</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-6 mt-5">
|
|
<div class="card border-0 shadow-sm mb-5 h-100">
|
|
<div class="card-header bg-white py-3 border-bottom-0 d-flex justify-content-between align-items-center">
|
|
<h5 class="fw-bold mb-0">Backups</h5>
|
|
<span class="badge rounded-pill bg-light text-muted fw-normal">Last 5 kept</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light">
|
|
<tr class="small text-uppercase tracking-wider">
|
|
<th class="ps-4">File Name</th>
|
|
<th>Size</th>
|
|
<th class="text-end pe-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (count($backups) > 0): ?>
|
|
<?php foreach ($backups as $b): ?>
|
|
<tr>
|
|
<td class="ps-4">
|
|
<div class="fw-medium small"><?php echo htmlspecialchars($b['filename']); ?></div>
|
|
<div class="text-muted" style="font-size: 0.75rem;"><?php echo date('M d, Y H:i', $b['created_at']); ?></div>
|
|
</td>
|
|
<td class="text-muted small"><?php echo $b['size']; ?></td>
|
|
<td class="text-end pe-4">
|
|
<a href="api/download_backup.php?filename=<?php echo urlencode($b['filename']); ?>" class="btn btn-sm btn-link text-primary text-decoration-none p-0">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-1"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
|
Download
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<tr><td colspan="3" class="text-center py-4 text-muted small">No backups found. Backups are created automatically before migrations.</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Credit Adjustment Modal -->
|
|
<div class="modal fade" id="creditModal" tabindex="-1" aria-labelledby="creditModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg rounded-4">
|
|
<div class="modal-header border-bottom-0 pb-0">
|
|
<h5 class="modal-title fw-bold" id="creditModalLabel">Adjust Credits</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body pt-3">
|
|
<p class="text-muted small mb-4">Adjust LPA credits for <strong id="modalUserName"></strong>. Current balance: <strong id="modalCurrentCredits"></strong></p>
|
|
|
|
<form id="creditForm">
|
|
<input type="hidden" id="modalUserId" name="user_id">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold text-uppercase">Action</label>
|
|
<select class="form-select rounded-3" name="action" id="creditAction">
|
|
<option value="add">Add Credits (+)</option>
|
|
<option value="set">Set Total Credits (=)</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="form-label small fw-bold text-uppercase">Amount</label>
|
|
<input type="number" class="form-control rounded-3" name="credits" id="creditAmount" min="0" value="1" required>
|
|
</div>
|
|
<div class="d-grid">
|
|
<button type="submit" class="btn btn-primary py-2 rounded-pill fw-bold">Update Credits</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
const creditModal = new bootstrap.Modal(document.getElementById('creditModal'));
|
|
|
|
function showCreditModal(id, name, current) {
|
|
document.getElementById('modalUserId').value = id;
|
|
document.getElementById('modalUserName').textContent = name;
|
|
document.getElementById('modalCurrentCredits').textContent = current;
|
|
document.getElementById('creditAmount').value = 1;
|
|
creditModal.show();
|
|
}
|
|
|
|
document.getElementById('creditForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(this);
|
|
|
|
fetch('api/allocate_credits.php', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error: ' + data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('An unexpected error occurred.');
|
|
});
|
|
});
|
|
|
|
function deleteLPA(id) {
|
|
if (confirm('Are you sure you want to delete this LPA application? This action cannot be undone.')) {
|
|
fetch('api/delete_lpa.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: 'id=' + id
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const row = document.getElementById('lpa-row-' + id);
|
|
if (row) {
|
|
row.remove();
|
|
location.reload();
|
|
}
|
|
} else {
|
|
alert('Error: ' + data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('An error occurred while trying to delete the application.');
|
|
});
|
|
}
|
|
}
|
|
|
|
function runMigrations() {
|
|
if (!confirm('Run database migrations? A database backup will be created automatically before proceeding.')) return;
|
|
|
|
const alertBox = document.getElementById('migration-alert');
|
|
const messageEl = document.getElementById('migration-message');
|
|
|
|
fetch('api/run_migrations.php', {
|
|
method: 'POST'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
alertBox.style.display = 'block';
|
|
if (data.success) {
|
|
const backupMsg = data.backup ? ` Backup created: ${data.backup}.` : '';
|
|
messageEl.textContent = data.message + backupMsg + ' Page will reload in 3 seconds.';
|
|
messageEl.parentElement.className = 'alert alert-success alert-dismissible fade show rounded-3 shadow-sm border-0 mb-4';
|
|
setTimeout(() => location.reload(), 3000);
|
|
} else {
|
|
messageEl.textContent = 'Error: ' + data.error;
|
|
messageEl.parentElement.className = 'alert alert-danger alert-dismissible fade show rounded-3 shadow-sm border-0 mb-4';
|
|
}
|
|
// Scroll to alert
|
|
alertBox.scrollIntoView({ behavior: 'smooth' });
|
|
})
|
|
.catch(error => {
|
|
alertBox.style.display = 'block';
|
|
messageEl.textContent = 'An unexpected error occurred.';
|
|
messageEl.parentElement.className = 'alert alert-danger alert-dismissible fade show rounded-3 shadow-sm border-0 mb-4';
|
|
console.error('Error:', error);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|