1073 lines
68 KiB
PHP
1073 lines
68 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>مُصادَق | لوحة التحكم 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; cursor: pointer; }
|
|
.bb-btn:hover:not(:disabled) { background: var(--bb-text); color: var(--bb-bg); }
|
|
.bb-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.bb-btn-primary { border-color: var(--bb-green); color: var(--bb-green); }
|
|
.bb-btn-primary:hover:not(:disabled) { 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; direction: ltr; }
|
|
.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-r border-bb-border text-bb-green font-bold">النظام نشط</div>
|
|
<div class="ticker flex gap-8 pr-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-r-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">مُصادَق.v2</span>
|
|
</div>
|
|
<div class="text-[10px] text-bb-dim bb-mono" id="user-display">جاري التحميل...</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> لوحة القيادة
|
|
</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> الفواتير والعمليات
|
|
</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> الشركات (الكيانات)
|
|
</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> المستخدمين والصلاحيات
|
|
</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> مؤشرات المخاطر
|
|
</a>
|
|
<div class="mt-8 px-6 text-[10px] text-bb-dim font-bold uppercase tracking-widest">إدارة النظام</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> الإعدادات ومفاتيح الربط
|
|
</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)</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">لوحة القيادة</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">الحالة:</span>
|
|
<span class="text-bb-green">تداول حي (LIVE)</span>
|
|
<span class="text-bb-dim">المنطقة:</span>
|
|
<span class="text-white">الأردن / عمّان</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<button onclick="showUploadModal()" class="bb-btn bb-btn-primary text-[11px] font-black">+ فاتورة جديدة</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 });
|
|
let data;
|
|
const isJson = res.headers.get('content-type')?.includes('application/json');
|
|
if (isJson) {
|
|
data = await res.json();
|
|
} else {
|
|
const text = await res.text();
|
|
console.error('Non-JSON response:', text);
|
|
throw { error: { message_ar: 'استجابة غير متوقعة من الخادم (تأكد من الرابط)' } };
|
|
}
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 401 && !path.includes('/auth/login')) logout();
|
|
throw data;
|
|
}
|
|
return data;
|
|
} catch (err) {
|
|
showToast(err.error?.message_ar || err.error?.message || err.message || 'خطأ غير متوقع في النظام', '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 text-bb-dim">جاري تحميل البيانات... (TERMINAL BUSY)</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();
|
|
else if (page === 'admin_tenants') await renderAdminTenants();
|
|
else if (page === 'admin_queue') await renderAdminQueue();
|
|
} catch (e) {
|
|
console.error(e);
|
|
content.innerHTML = '<div class="h-full flex items-center justify-center bb-mono text-bb-red">فشل تحميل الصفحة. يرجى التحقق من الشبكة أو الخادم.</div>';
|
|
}
|
|
}
|
|
|
|
// ── View Renderers ───────────────────────────────────────
|
|
|
|
async function renderDashboard() {
|
|
document.getElementById('page-title').textContent = 'لوحة القيادة';
|
|
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">إجمالي فواتير الشهر</div>
|
|
<div class="text-4xl font-black">${s.total_this_month || 0}</div>
|
|
<div class="text-[10px] mt-2 text-bb-green">+12.4% عن السابق</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">استهلاك الباقة</div>
|
|
<div class="text-4xl font-black">${s.subscription_usage || 0}%</div>
|
|
<div class="w-full h-1 bg-bb-border mt-4"><div class="h-full bg-bb-blue" style="width: ${s.subscription_usage || 0}%"></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">قيد المعالجة (AI)</div>
|
|
<div class="text-4xl font-black">${s.pending_extraction || 0}</div>
|
|
<div class="text-[10px] mt-2 text-bb-yellow">حالة الطابور: ممتاز</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">نسبة القبول (جوفوترة)</div>
|
|
<div class="text-4xl font-black">98.2%</div>
|
|
<div class="text-[10px] mt-2 text-bb-green">التوافق الضريبي: عالي</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">موجز العمليات الحديثة</h3>
|
|
<table class="w-full bb-table text-right">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-right">الشركة</th>
|
|
<th class="text-right">رقم الفاتورة</th>
|
|
<th class="text-right">المجموع</th>
|
|
<th class="text-center">الحالة</th>
|
|
<th class="text-left">دقة الذكاء الاصطناعي</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} دينار</td>
|
|
<td class="text-center">
|
|
<span class="status-pill ${getStatusColor(i.status)}">${translateStatus(i.status)}</span>
|
|
</td>
|
|
<td class="text-left bb-mono text-xs">${i.ai_confidence_score ? (i.ai_confidence_score * 100).toFixed(1) + '%' : '---'}</td>
|
|
</tr>
|
|
`).join('')}
|
|
${(!s.recent_invoices || s.recent_invoices.length === 0) ? `<tr><td colspan="5" class="text-center text-bb-dim py-4">لا توجد عمليات مسجلة</td></tr>` : ''}
|
|
</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">توزيع حالات الفواتير</h3>
|
|
<div class="flex-1 flex items-center justify-center">
|
|
<canvas id="statusChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
if (s.status_distribution && s.status_distribution.length > 0) {
|
|
new Chart(document.getElementById('statusChart'), {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: s.status_distribution.map(x => translateStatus(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: 'Noto Kufi Arabic' } } } }
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async function renderInvoices() {
|
|
document.getElementById('page-title').textContent = 'الفواتير والعمليات';
|
|
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="بحث برقم الفاتورة..." class="bb-panel bg-black px-4 py-2 text-xs bb-mono w-64 text-right">
|
|
<select class="bb-panel bg-black px-4 py-2 text-xs bb-mono">
|
|
<option>جميع الحالات</option>
|
|
<option>مقبولة</option>
|
|
<option>قيد الانتظار</option>
|
|
</select>
|
|
</div>
|
|
<div class="text-[10px] text-bb-dim bb-mono">الإجمالي: ${res.data.length} | الصفحة: 1/1</div>
|
|
</div>
|
|
<table class="w-full bb-table text-right">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-right">التاريخ</th>
|
|
<th class="text-right">الشركة</th>
|
|
<th class="text-right">الرقم المتسلسل</th>
|
|
<th class="text-right">الضريبة</th>
|
|
<th class="text-right">الإجمالي</th>
|
|
<th class="text-center">الحالة</th>
|
|
<th class="text-left">إجراءات</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${res.data.map(i => `
|
|
<tr class="hover:bg-white/5 transition">
|
|
<td class="bb-mono text-xs text-left" dir="ltr">${i.invoice_date || '---'}</td>
|
|
<td class="font-bold">${i.company_name}</td>
|
|
<td class="bb-mono text-xs text-left" dir="ltr">${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)}">${translateStatus(i.status)}</span></td>
|
|
<td class="text-left">
|
|
<button onclick="renderInvoiceDetail('${i.id}')" class="bb-btn text-[10px] font-black">فتح وعرض</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
${res.data.length === 0 ? `<tr><td colspan="7" class="text-center py-4 text-bb-dim">لا توجد فواتير</td></tr>` : ''}
|
|
</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">المستند الأصلي (المصدر)</span>
|
|
<a href="/api/v1/invoices/${i.id}/file" target="_blank" class="text-bb-blue text-[10px] hover:underline font-bold">عرض خارجي ↗</a>
|
|
</div>
|
|
<div class="flex-1 bg-zinc-900 flex items-center justify-center p-4">
|
|
${i.original_file_path && 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 || 'كيان غير معروف'}</h2>
|
|
<div class="flex gap-4 text-[10px] bb-mono">
|
|
<span class="text-bb-dim">رقم الفاتورة:</span> <span class="text-white">${i.invoice_number || 'قيد الانتظار'}</span>
|
|
<span class="text-bb-dim">الرقم الضريبي:</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">${translateStatus(i.status)}</span>
|
|
<div class="text-[10px] bb-mono text-bb-dim">دقة الذكاء الاصطناعي: ${i.ai_confidence_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">بيانات المشتري</div>
|
|
<div class="font-bold">${i.buyer_name || 'مشتري عام (نقدي)'}</div>
|
|
<div class="text-[9px] text-bb-dim mt-1">الرقم الضريبي: ${i.buyer_tin || '---'}</div>
|
|
</div>
|
|
<div class="bb-panel p-4">
|
|
<div class="text-bb-dim text-[9px] mb-1 uppercase">بيانات الوقت والتاريخ</div>
|
|
<div class="font-bold">${i.invoice_date || '---'}</div>
|
|
<div class="text-[9px] text-bb-dim mt-1 text-left" dir="ltr">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">الوصف (السلعة/الخدمة)</th>
|
|
<th class="text-center">الكمية</th>
|
|
<th class="text-right">السعر الإفرادي</th>
|
|
<th class="text-right">الإجمالي</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${i.lines && i.lines.length > 0 ? 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('') : `<tr><td colspan="4" class="text-center text-bb-dim">لا توجد أصناف مستخرجة أو قيد المعالجة</td></tr>`}
|
|
</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">المجموع الفرعي</span>
|
|
<span>${i.subtotal || 0} دينار</span>
|
|
</div>
|
|
<div class="flex justify-between text-xs mb-2">
|
|
<span class="text-bb-dim">قيمة الضريبة المضافة (16%)</span>
|
|
<span class="text-bb-yellow">${i.tax_amount || 0} دينار</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">المجموع الكلي</span>
|
|
<span class="text-bb-green">${i.grand_total || 0} دينار</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" ${i.status === 'approved' ? 'disabled' : ''}>إرسال لنظام جوفوترة</button>
|
|
<button onclick="navigateTo('invoices')" class="bb-btn px-8 font-black">العودة للقائمة</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function submitToJoFotara(id) {
|
|
if(!confirm('هل أنت متأكد من إرسال واعتماد هذه الفاتورة رسمياً عبر بوابة جوفوترة؟ لا يمكن التراجع.')) return;
|
|
try {
|
|
await API.post(`/invoices/${id}/submit`);
|
|
showToast('جاري إرسال الفاتورة...');
|
|
setTimeout(() => renderInvoiceDetail(id), 2000);
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function renderCompanies() {
|
|
document.getElementById('page-title').textContent = 'الشركات (الكيانات التابعة)';
|
|
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 font-black">+ تسجيل شركة جديدة</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 border-bb-green' : 'text-bb-red border-bb-red'}">${c.is_jofotara_linked ? 'مربوط' : 'غير مربوط'}</span>
|
|
</div>
|
|
<div class="space-y-3 bb-mono text-xs">
|
|
<div class="flex justify-between"><span class="text-bb-dim">الرقم الضريبي:</span> <span>${c.tax_identification_number}</span></div>
|
|
<div class="flex justify-between"><span class="text-bb-dim">المدينة:</span> <span>${c.city || '---'}</span></div>
|
|
</div>
|
|
<div class="mt-8 flex gap-2">
|
|
<button onclick="showJoFotaraModal('${c.id}')" class="flex-1 bb-btn text-[10px] font-black">إعدادات الربط الحكومي</button>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
${res.data.length === 0 ? `<div class="col-span-3 text-center py-10 text-bb-dim">لم يتم إضافة أي شركات بعد</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function renderUsers() {
|
|
document.getElementById('page-title').textContent = 'المستخدمين والصلاحيات';
|
|
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">صلاحيات الوصول</h3>
|
|
<button onclick="showAddUserModal()" class="bb-btn bb-btn-primary font-black">+ إضافة مستخدم</button>
|
|
</div>
|
|
<table class="w-full bb-table text-right">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-right">الاسم الكامل</th>
|
|
<th class="text-right text-left" dir="ltr">البريد الإلكتروني</th>
|
|
<th class="text-right">نوع الصلاحية</th>
|
|
<th class="text-center">2FA (التحقق الثنائي)</th>
|
|
<th class="text-left">إجراءات</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 text-left" dir="ltr">${u.email}</td>
|
|
<td><span class="status-pill text-bb-blue">${translateRole(u.role)}</span></td>
|
|
<td class="text-center">${u.totp_enabled ? '<span class="text-bb-green">مفعل</span>' : '<span class="text-bb-red">معطل</span>'}</td>
|
|
<td class="text-left">
|
|
<button onclick="deleteUser('${u.id}')" class="text-bb-red hover:underline text-[10px] bb-mono">إلغاء الصلاحية (إزالة)</button>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function renderRisk() {
|
|
document.getElementById('page-title').textContent = 'مؤشرات المخاطر';
|
|
try {
|
|
const res = await API.get('/risk');
|
|
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">تنبيهات التدقيق والمخاطر الضريبية</h3>
|
|
</div>
|
|
<table class="w-full bb-table text-right">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-right">التاريخ</th>
|
|
<th class="text-right">الشركة</th>
|
|
<th class="text-right">الفاتورة</th>
|
|
<th class="text-right">مستوى الخطر</th>
|
|
<th class="text-right">السبب</th>
|
|
<th class="text-center">الحالة</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${res.data.map(r => `
|
|
<tr class="hover:bg-white/5 transition">
|
|
<td class="bb-mono text-xs text-left" dir="ltr">${r.created_at}</td>
|
|
<td class="font-bold">${r.company_name}</td>
|
|
<td class="bb-mono text-xs">${r.invoice_number || '---'}</td>
|
|
<td class="${getRiskColor(r.risk_level)} font-bold">${translateRisk(r.risk_level)}</td>
|
|
<td class="text-bb-dim text-xs">${r.reason}</td>
|
|
<td class="text-center">${r.is_resolved ? '<span class="text-bb-green">محلول</span>' : '<span class="text-bb-red font-bold animate-pulse">قيد الانتظار</span>'}</td>
|
|
</tr>
|
|
`).join('')}
|
|
${res.data.length === 0 ? `<tr><td colspan="6" class="text-center py-10 text-bb-dim">لا توجد مخاطر مسجلة حتى الآن</td></tr>` : ''}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
} catch (e) {
|
|
document.getElementById('content').innerHTML = `<div class="bb-panel p-8 text-bb-dim text-center">خدمة المخاطر غير متاحة حالياً أو قيد الإنشاء</div>`;
|
|
}
|
|
}
|
|
|
|
async function renderSettings() {
|
|
document.getElementById('page-title').textContent = 'الإعدادات ومفاتيح الربط';
|
|
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">الحماية المتقدمة (التحقق الثنائي 2FA)</h3>
|
|
<p class="text-bb-dim text-[11px] mb-6">يضيف التحقق بخطوتين طبقة حماية إضافية لحسابك.</p>
|
|
<div id="2fa-area">
|
|
${u.totp_enabled ?
|
|
`<button onclick="disable2FA()" class="bb-btn text-bb-red font-black border-bb-red">إيقاف التحقق الثنائي</button>` :
|
|
`<button onclick="start2FA()" class="bb-btn bb-btn-primary font-black">تفعيل التحقق الثنائي الآن</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 Keys)</h3>
|
|
<button onclick="createApiKey()" class="bb-btn bb-btn-primary text-[10px] font-black">+ إنشاء مفتاح جديد</button>
|
|
</div>
|
|
<div id="api-keys-list" class="space-y-4">
|
|
<div class="bb-mono text-[10px] animate-pulse text-bb-dim">جاري جلب المفاتيح...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
loadApiKeys();
|
|
}
|
|
|
|
async function renderAdmin() {
|
|
document.getElementById('page-title').textContent = 'الإدارة العليا للأنظمة (SUPER ADMIN)';
|
|
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">صحة البنية التحتية</h3>
|
|
<div class="grid grid-cols-2 gap-4 bb-mono text-xs">
|
|
<div class="bb-panel p-4">قواعد البيانات: <span class="${h.db==='ok'?'text-bb-green':'text-bb-red'}">${h.db.toUpperCase()}</span></div>
|
|
<div class="bb-panel p-4">كاش Redis: <span class="${h.redis==='ok'?'text-bb-green':'text-bb-red'}">${h.redis.toUpperCase()}</span></div>
|
|
<div class="bb-panel p-4">المهام المنتظرة: <span class="text-bb-yellow">${h.queue_pending}</span></div>
|
|
<div class="bb-panel p-4">المهام الفاشلة: <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">إدارة الحسابات (Tenants)</button>
|
|
<button onclick="navigateTo('admin_queue')" class="flex-1 bb-btn text-[10px] font-black">مراقب الطابور (Queue)</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">إحصائيات المنصة الكلية</h3>
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-end">
|
|
<span class="bb-mono text-bb-dim text-[10px]">المستأجرين (TENANTS)</span>
|
|
<span class="text-3xl font-black text-white">${s.tenants}</span>
|
|
</div>
|
|
<div class="flex justify-between items-end">
|
|
<span class="bb-mono text-bb-dim text-[10px]">الفواتير المتراكمة</span>
|
|
<span class="text-3xl font-black text-bb-green">${s.invoices}</span>
|
|
</div>
|
|
<div class="flex justify-between items-end">
|
|
<span class="bb-mono text-bb-dim text-[10px]">إجمالي المستخدمين</span>
|
|
<span class="text-3xl font-black text-bb-blue">${s.users}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function renderAdminTenants() {
|
|
document.getElementById('page-title').textContent = 'إدارة حسابات المستأجرين (Tenants)';
|
|
const res = await API.get('/admin/tenants');
|
|
document.getElementById('content').innerHTML = `
|
|
<div class="bb-panel p-8">
|
|
<button onclick="navigateTo('admin')" class="bb-btn mb-6 font-black">< العودة</button>
|
|
<table class="w-full bb-table text-right">
|
|
<thead><tr><th>الاسم</th><th>البريد</th><th>الحالة</th><th>تاريخ التسجيل</th></tr></thead>
|
|
<tbody>
|
|
${res.data.map(t => `
|
|
<tr>
|
|
<td class="font-bold">${t.name}</td>
|
|
<td class="bb-mono text-left" dir="ltr">${t.email}</td>
|
|
<td><span class="status-pill text-bb-green">${t.status}</span></td>
|
|
<td class="bb-mono text-xs text-left" dir="ltr">${t.created_at}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function renderAdminQueue() {
|
|
document.getElementById('page-title').textContent = 'مراقب طابور المهام (Queue)';
|
|
const res = await API.get('/admin/queue');
|
|
document.getElementById('content').innerHTML = `
|
|
<div class="bb-panel p-8">
|
|
<button onclick="navigateTo('admin')" class="bb-btn mb-6 font-black">< العودة</button>
|
|
<table class="w-full bb-table text-right text-xs">
|
|
<thead><tr><th>المعرف</th><th>النوع</th><th>المحاولات</th><th>الحالة</th><th>تاريخ الجدولة</th></tr></thead>
|
|
<tbody>
|
|
${(res.data.failed_jobs || []).map(q => `
|
|
<tr>
|
|
<td class="bb-mono text-left" dir="ltr">${q.id.substring(0,8)}...</td>
|
|
<td class="font-bold text-bb-blue">${q.type}</td>
|
|
<td class="bb-mono text-center">${q.attempts}</td>
|
|
<td><span class="status-pill ${q.status==='pending'?'text-bb-yellow':'text-bb-red'}">${q.status}</span></td>
|
|
<td class="bb-mono text-left" dir="ltr">${q.scheduled_at}</td>
|
|
</tr>
|
|
`).join('')}
|
|
${(!res.data.failed_jobs || res.data.failed_jobs.length === 0) ? `<tr><td colspan="5" class="text-center py-4">لا توجد مهام معلقة أو فاشلة</td></tr>` : ''}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ── Helper Functions ─────────────────────────────────────
|
|
|
|
function getStatusColor(s) {
|
|
const m = { 'approved': 'text-bb-green border-bb-green', 'rejected': 'text-bb-red border-bb-red', 'extracted': 'text-bb-blue border-bb-blue', 'uploaded': 'text-bb-dim border-bb-dim', 'validation_failed': 'text-bb-red border-bb-red' };
|
|
return m[s] || 'text-bb-yellow border-bb-yellow';
|
|
}
|
|
|
|
function translateStatus(s) {
|
|
const m = { 'uploaded': 'مرفوعة (تنتظر الذكاء)', 'extracting': 'قيد الاستخراج', 'extracted': 'تم الاستخراج', 'validated': 'تم التدقيق', 'validation_failed': 'فشل التدقيق', 'submitting': 'جاري الإرسال', 'approved': 'تم الاعتماد', 'rejected': 'مرفوضة' };
|
|
return m[s] || s;
|
|
}
|
|
|
|
function translateRole(r) {
|
|
const m = { 'super_admin': 'مسؤول النظام', 'admin': 'مسؤول الكيان', 'accountant': 'محاسب', 'employee': 'موظف', 'viewer': 'مراقب' };
|
|
return m[r] || r;
|
|
}
|
|
|
|
function translateRisk(r) {
|
|
const m = { 'low': 'منخفض', 'medium': 'متوسط', 'high': 'عالي', 'critical': 'حرج' };
|
|
return m[r] || r;
|
|
}
|
|
|
|
function getRiskColor(r) {
|
|
const m = { 'low': 'text-bb-green', 'medium': 'text-bb-yellow', 'high': 'text-orange-500', 'critical': 'text-bb-red' };
|
|
return m[r] || 'text-white';
|
|
}
|
|
|
|
async function loadApiKeys() {
|
|
try {
|
|
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 text-left" dir="ltr">PUB: ${k.public_key}</div>
|
|
</div>
|
|
<button onclick="revokeApiKey('${k.id}')" class="text-bb-red text-[9px] bb-mono hover:underline font-bold">إلغاء وإيقاف</button>
|
|
</div>
|
|
`).join('') || '<div class="text-bb-dim text-xs bb-mono">لا توجد مفاتيح مسجلة</div>';
|
|
} catch(e) {}
|
|
}
|
|
|
|
// ── Modals Functions ─────────────────────────────────────
|
|
|
|
async function showAddCompanyModal() {
|
|
showModal(`
|
|
<h2 class="text-xl font-black mb-8 border-b border-bb-border pb-4 uppercase">تسجيل شركة / كيان جديد</h2>
|
|
<form id="add-company-form" class="space-y-4">
|
|
<input type="text" name="name" placeholder="اسم الشركة" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
|
|
<input type="text" name="tax_identification_number" placeholder="الرقم الضريبي (TIN)" class="w-full bb-panel bg-black p-3 bb-mono text-sm text-left" dir="ltr" required>
|
|
<input type="text" name="city" placeholder="المدينة (مثال: عمان)" class="w-full bb-panel bg-black p-3 bb-mono text-sm">
|
|
<div class="flex gap-4 pt-4">
|
|
<button type="submit" class="flex-1 bb-btn bb-btn-primary font-black py-4">حفظ وإضافة</button>
|
|
<button type="button" onclick="closeModal()" class="bb-btn px-10 font-black">إلغاء</button>
|
|
</div>
|
|
</form>
|
|
`);
|
|
document.getElementById('add-company-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(e.target);
|
|
try {
|
|
await API.post('/companies', Object.fromEntries(fd));
|
|
showToast('تمت إضافة الشركة بنجاح');
|
|
closeModal();
|
|
navigateTo('companies');
|
|
} catch(err) {}
|
|
};
|
|
}
|
|
|
|
async function showJoFotaraModal(companyId) {
|
|
showModal(`
|
|
<h2 class="text-xl font-black mb-8 border-b border-bb-border pb-4 uppercase">إعدادات الربط الحكومي (جوفوترة)</h2>
|
|
<form id="jofotara-form" class="space-y-4">
|
|
<input type="text" name="client_id" placeholder="Client ID (رقم العميل)" class="w-full bb-panel bg-black p-3 bb-mono text-sm text-left" dir="ltr" required>
|
|
<input type="password" name="secret_key" placeholder="Secret Key (المفتاح السري)" class="w-full bb-panel bg-black p-3 bb-mono text-sm text-left" dir="ltr" required>
|
|
<div class="flex gap-4 pt-4">
|
|
<button type="submit" class="flex-1 bb-btn bb-btn-primary font-black py-4">حفظ بيانات الربط</button>
|
|
<button type="button" onclick="closeModal()" class="bb-btn px-10 font-black">إلغاء</button>
|
|
</div>
|
|
</form>
|
|
`);
|
|
document.getElementById('jofotara-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(e.target);
|
|
try {
|
|
await API.post(`/companies/${companyId}/jofotara`, Object.fromEntries(fd));
|
|
showToast('تم حفظ بيانات الربط بنجاح');
|
|
closeModal();
|
|
navigateTo('companies');
|
|
} catch(err) {}
|
|
};
|
|
}
|
|
|
|
async function showAddUserModal() {
|
|
try {
|
|
const res = await API.get('/companies');
|
|
const comps = res.data;
|
|
showModal(`
|
|
<h2 class="text-xl font-black mb-8 border-b border-bb-border pb-4 uppercase">إضافة مستخدم للوحة</h2>
|
|
<form id="add-user-form" class="space-y-4">
|
|
<input type="text" name="name" placeholder="الاسم الكامل" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
|
|
<input type="email" name="email" placeholder="البريد الإلكتروني" class="w-full bb-panel bg-black p-3 bb-mono text-sm text-left" dir="ltr" required>
|
|
<input type="password" name="password" placeholder="كلمة المرور الافتراضية" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
|
|
<select name="role" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
|
|
<option value="accountant">محاسب (Accountant) - يقدر يرفع فواتير</option>
|
|
<option value="viewer">مراقب (Viewer) - فقط مشاهدة</option>
|
|
<option value="admin">مسؤول نظام (Admin) - كافة الصلاحيات</option>
|
|
</select>
|
|
<select name="assigned_company_id" class="w-full bb-panel bg-black p-3 bb-mono text-sm">
|
|
<option value="">كافة الشركات التابعة</option>
|
|
${comps.map(c => `<option value="${c.id}">${c.name}</option>`).join('')}
|
|
</select>
|
|
<div class="flex gap-4 pt-4">
|
|
<button type="submit" class="flex-1 bb-btn bb-btn-primary font-black py-4">إضافة المستخدم</button>
|
|
<button type="button" onclick="closeModal()" class="bb-btn px-10 font-black">إلغاء</button>
|
|
</div>
|
|
</form>
|
|
`);
|
|
document.getElementById('add-user-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(e.target);
|
|
try {
|
|
await API.post('/users', Object.fromEntries(fd));
|
|
showToast('تمت إضافة المستخدم بنجاح');
|
|
closeModal();
|
|
navigateTo('users');
|
|
} catch(err) {}
|
|
};
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function deleteUser(id) {
|
|
if(!confirm('تحذير: هل أنت متأكد من إلغاء صلاحية هذا المستخدم وإزالته نهائياً؟')) return;
|
|
try {
|
|
await API.delete(`/users/${id}`);
|
|
showToast('تمت الإزالة بنجاح');
|
|
navigateTo('users');
|
|
} catch(err) {}
|
|
}
|
|
|
|
async function createApiKey() {
|
|
showModal(`
|
|
<h2 class="text-xl font-black mb-8 border-b border-bb-border pb-4 uppercase">توليد مفتاح ربط جديد</h2>
|
|
<form id="api-key-form" class="space-y-4">
|
|
<input type="text" name="name" placeholder="اسم النظام / التطبيق المستفيد" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
|
|
<div class="flex gap-4 pt-4">
|
|
<button type="submit" class="flex-1 bb-btn bb-btn-primary font-black py-4">إنشاء</button>
|
|
<button type="button" onclick="closeModal()" class="bb-btn px-10 font-black">إلغاء</button>
|
|
</div>
|
|
</form>
|
|
`);
|
|
document.getElementById('api-key-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(e.target);
|
|
try {
|
|
const res = await API.post('/api-keys', Object.fromEntries(fd));
|
|
showModal(`
|
|
<h2 class="text-xl font-black mb-8 border-b border-bb-border pb-4 uppercase text-bb-green">تم التوليد بنجاح</h2>
|
|
<div class="space-y-4 bb-mono text-sm">
|
|
<div class="p-4 bb-panel bg-white/5 border-bb-dim text-left" dir="ltr">
|
|
<div class="text-bb-dim text-[10px] mb-1">Public Key</div>
|
|
<div class="break-all font-bold text-white">${res.data.public_key}</div>
|
|
</div>
|
|
<div class="p-4 bb-panel bg-red-900/20 border-bb-red text-left" dir="ltr">
|
|
<div class="text-bb-red text-[10px] mb-1 text-right" dir="rtl">Secret Key (تنبيه: انسخه الآن، لن يعرض مجدداً)</div>
|
|
<div class="break-all font-bold text-bb-red select-all py-2">${res.data.secret}</div>
|
|
</div>
|
|
<button onclick="closeModal(); loadApiKeys()" class="w-full bb-btn bb-btn-primary py-4 mt-4 font-black">لقد قمت بنسخ المفتاح، إغلاق</button>
|
|
</div>
|
|
`);
|
|
} catch(err) {}
|
|
};
|
|
}
|
|
|
|
async function revokeApiKey(id) {
|
|
if(!confirm('سيتم تعطيل الوصول عبر هذا المفتاح فوراً. هل أنت متأكد؟')) return;
|
|
try {
|
|
await API.delete(`/api-keys/${id}`);
|
|
showToast('تم إلغاء المفتاح');
|
|
loadApiKeys();
|
|
} catch(err) {}
|
|
}
|
|
|
|
async function start2FA() {
|
|
try {
|
|
const res = await API.post('/auth/2fa/enable', {});
|
|
showModal(`
|
|
<h2 class="text-xl font-black mb-8 border-b border-bb-border pb-4 uppercase">تفعيل التحقق الثنائي (2FA)</h2>
|
|
<div class="flex flex-col items-center space-y-6">
|
|
<img src="${res.data.qr_url}" alt="QR Code" class="w-48 h-48 bg-white p-2">
|
|
<div class="text-[10px] text-bb-dim bb-mono text-center">امسح الكود عبر تطبيق Google Authenticator<br>أو أدخل الرمز السري يدوياً:<br><span class="text-white select-all">${res.data.secret}</span></div>
|
|
|
|
<form id="verify-2fa-form" class="w-full space-y-4">
|
|
<input type="hidden" name="secret" value="${res.data.secret}">
|
|
<input type="text" name="code" placeholder="أدخل الرمز المكون من 6 أرقام" class="w-full bb-panel bg-black p-3 bb-mono text-center text-xl tracking-[1em]" dir="ltr" required>
|
|
<button type="submit" class="w-full bb-btn bb-btn-primary font-black py-4">تأكيد التفعيل</button>
|
|
</form>
|
|
</div>
|
|
`);
|
|
|
|
document.getElementById('verify-2fa-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
await API.post('/auth/2fa/verify', Object.fromEntries(new FormData(e.target)));
|
|
showToast('تم تفعيل التحقق الثنائي بنجاح');
|
|
closeModal();
|
|
navigateTo('settings');
|
|
} catch(err) {}
|
|
};
|
|
} catch(err) {}
|
|
}
|
|
|
|
async function disable2FA() {
|
|
if(!confirm('تحذير: هل أنت متأكد من رغبتك بإزالة طبقة الحماية الثنائية عن حسابك؟')) return;
|
|
try {
|
|
await API.post('/auth/2fa/disable', {});
|
|
showToast('تم تعطيل حماية 2FA');
|
|
navigateTo('settings');
|
|
} catch(err) {}
|
|
}
|
|
|
|
async function showUploadModal() {
|
|
try {
|
|
const res = await API.get('/companies');
|
|
const comps = res.data;
|
|
showModal(`
|
|
<h2 class="text-xl font-black mb-8 border-b border-bb-border pb-4 uppercase tracking-tighter">رفع فاتورة جديدة للتدقيق</h2>
|
|
<form id="upload-form" class="space-y-6">
|
|
<div class="space-y-2">
|
|
<label class="text-[10px] bb-mono text-bb-dim">الكيان التابع (الشركة المرفوع عنها)</label>
|
|
<select name="company_id" class="w-full bb-panel bg-black p-3 bb-mono text-sm border-bb-dim" required>
|
|
<option value="">-- اختر الشركة --</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">مستند الفاتورة (PDF / صور)</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 w-full cursor-pointer" 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">بدء المعالجة بالذكاء الاصطناعي</button>
|
|
<button type="button" onclick="closeModal()" class="bb-btn px-10 font-black">إلغاء</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 = 'جاري الرفع والجدولة...';
|
|
await API.upload('/invoices/upload', fd);
|
|
showToast('تم رفع الفاتورة، سيقوم النظام بالاستخراج فوراً');
|
|
closeModal();
|
|
navigateTo('invoices');
|
|
} catch(err) {
|
|
const btn = e.target.querySelector('button[type="submit"]');
|
|
btn.disabled = false; btn.textContent = 'بدء المعالجة بالذكاء الاصطناعي';
|
|
}
|
|
};
|
|
} catch (e) {
|
|
showToast('فشل جلب الشركات لرفع الفاتورة', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Auth Logic ───────────────────────────────────────────
|
|
|
|
async function initApp() {
|
|
if (!API.token) {
|
|
renderLogin();
|
|
return;
|
|
}
|
|
try {
|
|
const u = (await API.get('/auth/me')).data;
|
|
document.getElementById('user-display').textContent = `المنفذ: ${u.name} | الصلاحية: ${translateRole(u.role)}`;
|
|
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">مُصادَق للمؤسسات</h1>
|
|
<p class="text-bb-dim text-[10px] bb-mono uppercase">بوابة الدخول المشفرة (TERMINAL)</p>
|
|
</div>
|
|
<form id="login-form" class="space-y-6">
|
|
<input type="email" name="email" placeholder="البريد الإلكتروني (IDENTITY)" class="w-full bb-panel bg-black p-3 bb-mono text-sm text-left" dir="ltr" required>
|
|
<input type="password" name="password" placeholder="كلمة المرور (PASSWORD)" class="w-full bb-panel bg-black p-3 bb-mono text-sm text-left" dir="ltr" required>
|
|
<button type="submit" class="w-full bb-btn bb-btn-primary py-4 font-black text-lg tracking-widest">تأسيس الاتصال (LOGIN)</button>
|
|
</form>
|
|
<div class="mt-8 text-center text-bb-dim text-[10px] bb-mono">
|
|
لا تملك صلاحية وصول؟ <a href="#" onclick="renderRegister()" class="text-white hover:underline font-bold">تسجيل كيان جديد</a>
|
|
</div>
|
|
`;
|
|
document.getElementById('login-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 = 'جاري التحقق...';
|
|
const res = await API.post('/auth/login', Object.fromEntries(fd));
|
|
if (res.requires_2fa) render2FAChallenge(res.temp_token);
|
|
else saveAuth(res.data);
|
|
} catch(err) {
|
|
const btn = e.target.querySelector('button[type="submit"]');
|
|
btn.disabled = false; btn.textContent = 'تأسيس الاتصال (LOGIN)';
|
|
}
|
|
};
|
|
}
|
|
|
|
function render2FAChallenge(tempToken) {
|
|
document.getElementById('auth-content').innerHTML = `
|
|
<div class="text-center mb-10">
|
|
<h1 class="text-3xl font-black tracking-tighter mb-2 text-bb-yellow">مطلوب خطوة إضافية</h1>
|
|
<p class="text-bb-dim text-[10px] bb-mono uppercase">المصادقة الثنائية (2FA Challenge)</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-3xl tracking-[0.5em] text-bb-yellow font-bold" dir="ltr" required autofocus>
|
|
<button type="submit" class="w-full bb-btn bb-btn-primary py-4 font-black">تحقق ودخول</button>
|
|
</form>
|
|
`;
|
|
document.getElementById('2fa-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const code = new FormData(e.target).get('code');
|
|
try {
|
|
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 || 'رمز غير صحيح', 'error');
|
|
} catch(err) { showToast('حدث خطأ في الاتصال', 'error'); }
|
|
};
|
|
}
|
|
|
|
function renderRegister() {
|
|
document.getElementById('auth-content').innerHTML = `
|
|
<div class="text-center mb-10">
|
|
<h1 class="text-3xl font-black tracking-tighter mb-2">تأسيس مساحة عمل</h1>
|
|
<p class="text-bb-dim text-[10px] bb-mono uppercase">مستأجر جديد (New Tenant)</p>
|
|
</div>
|
|
<form id="register-form" class="space-y-4">
|
|
<input type="text" name="name" placeholder="اسمك الكامل" class="w-full bb-panel bg-black p-3 bb-mono text-sm" required>
|
|
<input type="email" name="email" placeholder="البريد الإلكتروني" class="w-full bb-panel bg-black p-3 bb-mono text-sm text-left" dir="ltr" required>
|
|
<input type="password" name="password" placeholder="كلمة المرور القوية" class="w-full bb-panel bg-black p-3 bb-mono text-sm text-left" dir="ltr" required>
|
|
<button type="submit" class="w-full bb-btn bb-btn-primary py-4 font-black tracking-widest mt-4">إنشاء وبدء الاستخدام</button>
|
|
</form>
|
|
<div class="mt-8 text-center text-bb-dim text-[10px] bb-mono">
|
|
لديك مساحة عمل مسبقاً؟ <a href="#" onclick="renderLogin()" class="text-white hover:underline font-bold">العودة لتسجيل الدخول</a>
|
|
</div>
|
|
`;
|
|
document.getElementById('register-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 = 'جاري التأسيس...';
|
|
const res = await API.post('/auth/register', Object.fromEntries(fd));
|
|
saveAuth(res.data);
|
|
} catch(err) {
|
|
const btn = e.target.querySelector('button[type="submit"]');
|
|
btn.disabled = false; btn.textContent = 'إنشاء وبدء الاستخدام';
|
|
}
|
|
};
|
|
}
|
|
|
|
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>
|