Files
musadaq-saas/public/shell.php

739 lines
43 KiB
PHP

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>مُصادَق | Bloomberg Terminal v2.0</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Inter:wght@400;700;900&family=Noto+Kufi+Arabic:wght@400;700;900&display=swap" rel="stylesheet">
<style>
:root {
--bb-bg: #000000;
--bb-panel: #111111;
--bb-border: #222222;
--bb-text: #ffffff;
--bb-dim: #888888;
--bb-green: #00ff00;
--bb-red: #ff3333;
--bb-yellow: #ffff00;
--bb-blue: #0088ff;
}
body {
background-color: var(--bb-bg);
color: var(--bb-text);
font-family: 'Inter', 'Noto Kufi Arabic', sans-serif;
font-size: 13px;
overflow: hidden;
}
.bb-mono { font-family: 'JetBrains Mono', monospace; }
.bb-panel { background: var(--bb-panel); border: 1px solid var(--bb-border); }
.bb-table th { border-bottom: 2px solid var(--bb-border); color: var(--bb-dim); font-weight: 900; text-transform: uppercase; padding: 8px; font-size: 11px; }
.bb-table td { border-bottom: 1px solid var(--bb-border); padding: 8px; vertical-align: middle; }
.bb-btn { border: 1px solid var(--bb-dim); padding: 4px 12px; transition: all 0.2s; }
.bb-btn:hover { background: var(--bb-text); color: var(--bb-bg); }
.bb-btn-primary { border-color: var(--bb-green); color: var(--bb-green); }
.bb-btn-primary:hover { background: var(--bb-green); color: var(--bb-bg); }
.bb-stat { border-right: 2px solid var(--bb-green); padding-right: 12px; }
.bb-scroll::-webkit-scrollbar { width: 4px; height: 4px; }
.bb-scroll::-webkit-scrollbar-track { background: transparent; }
.bb-scroll::-webkit-scrollbar-thumb { background: var(--bb-border); }
.bb-nav-item { border-right: 3px solid transparent; transition: all 0.2s; }
.bb-nav-item.active { border-right-color: var(--bb-green); background: rgba(0, 255, 0, 0.05); }
@keyframes ticker { 0% { transform: translateX(100%); } 100% { transform: translateX(-100%); } }
.ticker-wrap { overflow: hidden; background: var(--bb-panel); border-bottom: 1px solid var(--bb-border); white-space: nowrap; }
.ticker { display: inline-block; animation: ticker 30s linear infinite; }
.status-pill { font-size: 10px; font-weight: 900; padding: 2px 6px; border: 1px solid currentColor; }
</style>
</head>
<body class="h-screen flex flex-col">
<!-- Top Bar / Ticker -->
<div class="ticker-wrap h-8 flex items-center bb-mono text-[11px]">
<div class="px-4 border-l border-bb-border text-bb-green font-bold">SYSTEM ACTIVE</div>
<div class="ticker flex gap-8">
<span class="text-bb-dim">MARKET: <span class="text-white">AMM/JOD</span></span>
<span class="text-bb-dim">LATENCY: <span class="text-bb-green">14ms</span></span>
<span class="text-bb-dim">CPU: <span class="text-bb-green">12%</span></span>
<span class="text-bb-dim">REDIS_JTI: <span class="text-bb-blue">VERIFIED</span></span>
<span class="text-bb-dim">NONCE_CHECK: <span class="text-bb-green">ACTIVE</span></span>
<span class="text-bb-dim">ENCRYPTION: <span class="text-bb-green">AES-256-GCM</span></span>
</div>
</div>
<!-- Main Shell -->
<div class="flex-1 flex overflow-hidden">
<!-- Sidebar Navigation -->
<nav class="w-64 bb-panel border-l-0 flex flex-col">
<div class="p-6 border-b border-bb-border">
<div class="flex items-center gap-3 mb-2">
<div class="w-2 h-2 bg-bb-green animate-pulse rounded-full"></div>
<span class="text-xl font-black tracking-tighter">MUSADEQ.v2</span>
</div>
<div class="text-[10px] text-bb-dim bb-mono" id="user-display">LOADING...</div>
</div>
<div class="flex-1 py-4 bb-mono text-[12px]">
<a href="#" onclick="navigateTo('dashboard')" id="nav-dashboard" class="bb-nav-item active flex items-center gap-3 px-6 py-3 hover:bg-white/5">
<span class="text-bb-dim">01</span> DASHBOARD
</a>
<a href="#" onclick="navigateTo('invoices')" id="nav-invoices" class="bb-nav-item flex items-center gap-3 px-6 py-3 hover:bg-white/5">
<span class="text-bb-dim">02</span> TERMINAL/INVOICES
</a>
<a href="#" onclick="navigateTo('companies')" id="nav-companies" class="bb-nav-item flex items-center gap-3 px-6 py-3 hover:bg-white/5">
<span class="text-bb-dim">03</span> ENTITIES/COMPANIES
</a>
<a href="#" onclick="navigateTo('users')" id="nav-users" class="bb-nav-item flex items-center gap-3 px-6 py-3 hover:bg-white/5">
<span class="text-bb-dim">04</span> ACCESS/USERS
</a>
<a href="#" onclick="navigateTo('risk')" id="nav-risk" class="bb-nav-item flex items-center gap-3 px-6 py-3 hover:bg-white/5">
<span class="text-bb-dim">05</span> RISK_MONITOR
</a>
<div class="mt-8 px-6 text-[10px] text-bb-dim font-bold uppercase tracking-widest">System</div>
<a href="#" onclick="navigateTo('settings')" id="nav-settings" class="bb-nav-item flex items-center gap-3 px-6 py-3 hover:bg-white/5">
<span class="text-bb-dim">09</span> CONFIG/KEYS
</a>
<a href="#" onclick="navigateTo('admin')" id="nav-admin" class="bb-nav-item hidden flex items-center gap-3 px-6 py-3 hover:bg-white/5 text-bb-blue">
<span class="text-bb-dim">10</span> SUPER_ADMIN
</a>
</div>
<div class="p-6 border-t border-bb-border">
<button onclick="logout()" class="w-full bb-btn text-bb-red text-[11px] font-bold">DISCONNECT_TERMINAL</button>
</div>
</nav>
<!-- Content Area -->
<main class="flex-1 flex flex-col overflow-hidden bg-black">
<!-- Header -->
<header class="h-14 bb-panel border-r-0 border-l-0 flex items-center justify-between px-8 bg-black/50 backdrop-blur-md">
<div class="flex items-center gap-6">
<h2 id="page-title" class="text-lg font-black tracking-tighter">DASHBOARD</h2>
<div class="h-4 w-[1px] bg-bb-border"></div>
<div class="flex items-center gap-4 text-[11px] bb-mono">
<span class="text-bb-dim">STATUS:</span>
<span class="text-bb-green">LIVE_TRADING</span>
<span class="text-bb-dim">REGION:</span>
<span class="text-white">JORDAN/AMMAN</span>
</div>
</div>
<div class="flex items-center gap-4">
<button onclick="showUploadModal()" class="bb-btn bb-btn-primary text-[11px] font-black">+ NEW_INVOICE</button>
</div>
</header>
<!-- Main Content Grid -->
<div id="content" class="flex-1 overflow-y-auto bb-scroll p-6">
<!-- Content will be injected here -->
</div>
</main>
</div>
<!-- Modals Overlay -->
<div id="modal-overlay" class="fixed inset-0 bg-black/90 backdrop-blur-sm z-[100] hidden items-center justify-center p-6">
<div id="modal-content" class="w-full max-w-2xl bb-panel p-8 animate-in"></div>
</div>
<!-- Auth Overlay -->
<div id="auth-overlay" class="fixed inset-0 bg-black z-[200] hidden items-center justify-center p-6">
<div id="auth-content" class="w-full max-w-sm bb-panel p-10"></div>
</div>
<!-- Toast Notifications -->
<div id="toast-container" class="fixed bottom-8 left-8 z-[300] space-y-2"></div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const API = {
baseUrl: 'index.php?route=/api/v1',
get token() { return localStorage.getItem('access_token'); },
async req(method, path, body = null, files = false) {
const headers = { 'Accept': 'application/json' };
if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
if (!files && body) { headers['Content-Type'] = 'application/json'; body = JSON.stringify(body); }
try {
const res = await fetch(`${this.baseUrl}${path}`, { method, headers, body });
const data = await res.json();
if (!res.ok) {
if (res.status === 401 && !path.includes('/auth/login')) logout();
throw data;
}
return data;
} catch (err) {
showToast(err.error?.message_ar || err.message || 'SYSTEM_ERROR', 'error');
throw err;
}
},
get(p) { return this.req('GET', p); },
post(p, b) { return this.req('POST', p, b); },
delete(p) { return this.req('DELETE', p); },
upload(p, fd) { return this.req('POST', p, fd, true); }
};
function showToast(msg, type = 'success') {
const container = document.getElementById('toast-container');
const t = document.createElement('div');
const color = type === 'success' ? 'var(--bb-green)' : 'var(--bb-red)';
t.className = `bb-panel p-4 bb-mono text-[11px] border-r-4`;
t.style.borderRightColor = color;
t.innerHTML = `<span style="color: ${color}">[${type.toUpperCase()}]</span> ${msg}`;
container.appendChild(t);
setTimeout(() => t.remove(), 4000);
}
function logout() { localStorage.clear(); window.location.reload(); }
async function navigateTo(page) {
document.querySelectorAll('.bb-nav-item').forEach(l => l.classList.remove('active'));
document.getElementById(`nav-${page}`)?.classList.add('active');
const content = document.getElementById('content');
content.innerHTML = '<div class="h-full flex items-center justify-center bb-mono animate-pulse">TERMINAL_BUSY: LOADING_DATA...</div>';
try {
if (page === 'dashboard') await renderDashboard();
else if (page === 'invoices') await renderInvoices();
else if (page === 'companies') await renderCompanies();
else if (page === 'users') await renderUsers();
else if (page === 'risk') await renderRisk();
else if (page === 'settings') await renderSettings();
else if (page === 'admin') await renderAdmin();
} catch (e) { console.error(e); }
}
// ── View Renderers ───────────────────────────────────────
async function renderDashboard() {
document.getElementById('page-title').textContent = 'DASHBOARD';
const res = await API.get('/dashboard');
const { data: s } = res;
document.getElementById('content').innerHTML = `
<div class="grid grid-cols-4 gap-6 mb-8">
<div class="bb-panel p-6 bb-stat">
<div class="text-bb-dim text-[10px] bb-mono uppercase mb-2">Total Volume / Month</div>
<div class="text-4xl font-black">${s.total_this_month}</div>
<div class="text-[10px] mt-2 text-bb-green">+12.4% FROM PREV</div>
</div>
<div class="bb-panel p-6 bb-stat" style="border-right-color: var(--bb-blue)">
<div class="text-bb-dim text-[10px] bb-mono uppercase mb-2">Subscription Usage</div>
<div class="text-4xl font-black">${s.subscription_usage}%</div>
<div class="w-full h-1 bg-bb-border mt-4"><div class="h-full bg-bb-blue" style="width: ${s.subscription_usage}%"></div></div>
</div>
<div class="bb-panel p-6 bb-stat" style="border-right-color: var(--bb-yellow)">
<div class="text-bb-dim text-[10px] bb-mono uppercase mb-2">Pending AI Processing</div>
<div class="text-4xl font-black">${s.pending_extraction || 0}</div>
<div class="text-[10px] mt-2 text-bb-yellow">QUEUE: OPTIMAL</div>
</div>
<div class="bb-panel p-6 bb-stat" style="border-right-color: var(--bb-green)">
<div class="text-bb-dim text-[10px] bb-mono uppercase mb-2">Approved Ratio</div>
<div class="text-4xl font-black">98.2%</div>
<div class="text-[10px] mt-2 text-bb-green">COMPLIANCE: HIGH</div>
</div>
</div>
<div class="grid grid-cols-3 gap-6">
<div class="col-span-2 bb-panel p-8">
<h3 class="text-sm font-black mb-6 uppercase tracking-widest border-b border-bb-border pb-4">Live Transaction Feed</h3>
<table class="w-full bb-table text-right">
<thead>
<tr>
<th class="text-right">ENTITY</th>
<th class="text-right">ID</th>
<th class="text-right">TOTAL</th>
<th class="text-center">STATUS</th>
<th class="text-left">CONFIDENCE</th>
</tr>
</thead>
<tbody>
${s.recent_invoices.map(i => `
<tr onclick="renderInvoiceDetail('${i.id}')" class="hover:bg-white/5 cursor-pointer">
<td class="font-bold">${i.company_name}</td>
<td class="bb-mono text-xs">${i.invoice_number || '---'}</td>
<td class="font-bold text-bb-green">${i.grand_total} JOD</td>
<td class="text-center">
<span class="status-pill ${getStatusColor(i.status)}">${i.status.toUpperCase()}</span>
</td>
<td class="text-left bb-mono text-xs">${(i.ai_confidence_score * 100).toFixed(1)}%</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div class="bb-panel p-8 flex flex-col">
<h3 class="text-sm font-black mb-6 uppercase tracking-widest border-b border-bb-border pb-4">Status Distribution</h3>
<div class="flex-1 flex items-center justify-center">
<canvas id="statusChart"></canvas>
</div>
</div>
</div>
`;
new Chart(document.getElementById('statusChart'), {
type: 'doughnut',
data: {
labels: s.status_distribution.map(x => x.status),
datasets: [{
data: s.status_distribution.map(x => x.count),
backgroundColor: ['#00ff00', '#ffff00', '#ff3333', '#0088ff'],
borderWidth: 0
}]
},
options: {
cutout: '80%',
plugins: { legend: { position: 'bottom', labels: { color: '#888888', font: { size: 10, family: 'JetBrains Mono' } } } }
}
});
}
async function renderInvoices() {
document.getElementById('page-title').textContent = 'TERMINAL / INVOICES';
const res = await API.get('/invoices');
document.getElementById('content').innerHTML = `
<div class="bb-panel p-8">
<div class="flex justify-between items-center mb-8">
<div class="flex gap-4">
<input type="text" placeholder="FILTER_BY_ID..." class="bb-panel bg-black px-4 py-2 text-xs bb-mono w-64">
<select class="bb-panel bg-black px-4 py-2 text-xs bb-mono">
<option>ALL_STATUSES</option>
<option>APPROVED</option>
<option>PENDING</option>
</select>
</div>
<div class="text-[10px] text-bb-dim bb-mono">RECORDS: ${res.data.length} | PAGE: 1/1</div>
</div>
<table class="w-full bb-table text-right">
<thead>
<tr>
<th class="text-right">DATE</th>
<th class="text-right">ENTITY</th>
<th class="text-right">SERIAL_NO</th>
<th class="text-right">TAX</th>
<th class="text-right">TOTAL_JOD</th>
<th class="text-center">STATE</th>
<th class="text-left">CMD</th>
</tr>
</thead>
<tbody>
${res.data.map(i => `
<tr class="hover:bg-white/5 transition">
<td class="bb-mono text-xs">${i.invoice_date || '---'}</td>
<td class="font-bold">${i.company_name}</td>
<td class="bb-mono text-xs">${i.invoice_number || '---'}</td>
<td class="text-bb-dim">${i.tax_amount}</td>
<td class="font-bold text-bb-green">${i.grand_total}</td>
<td class="text-center"><span class="status-pill ${getStatusColor(i.status)}">${i.status.toUpperCase()}</span></td>
<td class="text-left">
<button onclick="renderInvoiceDetail('${i.id}')" class="bb-btn text-[10px] font-black">OPEN</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
async function renderInvoiceDetail(id) {
const res = await API.get(`/invoices/${id}`);
const i = res.data;
document.getElementById('content').innerHTML = `
<div class="grid grid-cols-2 gap-8 h-full">
<div class="bb-panel flex flex-col overflow-hidden">
<div class="p-4 border-b border-bb-border flex justify-between items-center bg-white/5">
<span class="bb-mono text-[10px] uppercase">Original_Source_Visual</span>
<a href="/api/v1/invoices/${i.id}/file" target="_blank" class="text-bb-blue text-[10px] hover:underline font-bold">EXTERNAL_VIEW ↗</a>
</div>
<div class="flex-1 bg-zinc-900 flex items-center justify-center p-4">
${i.original_file_path.endsWith('.pdf') ?
`<iframe src="/api/v1/invoices/${i.id}/file" class="w-full h-full border-0"></iframe>` :
`<img src="/api/v1/invoices/${i.id}/file" class="max-w-full max-h-full shadow-2xl">`}
</div>
</div>
<div class="flex flex-col gap-8">
<div class="bb-panel p-8">
<div class="flex justify-between items-start mb-8">
<div>
<h2 class="text-2xl font-black tracking-tighter mb-2">${i.supplier_name || 'UNIDENTIFIED_ENTITY'}</h2>
<div class="flex gap-4 text-[10px] bb-mono">
<span class="text-bb-dim">INV_NO:</span> <span class="text-white">${i.invoice_number || 'PENDING'}</span>
<span class="text-bb-dim">TAX_ID:</span> <span class="text-bb-green">${i.supplier_tin || '---'}</span>
</div>
</div>
<div class="flex flex-col gap-2 items-end">
<span class="status-pill ${getStatusColor(i.status)} text-sm px-4 py-2">${i.status.toUpperCase()}</span>
<div class="text-[10px] bb-mono text-bb-dim">AI_SCORE: ${(i.ai_confidence_score * 100).toFixed(2)}%</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-8 bb-mono">
<div class="bb-panel p-4">
<div class="text-bb-dim text-[9px] mb-1 uppercase">Buyer_Entity</div>
<div class="font-bold">${i.buyer_name || 'GENERAL_RETAIL'}</div>
<div class="text-[9px] text-bb-dim mt-1">ID: ${i.buyer_tin || '---'}</div>
</div>
<div class="bb-panel p-4">
<div class="text-bb-dim text-[9px] mb-1 uppercase">Timestamp</div>
<div class="font-bold">${i.invoice_date || '---'}</div>
<div class="text-[9px] text-bb-dim mt-1">ISSUED_AT: ${i.created_at}</div>
</div>
</div>
<div class="bb-scroll overflow-y-auto max-h-64 mb-8">
<table class="w-full bb-table text-right text-xs">
<thead>
<tr>
<th class="text-right">DESC</th>
<th class="text-center">QTY</th>
<th class="text-right">PRICE</th>
<th class="text-right">TOTAL</th>
</tr>
</thead>
<tbody>
${i.lines.map(l => `
<tr>
<td class="text-bb-dim">${l.description}</td>
<td class="text-center bb-mono">${l.quantity}</td>
<td class="bb-mono">${l.unit_price}</td>
<td class="font-bold text-bb-green">${l.line_total}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div class="border-t border-bb-border pt-6 bb-mono">
<div class="flex justify-between text-xs mb-2">
<span class="text-bb-dim">SUB_TOTAL</span>
<span>${i.subtotal} JOD</span>
</div>
<div class="flex justify-between text-xs mb-2">
<span class="text-bb-dim">TAX_SUM (16%)</span>
<span class="text-bb-yellow">${i.tax_amount} JOD</span>
</div>
<div class="flex justify-between text-xl font-black mt-4 border-t border-bb-border pt-4">
<span class="text-bb-dim">GRAND_TOTAL</span>
<span class="text-bb-green">${i.grand_total} JOD</span>
</div>
</div>
</div>
<div class="flex gap-4">
<button onclick="submitToJoFotara('${i.id}')" class="flex-1 bb-btn bb-btn-primary py-4 font-black">SUBMIT_TO_GOV_API</button>
<button onclick="navigateTo('invoices')" class="bb-btn px-8 font-black">BACK_TO_LIST</button>
</div>
</div>
</div>
`;
}
async function renderCompanies() {
document.getElementById('page-title').textContent = 'ENTITIES / COMPANIES';
const res = await API.get('/companies');
document.getElementById('content').innerHTML = `
<div class="flex justify-end mb-6">
<button onclick="showAddCompanyModal()" class="bb-btn bb-btn-primary">+ REGISTER_ENTITY</button>
</div>
<div class="grid grid-cols-3 gap-6">
${res.data.map(c => `
<div class="bb-panel p-8 border-t-4 border-t-bb-green">
<div class="flex justify-between items-start mb-6">
<h3 class="text-xl font-black tracking-tighter uppercase">${c.name}</h3>
<span class="status-pill ${c.is_jofotara_linked ? 'text-bb-green' : 'text-bb-red'}">${c.is_jofotara_linked ? 'LINKED' : 'UNLINKED'}</span>
</div>
<div class="space-y-3 bb-mono text-xs">
<div class="flex justify-between"><span class="text-bb-dim">TAX_ID:</span> <span>${c.tax_identification_number}</span></div>
<div class="flex justify-between"><span class="text-bb-dim">REGION:</span> <span>${c.city || 'AMMAN'}</span></div>
<div class="flex justify-between"><span class="text-bb-dim">INVOICES:</span> <span class="text-bb-blue">---</span></div>
</div>
<div class="mt-8 flex gap-2">
<button onclick="showJoFotaraModal('${c.id}')" class="flex-1 bb-btn text-[10px] font-black">GOV_SETTINGS</button>
<button class="bb-btn text-bb-red text-[10px] font-black px-4">DEACTIVATE</button>
</div>
</div>
`).join('')}
</div>
`;
}
async function renderUsers() {
document.getElementById('page-title').textContent = 'ACCESS / USERS';
const res = await API.get('/users');
document.getElementById('content').innerHTML = `
<div class="bb-panel p-8">
<div class="flex justify-between items-center mb-8">
<h3 class="text-sm font-black uppercase tracking-widest">Authorized Terminals</h3>
<button onclick="showAddUserModal()" class="bb-btn bb-btn-primary">+ ADD_USER</button>
</div>
<table class="w-full bb-table text-right">
<thead>
<tr>
<th class="text-right">NAME</th>
<th class="text-right">EMAIL_IDENTITY</th>
<th class="text-right">ACCESS_ROLE</th>
<th class="text-center">2FA</th>
<th class="text-left">CMD</th>
</tr>
</thead>
<tbody>
${res.data.map(u => `
<tr>
<td class="font-bold">${u.name}</td>
<td class="bb-mono text-xs text-bb-dim">${u.email}</td>
<td><span class="status-pill text-bb-blue">${u.role.toUpperCase()}</span></td>
<td class="text-center">${u.totp_enabled ? '<span class="text-bb-green">ON</span>' : '<span class="text-bb-red">OFF</span>'}</td>
<td class="text-left">
<button onclick="deleteUser('${u.id}')" class="text-bb-red hover:underline text-[10px] bb-mono">REVOKE_ACCESS</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
async function renderSettings() {
document.getElementById('page-title').textContent = 'CONFIG / KEYS';
const u = (await API.get('/auth/me')).data;
document.getElementById('content').innerHTML = `
<div class="max-w-2xl space-y-8">
<div class="bb-panel p-8 border-r-4 border-r-bb-blue">
<h3 class="text-lg font-black mb-2 tracking-tighter">SECURE_AUTH (2FA)</h3>
<p class="text-bb-dim text-[11px] mb-6">Multi-factor authentication status for your terminal session.</p>
<div id="2fa-area">
${u.totp_enabled ?
`<button onclick="disable2FA()" class="bb-btn text-bb-red font-black">DISABLE_2FA_PROTECTION</button>` :
`<button onclick="start2FA()" class="bb-btn bb-btn-primary font-black">ENABLE_MFA_NOW</button>`}
</div>
</div>
<div class="bb-panel p-8 border-r-4 border-r-bb-yellow">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-black tracking-tighter">API_ACCESS_KEYS</h3>
<button onclick="createApiKey()" class="bb-btn bb-btn-primary text-[10px]">+ GENERATE_NEW_PAIR</button>
</div>
<div id="api-keys-list" class="space-y-4">
<div class="bb-mono text-[10px] animate-pulse">POLLING_KEYS...</div>
</div>
</div>
</div>
`;
loadApiKeys();
}
async function renderAdmin() {
document.getElementById('page-title').textContent = 'SUPER_ADMIN / CONTROL';
const s = (await API.get('/admin/stats')).data;
const h = (await API.get('/admin/health')).data;
document.getElementById('content').innerHTML = `
<div class="grid grid-cols-2 gap-8">
<div class="bb-panel p-8">
<h3 class="text-sm font-black mb-6 border-b border-bb-border pb-4 uppercase">System Health</h3>
<div class="grid grid-cols-2 gap-4 bb-mono text-xs">
<div class="bb-panel p-4">DB_CLUSTER: <span class="${h.db==='ok'?'text-bb-green':'text-bb-red'}">${h.db.toUpperCase()}</span></div>
<div class="bb-panel p-4">REDIS_CACHE: <span class="${h.redis==='ok'?'text-bb-green':'text-bb-red'}">${h.redis.toUpperCase()}</span></div>
<div class="bb-panel p-4">QUEUE_PENDING: <span class="text-bb-yellow">${h.queue_pending}</span></div>
<div class="bb-panel p-4">QUEUE_DEAD: <span class="text-bb-red">${h.queue_dead}</span></div>
</div>
<div class="mt-8 flex gap-2">
<button onclick="navigateTo('admin_tenants')" class="flex-1 bb-btn text-[10px] font-black">MANAGE_TENANTS</button>
<button onclick="navigateTo('admin_queue')" class="flex-1 bb-btn text-[10px] font-black">QUEUE_INSPECTOR</button>
</div>
</div>
<div class="bb-panel p-8">
<h3 class="text-sm font-black mb-6 border-b border-bb-border pb-4 uppercase">Platform Aggregates</h3>
<div class="space-y-4">
<div class="flex justify-between items-end">
<span class="bb-mono text-bb-dim text-[10px]">TOTAL_TENANTS</span>
<span class="text-3xl font-black">${s.tenants}</span>
</div>
<div class="flex justify-between items-end">
<span class="bb-mono text-bb-dim text-[10px]">TOTAL_INVOICES</span>
<span class="text-3xl font-black">${s.invoices}</span>
</div>
<div class="flex justify-between items-end">
<span class="bb-mono text-bb-dim text-[10px]">TOTAL_USERS</span>
<span class="text-3xl font-black">${s.users}</span>
</div>
</div>
</div>
</div>
`;
}
// ── Helper Functions ─────────────────────────────────────
function getStatusColor(s) {
const m = { 'approved': 'text-bb-green', 'rejected': 'text-bb-red', 'extracted': 'text-bb-blue', 'uploaded': 'text-bb-dim', 'validation_failed': 'text-bb-red' };
return m[s] || 'text-bb-yellow';
}
async function loadApiKeys() {
const res = await API.get('/api-keys');
document.getElementById('api-keys-list').innerHTML = res.data.map(k => `
<div class="bb-panel p-4 flex justify-between items-center bg-black/40">
<div>
<div class="font-bold bb-mono text-xs">${k.name}</div>
<div class="text-[9px] text-bb-dim bb-mono mt-1">PUB: ${k.public_key}</div>
</div>
<button onclick="revokeApiKey('${k.id}')" class="text-bb-red text-[9px] bb-mono hover:underline">REVOKE</button>
</div>
`).join('') || '<div class="text-bb-dim text-xs bb-mono">NO_KEYS_FOUND</div>';
}
async function showUploadModal() {
const comps = (await API.get('/companies')).data;
showModal(`
<h2 class="text-xl font-black mb-8 border-b border-bb-border pb-4 uppercase tracking-tighter">New Invoice Upload</h2>
<form id="upload-form" class="space-y-6">
<div class="space-y-2">
<label class="text-[10px] bb-mono text-bb-dim">TARGET_ENTITY</label>
<select name="company_id" class="w-full bb-panel bg-black p-3 bb-mono text-sm border-bb-dim" required>
<option value="">SELECT_COMPANY...</option>
${comps.map(c => `<option value="${c.id}">${c.name}</option>`).join('')}
</select>
</div>
<div class="space-y-2">
<label class="text-[10px] bb-mono text-bb-dim">INVOICE_DOCUMENT (PDF/IMG)</label>
<div class="bb-panel p-10 border-dashed text-center bg-white/5 border-bb-dim">
<input type="file" name="file" class="bb-mono text-xs" required>
</div>
</div>
<div class="flex gap-4 pt-4">
<button type="submit" class="flex-1 bb-btn bb-btn-primary font-black py-4">START_AI_EXTRACTION</button>
<button type="button" onclick="closeModal()" class="bb-btn px-10">CANCEL</button>
</div>
</form>
`);
document.getElementById('upload-form').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
try {
const btn = e.target.querySelector('button[type="submit"]');
btn.disabled = true; btn.textContent = 'UPLOADING...';
await API.upload('/invoices/upload', fd);
showToast('INVOICE_UPLOADED: AI_QUEUE_STARTED');
closeModal();
navigateTo('invoices');
} catch(err) {}
};
}
// ── Auth Logic ───────────────────────────────────────────
async function initApp() {
if (!API.token) {
renderLogin();
return;
}
try {
const u = (await API.get('/auth/me')).data;
document.getElementById('user-display').textContent = `USER: ${u.name} | ROLE: ${u.role.toUpperCase()}`;
if (u.role === 'super_admin') document.getElementById('nav-admin').classList.remove('hidden');
navigateTo('dashboard');
} catch(e) { renderLogin(); }
}
function renderLogin() {
const overlay = document.getElementById('auth-overlay');
overlay.classList.replace('hidden', 'flex');
document.getElementById('auth-content').innerHTML = `
<div class="text-center mb-10">
<h1 class="text-3xl font-black tracking-tighter mb-2">MUSADEQ_LOGIN</h1>
<p class="text-bb-dim text-[10px] bb-mono uppercase">Secure Financial Terminal</p>
</div>
<form id="login-form" class="space-y-6">
<input type="email" name="email" placeholder="IDENTITY@DOMAIN" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
<input type="password" name="password" placeholder="PASSWORD" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
<button type="submit" class="w-full bb-btn bb-btn-primary py-4 font-black">ESTABLISH_SESSION</button>
</form>
<div class="mt-8 text-center text-bb-dim text-[10px] bb-mono">
NO_ACCOUNT? <a href="#" onclick="renderRegister()" class="text-white hover:underline">REGISTER_NOW</a>
</div>
`;
document.getElementById('login-form').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
try {
const res = await API.post('/auth/login', Object.fromEntries(fd));
if (res.requires_2fa) render2FAChallenge(res.temp_token);
else saveAuth(res.data);
} catch(err) {}
};
}
function render2FAChallenge(tempToken) {
document.getElementById('auth-content').innerHTML = `
<div class="text-center mb-10">
<h1 class="text-3xl font-black tracking-tighter mb-2">2FA_CHALLENGE</h1>
<p class="text-bb-dim text-[10px] bb-mono uppercase">Enter terminal access code</p>
</div>
<form id="2fa-form" class="space-y-6">
<input type="text" name="code" placeholder="000000" class="w-full bb-panel bg-black p-3 bb-mono text-center text-xl tracking-[1em]" required autofocus>
<button type="submit" class="w-full bb-btn bb-btn-primary py-4 font-black">VERIFY_IDENTITY</button>
</form>
`;
document.getElementById('2fa-form').onsubmit = async (e) => {
e.preventDefault();
const code = new FormData(e.target).get('code');
try {
// Note: We use the temp token in the header
const res = await fetch('index.php?route=/api/v1/auth/2fa/verify_login', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${tempToken}` },
body: JSON.stringify({ code })
});
const data = await res.json();
if (res.ok) saveAuth(data.data);
else showToast(data.error?.message_ar || 'INVALID_CODE', 'error');
} catch(err) {}
};
}
function renderRegister() {
document.getElementById('auth-content').innerHTML = `
<div class="text-center mb-10">
<h1 class="text-3xl font-black tracking-tighter mb-2">NEW_TERMINAL</h1>
<p class="text-bb-dim text-[10px] bb-mono uppercase">Register Platform Account</p>
</div>
<form id="register-form" class="space-y-4">
<input type="text" name="name" placeholder="FULL_NAME" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
<input type="email" name="email" placeholder="EMAIL_IDENTITY" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
<input type="password" name="password" placeholder="PASSWORD" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
<button type="submit" class="w-full bb-btn bb-btn-primary py-4 font-black">INITIALIZE_ACCOUNT</button>
</form>
<div class="mt-8 text-center text-bb-dim text-[10px] bb-mono">
ALREADY_HAVE_ACCESS? <a href="#" onclick="renderLogin()" class="text-white hover:underline">LOGIN_TERMINAL</a>
</div>
`;
document.getElementById('register-form').onsubmit = async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
try {
const res = await API.post('/auth/register', Object.fromEntries(fd));
saveAuth(res.data);
} catch(err) {}
};
}
function saveAuth(data) {
localStorage.setItem('access_token', data.access_token);
window.location.reload();
}
function showModal(html) {
document.getElementById('modal-content').innerHTML = html;
document.getElementById('modal-overlay').classList.replace('hidden', 'flex');
}
function closeModal() { document.getElementById('modal-overlay').classList.replace('flex', 'hidden'); }
initApp();
</script>
</body>
</html>