dealspace/portal/templates/admin/dashboard.html

241 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin — Dealspace</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<style>
* { font-family: 'Inter', sans-serif; }
body { background: #0a1628; }
.tab-active { border-bottom: 2px solid #c9a84c; color: #c9a84c; }
.tab-inactive { border-bottom: 2px solid transparent; color: #94a3b8; }
.tab-inactive:hover { color: #e2e8f0; }
</style>
</head>
<body class="min-h-screen text-white">
<!-- Top bar -->
<header class="bg-[#0d1f3c] border-b border-white/[0.08] px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4">
<a href="/app/tasks" class="text-xl font-bold tracking-tight">
<span class="text-[#c9a84c]">Deal</span>space
</a>
<span class="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded-full font-medium">SUPER ADMIN</span>
</div>
<div class="flex items-center gap-4">
<a href="/app/tasks" class="text-sm text-[#94a3b8] hover:text-white transition">&larr; Back to app</a>
<button id="logoutBtn" class="text-sm text-[#94a3b8] hover:text-white transition">Sign out</button>
</div>
</header>
<div class="max-w-7xl mx-auto px-6 py-8">
<h1 class="text-2xl font-bold mb-6">Platform Administration</h1>
<!-- Tabs -->
<div class="flex gap-6 mb-8 border-b border-white/[0.08]">
<button class="tab-active pb-3 text-sm font-medium" data-tab="users">Users</button>
<button class="tab-inactive pb-3 text-sm font-medium" data-tab="projects">Projects</button>
<button class="tab-inactive pb-3 text-sm font-medium" data-tab="audit">Audit Log</button>
</div>
<!-- Users tab -->
<div id="tab-users" class="tab-content">
<div class="flex items-center justify-between mb-4">
<input type="text" id="userSearch" placeholder="Search by email or name..."
class="px-4 py-2 bg-[#0d1f3c] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] w-80">
<span id="userCount" class="text-sm text-[#94a3b8]"></span>
</div>
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-white/[0.08] text-[#94a3b8]">
<th class="text-left px-4 py-3 font-medium">Name</th>
<th class="text-left px-4 py-3 font-medium">Email</th>
<th class="text-left px-4 py-3 font-medium">Organization</th>
<th class="text-left px-4 py-3 font-medium">Status</th>
<th class="text-left px-4 py-3 font-medium">Created</th>
<th class="text-left px-4 py-3 font-medium">Actions</th>
</tr>
</thead>
<tbody id="usersTable"></tbody>
</table>
</div>
</div>
<!-- Projects tab -->
<div id="tab-projects" class="tab-content hidden">
<div class="flex items-center justify-between mb-4">
<span id="projectCount" class="text-sm text-[#94a3b8]"></span>
</div>
<div class="grid gap-4" id="projectsGrid"></div>
</div>
<!-- Audit tab -->
<div id="tab-audit" class="tab-content hidden">
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-white/[0.08] text-[#94a3b8]">
<th class="text-left px-4 py-3 font-medium">Time</th>
<th class="text-left px-4 py-3 font-medium">Actor</th>
<th class="text-left px-4 py-3 font-medium">Action</th>
<th class="text-left px-4 py-3 font-medium">Target</th>
<th class="text-left px-4 py-3 font-medium">IP</th>
</tr>
</thead>
<tbody id="auditTable"></tbody>
</table>
</div>
</div>
</div>
<script>
const token = localStorage.getItem('ds_token');
const user = JSON.parse(localStorage.getItem('ds_user') || '{}');
if (!token || !user.is_super_admin) {
window.location.href = '/app/login';
}
function fetchAPI(url) {
return fetch(url, { headers: { 'Authorization': 'Bearer ' + token } }).then(r => {
if (r.status === 401) { localStorage.clear(); window.location.href = '/app/login'; }
return r.json();
});
}
function postAPI(url, body) {
return fetch(url, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(r => r.json());
}
function formatDate(ts) {
if (!ts) return '—';
return new Date(ts).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}
function formatDateTime(ts) {
if (!ts) return '—';
return new Date(ts).toLocaleString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
}
function escapeHtml(s) {
const d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
// Tabs
document.querySelectorAll('[data-tab]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-tab]').forEach(b => {
b.className = b === btn ? 'tab-active pb-3 text-sm font-medium' : 'tab-inactive pb-3 text-sm font-medium';
});
document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'));
document.getElementById('tab-' + btn.dataset.tab).classList.remove('hidden');
if (btn.dataset.tab === 'projects') loadProjects();
if (btn.dataset.tab === 'audit') loadAudit();
});
});
// Users
let allUsers = [];
async function loadUsers() {
allUsers = await fetchAPI('/api/admin/users');
renderUsers(allUsers);
}
function renderUsers(users) {
document.getElementById('userCount').textContent = users.length + ' user' + (users.length !== 1 ? 's' : '');
document.getElementById('usersTable').innerHTML = users.map(u => `
<tr class="border-b border-white/[0.05] hover:bg-white/[0.02]">
<td class="px-4 py-3 font-medium">${escapeHtml(u.name)}</td>
<td class="px-4 py-3 text-[#94a3b8]">${escapeHtml(u.email)}</td>
<td class="px-4 py-3 text-[#94a3b8]">${escapeHtml(u.org_name || u.org_id || '—')}</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded-full text-xs ${u.active ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'}">
${u.active ? 'Active' : 'Inactive'}
</span>
</td>
<td class="px-4 py-3 text-[#94a3b8]">${formatDate(u.created_at)}</td>
<td class="px-4 py-3">
<button onclick="impersonate('${u.user_id}')" class="text-[#c9a84c] hover:underline text-xs">Impersonate</button>
</td>
</tr>
`).join('');
}
document.getElementById('userSearch').addEventListener('input', (e) => {
const q = e.target.value.toLowerCase();
const filtered = allUsers.filter(u =>
u.email.toLowerCase().includes(q) || u.name.toLowerCase().includes(q)
);
renderUsers(filtered);
});
async function impersonate(userId) {
if (!confirm('Impersonate this user? You will be signed in as them.')) return;
const data = await postAPI('/api/admin/impersonate', { user_id: userId });
if (data.token) {
localStorage.setItem('ds_token', data.token);
localStorage.setItem('ds_user', JSON.stringify(data.user));
window.location.href = '/app/tasks';
}
}
// Projects
async function loadProjects() {
const projects = await fetchAPI('/api/admin/projects');
document.getElementById('projectCount').textContent = projects.length + ' project' + (projects.length !== 1 ? 's' : '');
document.getElementById('projectsGrid').innerHTML = projects.map(p => {
let name = p.summary || 'Unnamed Project';
return `
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5">
<div class="flex items-center justify-between mb-2">
<h3 class="font-semibold text-white">${escapeHtml(name)}</h3>
<span class="text-xs px-2 py-0.5 rounded-full bg-[#c9a84c]/10 text-[#c9a84c]">${escapeHtml(p.stage)}</span>
</div>
<div class="text-sm text-[#94a3b8]">
<span>ID: ${p.entry_id.slice(0, 8)}...</span>
<span class="ml-4">Created: ${formatDate(p.created_at)}</span>
</div>
</div>
`;
}).join('');
}
// Audit
async function loadAudit() {
const entries = await fetchAPI('/api/admin/audit');
document.getElementById('auditTable').innerHTML = entries.length === 0
? '<tr><td colspan="5" class="px-4 py-8 text-center text-[#94a3b8]">No audit events yet</td></tr>'
: entries.map(a => `
<tr class="border-b border-white/[0.05] hover:bg-white/[0.02]">
<td class="px-4 py-3 text-[#94a3b8]">${formatDateTime(a.ts)}</td>
<td class="px-4 py-3">${escapeHtml(a.actor_id ? a.actor_id.slice(0, 8) + '...' : '—')}</td>
<td class="px-4 py-3">${escapeHtml(a.action || '—')}</td>
<td class="px-4 py-3 text-[#94a3b8]">${escapeHtml(a.target_id ? a.target_id.slice(0, 8) + '...' : '—')}</td>
<td class="px-4 py-3 text-[#94a3b8]">${escapeHtml(a.ip || '—')}</td>
</tr>
`).join('');
}
// Logout
document.getElementById('logoutBtn').addEventListener('click', async () => {
try { await fetch('/api/auth/logout', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } }); } catch {}
localStorage.clear();
window.location.href = '/app/login';
});
// Initial load
loadUsers();
</script>
</body>
</html>