FinancePlanner/admin.php
2026-05-29 12:00:49 +01:00

251 lines
12 KiB
PHP

<?php
// ─────────────────────────────────────────────
// admin.php · Admin Panel
// ─────────────────────────────────────────────
require_once 'db.php';
require_once 'auth.php';
requireAdmin();
require_once 'nav.php';
renderHead('Admin Panel');
renderTopBar('Admin Panel');
?>
<div class="page">
<!-- User stats bar -->
<div class="summary-row" id="statsRow" style="margin-top:8px">
<div class="summary-chip"><div class="chip-label">Total Users</div><div class="chip-val" id="statTotal">—</div></div>
<div class="summary-chip"><div class="chip-label">Admins</div><div class="chip-val income" id="statAdmins">—</div></div>
<div class="summary-chip"><div class="chip-label">Suspended</div><div class="chip-val expense" id="statSuspended">—</div></div>
<div class="summary-chip"><div class="chip-label">Active</div><div class="chip-val" id="statActive">—</div></div>
</div>
<!-- User list -->
<div class="section-label">Users</div>
<div class="card" id="userList">
<div class="empty"><div class="empty-icon">⏳</div><p>Loading…</p></div>
</div>
<!-- System Settings -->
<div class="section-label">System Settings</div>
<div class="card" style="padding:16px 16px 20px">
<div class="form-group">
<label>Site Name</label>
<input type="text" id="siteName" placeholder="Finance Planner">
</div>
<div class="form-group" style="display:flex;align-items:center;gap:10px;margin-bottom:0">
<input type="checkbox" id="regEnabled"
style="width:18px;height:18px;accent-color:var(--accent);cursor:pointer;flex-shrink:0">
<label for="regEnabled" style="cursor:pointer;font-size:.88rem;color:var(--text)">
Allow new user registration
</label>
</div>
</div>
<!-- Donation Settings -->
<div class="section-label">Donation Settings</div>
<div class="card" style="padding:16px 16px 20px">
<p style="font-size:.8rem;color:var(--text-dim);margin-bottom:14px">
Configure donation links shown to all users on the Donate page and Settings. Leave blank to hide.
</p>
<div class="form-group">
<label>Message shown to users</label>
<input type="text" id="donationMsg"
placeholder="If you find this app useful, consider buying me a coffee!">
</div>
<div class="form-group">
<label>💳 PayPal link</label>
<input type="url" id="paypalLink" placeholder="https://paypal.me/yourname">
</div>
<div class="form-group">
<label>☕ Ko-fi link</label>
<input type="url" id="kofiLink" placeholder="https://ko-fi.com/yourname">
</div>
<div class="form-group">
<label>☕ Buy Me a Coffee link</label>
<input type="url" id="bmcLink" placeholder="https://buymeacoffee.com/yourname">
</div>
<button class="btn btn-primary" onclick="saveSettings()">Save All Settings</button>
<span id="settingsMsg" style="margin-left:10px;font-size:.82rem"></span>
</div>
</div>
<!-- Reset Password Modal -->
<div class="modal-backdrop" id="resetModal">
<div class="modal">
<div class="modal-header">
<h2>Reset Password</h2>
<button class="modal-close" onclick="closeResetModal()">✕</button>
</div>
<input type="hidden" id="resetUserId">
<p id="resetUserLabel" style="color:var(--text-dim);font-size:.85rem;margin-bottom:16px"></p>
<div class="form-group">
<label>New Password</label>
<input type="password" id="resetPw" placeholder="Minimum 6 characters">
</div>
<div class="form-group">
<label>Confirm Password</label>
<input type="password" id="resetPwConfirm" placeholder="Repeat password">
</div>
<button class="btn btn-primary btn-full" onclick="doReset()">Set New Password</button>
<div id="resetMsg" style="margin-top:10px;font-size:.85rem;text-align:center"></div>
</div>
</div>
<?php renderNav('admin'); ?>
<style>
.badge { display:inline-block;padding:2px 8px;border-radius:20px;font-size:.65rem;font-weight:700;vertical-align:middle; }
.badge-admin { background:rgba(58,180,242,.18);color:var(--accent);border:1px solid rgba(58,180,242,.3); }
.badge-suspended { background:rgba(248,113,113,.18);color:var(--red);border:1px solid rgba(248,113,113,.3); }
.user-actions { display:flex;gap:6px;flex-wrap:wrap;margin-top:6px; }
.user-actions .btn { padding:5px 10px;font-size:.75rem; }
@media(min-width:600px){ .user-actions { margin-top:0; } }
</style>
<script>
async function api(action, method='GET', body=null) {
const url = `api.php?action=${action}`;
const opt = { method };
if (body) { opt.headers={'Content-Type':'application/x-www-form-urlencoded'}; opt.body=body; }
const r = await fetch(url, opt);
return r.json();
}
function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function fmtDate(d) {
return new Date(d).toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'});
}
// ── Load users ──────────────────────────────
async function loadUsers() {
const res = await api('admin_list_users');
const el = document.getElementById('userList');
if (!res.ok || !res.data.length) {
el.innerHTML = '<div class="empty"><div class="empty-icon">👥</div><p>No users found.</p></div>';
return;
}
// Stats
const total = res.data.length;
const admins = res.data.filter(u => u.is_admin == 1).length;
const suspended = res.data.filter(u => u.is_suspended == 1).length;
document.getElementById('statTotal').textContent = total;
document.getElementById('statAdmins').textContent = admins;
document.getElementById('statSuspended').textContent = suspended;
document.getElementById('statActive').textContent = total - suspended;
const myId = <?= (int)currentUserId() ?>;
el.innerHTML = res.data.map(u => {
const isMe = u.id == myId;
const badges = [];
if (u.is_admin == 1) badges.push('<span class="badge badge-admin">Admin</span>');
if (u.is_suspended == 1) badges.push('<span class="badge badge-suspended">Suspended</span>');
return `<div class="card-row" style="flex-wrap:wrap;align-items:flex-start;gap:6px">
<div style="flex:1;min-width:180px">
<div class="row-label">
${escHtml(u.display_name || u.username)}
${badges.join(' ')}
${isMe ? '<span style="color:var(--text-muted);font-size:.7rem">&nbsp;(you)</span>' : ''}
</div>
<div class="row-sub">
@${escHtml(u.username)} &nbsp;·&nbsp; Joined ${fmtDate(u.created_at)}
&nbsp;·&nbsp; ${u.account_count} account(s)
</div>
</div>
<div class="user-actions">
<button class="btn btn-ghost" onclick="openReset(${u.id},'${escHtml(u.username)}')">🔑 Reset PW</button>
${!isMe ? `<button class="btn btn-ghost" onclick="toggleAdmin(${u.id})">
${u.is_admin == 1 ? '⬇️ Revoke Admin' : '⬆️ Make Admin'}
</button>` : ''}
${!isMe ? `<button class="btn btn-ghost" onclick="toggleSuspend(${u.id})">
${u.is_suspended == 1 ? '✅ Unsuspend' : '🚫 Suspend'}
</button>` : ''}
${!isMe && u.is_admin != 1 ? `<button class="btn btn-danger" onclick="deleteUser(${u.id},'${escHtml(u.username)}')">🗑 Delete</button>` : ''}
</div>
</div>`;
}).join('');
}
async function toggleAdmin(id) {
if (!confirm('Toggle admin status for this user?')) return;
const res = await api('admin_toggle_admin', 'POST', new URLSearchParams({id}).toString());
if (res.ok) loadUsers(); else alert(res.error || 'Failed');
}
async function toggleSuspend(id) {
if (!confirm('Toggle suspension for this user?')) return;
const res = await api('admin_toggle_suspend', 'POST', new URLSearchParams({id}).toString());
if (res.ok) loadUsers(); else alert(res.error || 'Failed');
}
async function deleteUser(id, username) {
if (!confirm(`Permanently delete @${username} and ALL their data? This cannot be undone.`)) return;
const res = await api('admin_delete_user', 'POST', new URLSearchParams({id}).toString());
if (res.ok) loadUsers(); else alert(res.error || 'Failed');
}
// ── Reset password modal ─────────────────────
function openReset(id, username) {
document.getElementById('resetUserId').value = id;
document.getElementById('resetUserLabel').textContent = `Setting a new password for @${username}`;
document.getElementById('resetPw').value = '';
document.getElementById('resetPwConfirm').value = '';
document.getElementById('resetMsg').textContent = '';
document.getElementById('resetModal').classList.add('open');
setTimeout(() => document.getElementById('resetPw').focus(), 200);
}
function closeResetModal() { document.getElementById('resetModal').classList.remove('open'); }
document.getElementById('resetModal').addEventListener('click', e => {
if (e.target === document.getElementById('resetModal')) closeResetModal();
});
async function doReset() {
const id = document.getElementById('resetUserId').value;
const pw = document.getElementById('resetPw').value;
const pw2 = document.getElementById('resetPwConfirm').value;
const el = document.getElementById('resetMsg');
if (pw.length < 6) { el.style.color='var(--red)'; el.textContent='Min. 6 characters'; return; }
if (pw !== pw2) { el.style.color='var(--red)'; el.textContent='Passwords do not match'; return; }
const res = await api('admin_reset_password', 'POST', new URLSearchParams({id, password: pw}).toString());
el.style.color = res.ok ? 'var(--green)' : 'var(--red)';
el.textContent = res.ok ? '✓ Password updated' : ('✗ ' + (res.error || 'Failed'));
if (res.ok) setTimeout(closeResetModal, 1200);
}
// ── Settings ─────────────────────────────────
async function loadSettings() {
const res = await api('admin_get_settings');
if (!res.ok) return;
const s = res.data;
document.getElementById('siteName').value = s.site_name || '';
document.getElementById('regEnabled').checked = s.registration_enabled === '1';
document.getElementById('donationMsg').value = s.donation_message || '';
document.getElementById('paypalLink').value = s.paypal_link || '';
document.getElementById('kofiLink').value = s.kofi_link || '';
document.getElementById('bmcLink').value = s.buymeacoffee_link || '';
}
async function saveSettings() {
const fd = new URLSearchParams({
site_name: document.getElementById('siteName').value.trim(),
registration_enabled: document.getElementById('regEnabled').checked ? '1' : '0',
donation_message: document.getElementById('donationMsg').value.trim(),
paypal_link: document.getElementById('paypalLink').value.trim(),
kofi_link: document.getElementById('kofiLink').value.trim(),
buymeacoffee_link: document.getElementById('bmcLink').value.trim(),
});
const res = await api('admin_save_settings', 'POST', fd.toString());
const el = document.getElementById('settingsMsg');
el.style.color = res.ok ? 'var(--green)' : 'var(--red)';
el.textContent = res.ok ? '✓ Settings saved' : '✗ Failed to save';
if (res.ok) setTimeout(() => el.textContent = '', 3000);
}
loadUsers();
loadSettings();
</script>
</body>
</html>