583 lines
39 KiB
PHP
583 lines
39 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="ar">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>مُصادَق — منصة أتمتة الفواتير الإلكترونية</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;900&family=Noto+Kufi+Arabic:wght@400;700;900&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--primary: #10b981;
|
|
--primary-glow: rgba(16, 185, 129, 0.3);
|
|
--bg: #030712;
|
|
--panel: rgba(17, 24, 39, 0.8);
|
|
}
|
|
body {
|
|
background: var(--bg);
|
|
color: #f1f5f9;
|
|
font-family: 'Noto Kufi Arabic', sans-serif;
|
|
min-height: 100vh;
|
|
}
|
|
.glass { background: var(--panel); backdrop-filter: blur(20px); border: 1px solid rgba(255,255,255,0.05); }
|
|
.nav-link { @apply flex items-center gap-3 px-6 py-4 rounded-2xl transition-all text-slate-400 hover:text-white hover:bg-white/5; }
|
|
.nav-link.active { @apply bg-primary/10 text-primary border border-primary/20 shadow-[0_0_30px_var(--primary-glow)]; }
|
|
.btn-primary { @apply px-6 py-3 bg-primary hover:bg-emerald-600 text-white rounded-2xl font-bold transition-all shadow-xl shadow-primary/20 flex items-center justify-center gap-2; }
|
|
.stat-card { @apply glass p-6 rounded-[2rem] hover:border-primary/50 transition-all duration-500; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
.animate-in { animation: fadeIn 0.4s ease-out forwards; }
|
|
input, select { @apply bg-white/5 border border-white/10 rounded-2xl px-4 py-3 text-white focus:border-primary outline-none transition-all; }
|
|
</style>
|
|
</head>
|
|
<body dir="rtl" class="overflow-x-hidden">
|
|
|
|
<!-- Auth Wrapper -->
|
|
<div id="auth-container" class="fixed inset-0 z-[100] bg-bg flex items-center justify-center p-6 hidden">
|
|
<!-- Login/Register will be rendered here -->
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<aside id="sidebar" class="fixed right-0 top-0 h-screen w-80 glass border-l border-white/5 flex flex-col p-8 z-50 translate-x-full transition-transform duration-500">
|
|
<div class="flex items-center gap-4 mb-12">
|
|
<div class="w-12 h-12 bg-primary rounded-2xl flex items-center justify-center shadow-2xl shadow-primary/30">
|
|
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04m12.868 5.31c.477.81 1.437 1.29 2.43 1.014a11.903 11.903 0 01-1.565 3.523 11.91 11.91 0 01-3.01 3.01c-1.333.77-2.962.77-4.295 0a11.91 11.91 0 01-3.01-3.01 11.903 11.903 0 01-1.565-3.523c.993.276 1.953-.204 2.43-1.014a11.91 11.91 0 013.01-3.01 11.955 11.955 0 014.295 0 11.91 11.91 0 013.01 3.01z"></path></svg>
|
|
</div>
|
|
<h1 class="text-2xl font-black text-white">مُصادَق</h1>
|
|
</div>
|
|
|
|
<nav class="flex-1 space-y-2 overflow-y-auto pr-2 custom-scrollbar">
|
|
<a href="#" onclick="navigateTo('dashboard')" id="nav-dashboard" class="nav-link active">لوحة التحكم</a>
|
|
<a href="#" onclick="navigateTo('companies')" id="nav-companies" class="nav-link">الشركات</a>
|
|
<a href="#" onclick="navigateTo('invoices')" id="nav-invoices" class="nav-link">الفواتير</a>
|
|
<a href="#" onclick="showAddInvoiceModal()" id="nav-upload-invoice" class="nav-link">رفع فاتورة</a>
|
|
<a href="#" onclick="navigateTo('users')" id="nav-users" class="nav-link">الموظفين والمستخدمين</a>
|
|
<a href="#" onclick="navigateTo('risk-monitor')" id="nav-risk-monitor" class="nav-link">مراقبة المخاطر</a>
|
|
<a href="#" onclick="navigateTo('admin')" id="nav-admin" class="nav-link hidden text-secondary">الإدارة</a>
|
|
</nav>
|
|
|
|
<div class="pt-6 border-t border-white/5 space-y-2 mt-auto">
|
|
<a href="#" onclick="navigateTo('settings')" id="nav-settings" class="nav-link">الإعدادات</a>
|
|
<button onclick="logout()" class="w-full nav-link text-red-400 hover:bg-red-500/10 text-right">تسجيل الخروج</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Content -->
|
|
<main id="main-content" class="md:mr-80 transition-all duration-500 min-h-screen opacity-0">
|
|
<header class="h-24 glass border-b border-white/5 flex items-center justify-between px-12 sticky top-0 z-40">
|
|
<h2 id="page-title" class="text-2xl font-bold">لوحة التحكم</h2>
|
|
<div class="flex items-center gap-6">
|
|
<div class="relative">
|
|
<input type="text" id="ai-chat" class="w-80 pr-12 text-sm" placeholder="اسأل الذكاء الاصطناعي...">
|
|
<svg class="w-5 h-5 absolute right-4 top-3.5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
|
</div>
|
|
<button onclick="showAddInvoiceModal()" class="btn-primary">+ رفع فاتورة</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div id="content" class="p-12 max-w-[1600px] mx-auto animate-in"></div>
|
|
</main>
|
|
|
|
<!-- Toast & Modals -->
|
|
<div id="toast-container" class="fixed top-8 left-1/2 -translate-x-1/2 z-[200] space-y-4"></div>
|
|
<div id="modals" class="fixed inset-0 z-[150] hidden items-center justify-center p-6 bg-black/80 backdrop-blur-md"></div>
|
|
|
|
<!-- Global Loader -->
|
|
<div id="global-loader" class="fixed inset-0 z-[250] hidden items-center justify-center bg-black/40 backdrop-blur-sm">
|
|
<div class="flex flex-col items-center gap-4">
|
|
<div class="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin shadow-[0_0_30px_var(--primary-glow)]"></div>
|
|
<p class="text-white font-bold animate-pulse">جاري المعالجة...</p>
|
|
</div>
|
|
</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 loader = document.getElementById('global-loader');
|
|
if (loader) loader.classList.replace('hidden', 'flex');
|
|
|
|
try {
|
|
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); }
|
|
|
|
const res = await fetch(`${this.baseUrl}${path}`, { method, headers, body });
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
if (res.status === 401) logout();
|
|
throw data;
|
|
}
|
|
return data;
|
|
} catch (err) {
|
|
showToast(err.error?.message || 'حدث خطأ غير متوقع في النظام', 'error');
|
|
throw err;
|
|
} finally {
|
|
if (loader) loader.classList.replace('flex', 'hidden');
|
|
}
|
|
},
|
|
get(p) { return this.req('GET', p); },
|
|
post(p, b) { return this.req('POST', p, b); },
|
|
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');
|
|
t.className = `px-8 py-4 rounded-2xl text-white font-bold shadow-2xl transition-all duration-500 translate-y-10 opacity-0 ${type === 'success' ? 'bg-emerald-500' : 'bg-red-500'}`;
|
|
t.textContent = msg;
|
|
container.appendChild(t);
|
|
setTimeout(() => t.classList.remove('translate-y-10', 'opacity-0'), 10);
|
|
setTimeout(() => { t.classList.add('-translate-y-10', 'opacity-0'); setTimeout(() => t.remove(), 500); }, 4000);
|
|
}
|
|
|
|
function logout() { localStorage.clear(); window.location.reload(); }
|
|
|
|
async function navigateTo(page) {
|
|
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
|
|
document.getElementById(`nav-${page}`)?.classList.add('active');
|
|
const content = document.getElementById('content');
|
|
content.innerHTML = '<div class="flex justify-center p-20"><div class="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin"></div></div>';
|
|
|
|
try {
|
|
if (page === 'dashboard') await renderDashboard();
|
|
else if (page === 'companies') await renderCompanies();
|
|
else if (page === 'invoices') await renderInvoices();
|
|
else if (page === 'users') await renderUsers();
|
|
else if (page === 'risk-monitor') await renderRiskMonitor();
|
|
else if (page === 'settings') await renderSettings();
|
|
else if (page === 'admin') await renderAdmin();
|
|
} catch (e) { showToast('خطأ في تحميل الصفحة', 'error'); }
|
|
}
|
|
|
|
// ── 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-1 md:grid-cols-4 gap-8 mb-12">
|
|
<div class="stat-card">
|
|
<p class="text-slate-500 text-sm mb-1">فواتير الشهر</p>
|
|
<p class="text-5xl font-black text-white">${s.total_this_month}</p>
|
|
</div>
|
|
<div class="stat-card">
|
|
<p class="text-slate-500 text-sm mb-1">استهلاك الباقة</p>
|
|
<p class="text-5xl font-black text-primary">${s.subscription_usage}%</p>
|
|
</div>
|
|
<div class="stat-card">
|
|
<p class="text-slate-500 text-sm mb-1">مؤشر المخاطر</p>
|
|
<p class="text-5xl font-black text-yellow-500">منخفض</p>
|
|
</div>
|
|
<div class="stat-card">
|
|
<p class="text-slate-500 text-sm mb-1">حالة الربط</p>
|
|
<p class="text-5xl font-black text-emerald-500">نشط</p>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<div class="lg:col-span-2 glass p-10 rounded-[3rem]">
|
|
<h3 class="text-xl font-bold mb-8">أحدث الفواتير</h3>
|
|
<div class="space-y-4">
|
|
${s.recent_invoices.map(i => `
|
|
<div onclick="renderInvoiceDetail('${i.id}')" class="flex justify-between items-center p-5 bg-white/5 rounded-3xl border border-transparent hover:border-primary/30 transition cursor-pointer">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center text-primary font-bold">INV</div>
|
|
<div>
|
|
<p class="font-bold">${i.invoice_number || '---'}</p>
|
|
<p class="text-xs text-slate-500">${i.company_name}</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-left">
|
|
<p class="font-bold">${i.grand_total} JOD</p>
|
|
<p class="text-[10px] font-bold uppercase ${i.status==='approved'?'text-primary':'text-yellow-500'}">${i.status}</p>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="glass p-10 rounded-[3rem] flex flex-col items-center">
|
|
<h3 class="text-xl font-bold mb-8 self-start">توزيع الحالات</h3>
|
|
<div class="w-full max-w-[280px] aspect-square"><canvas id="dashChart"></canvas></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
new Chart(document.getElementById('dashChart'), {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: s.status_distribution.map(x=>x.status),
|
|
datasets: [{ data: s.status_distribution.map(x=>x.count), backgroundColor: ['#10b981', '#fbbf24', '#f87171', '#6366f1'], borderWidth: 0 }]
|
|
},
|
|
options: { cutout: '80%', plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8', font: { size: 10 } } } } }
|
|
});
|
|
}
|
|
|
|
async function renderCompanies() {
|
|
document.getElementById('page-title').textContent = 'إدارة الشركات';
|
|
const res = await API.get('/companies');
|
|
document.getElementById('content').innerHTML = `
|
|
<div class="flex justify-end mb-8"><button onclick="showAddCompanyModal()" class="btn-primary">+ إضافة شركة</button></div>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
${res.data.map(c => `
|
|
<div class="glass p-8 rounded-[2.5rem] border-t-8 border-t-primary">
|
|
<h3 class="text-2xl font-bold mb-1">${c.name}</h3>
|
|
<p class="text-sm text-slate-500 mb-6">الرقم الضريبي: ${c.tax_identification_number}</p>
|
|
<div class="p-4 bg-black/20 rounded-2xl border border-white/5 flex justify-between items-center mb-6">
|
|
<span class="text-xs text-slate-400">JoFotara</span>
|
|
<span class="text-xs font-bold ${c.is_jofotara_linked?'text-primary':'text-red-400'}">${c.is_jofotara_linked?'مربوط ✓':'غير مربوط'}</span>
|
|
</div>
|
|
<button onclick="showJoFotaraModal('${c.id}')" class="w-full py-3 bg-white/5 hover:bg-white/10 rounded-2xl text-sm transition">إعدادات الربط</button>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function renderInvoices() {
|
|
document.getElementById('page-title').textContent = 'الفواتير والتدقيق';
|
|
const res = await API.get('/invoices');
|
|
document.getElementById('content').innerHTML = `
|
|
<div class="glass rounded-[3rem] overflow-hidden">
|
|
<table class="w-full text-right">
|
|
<thead class="bg-white/5 text-slate-400 text-sm">
|
|
<tr><th class="p-6">الشركة</th><th class="p-6">الرقم</th><th class="p-6">التاريخ</th><th class="p-6">المجموع</th><th class="p-6 text-center">الحالة</th></tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-white/5">
|
|
${res.data.map(i => `
|
|
<tr onclick="renderInvoiceDetail('${i.id}')" class="hover:bg-white/5 cursor-pointer transition">
|
|
<td class="p-6 font-bold">${i.company_name}</td>
|
|
<td class="p-6 font-mono text-sm">${i.invoice_number || '---'}</td>
|
|
<td class="p-6 text-slate-400 text-sm">${i.invoice_date || '---'}</td>
|
|
<td class="p-6 font-bold text-white">${i.grand_total} JOD</td>
|
|
<td class="p-6 text-center"><span class="px-4 py-1.5 rounded-full text-[10px] font-bold border ${i.status==='approved'?'border-primary text-primary':'border-yellow-500 text-yellow-500'} bg-white/5 uppercase">${i.status}</span></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="flex flex-col lg:flex-row gap-10 animate-in">
|
|
<div class="lg:w-1/2 glass rounded-[3rem] h-[750px] overflow-hidden flex flex-col">
|
|
<div class="p-5 bg-white/5 border-b border-white/5 flex justify-between text-sm"><span>المستند الأصلي</span><a href="index.php?route=/api/v1/invoices/${i.id}/file" target="_blank" class="text-primary">فتح في نافذة جديدة</a></div>
|
|
<div class="flex-1 bg-black/50 p-6 flex items-center justify-center">
|
|
${i.original_file_path.endsWith('.pdf') ? `<iframe src="index.php?route=/api/v1/invoices/${i.id}/file" class="w-full h-full rounded-2xl"></iframe>` : `<img src="index.php?route=/api/v1/invoices/${i.id}/file" class="max-w-full max-h-full rounded-2xl shadow-2xl">`}
|
|
</div>
|
|
</div>
|
|
<div class="lg:w-1/2 glass p-10 rounded-[3rem] overflow-y-auto custom-scrollbar">
|
|
<div class="flex justify-between items-start mb-10">
|
|
<div><h3 class="text-3xl font-black mb-2">${i.supplier_name || 'غير معروف'}</h3><p class="text-slate-400">رقم الفاتورة: <span class="text-white font-mono">${i.invoice_number || '---'}</span></p></div>
|
|
<button onclick="submitToJoFotara('${i.id}')" class="btn-primary">إرسال لـ JoFotara</button>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-6 mb-10">
|
|
<div class="p-6 bg-white/5 rounded-3xl border border-white/5"><p class="text-xs text-slate-500 mb-1">تاريخ الإصدار</p><p class="font-bold">${i.invoice_date || '---'}</p></div>
|
|
<div class="p-6 bg-white/5 rounded-3xl border border-white/5"><p class="text-xs text-slate-500 mb-1">الرقم الضريبي</p><p class="font-bold text-primary font-mono">${i.supplier_tin || '---'}</p></div>
|
|
</div>
|
|
<table class="w-full text-sm mb-10">
|
|
<thead class="text-slate-500 border-b border-white/10 text-right"><tr class="text-xs"><th class="pb-4">الوصف</th><th class="pb-4 text-center">الكمية</th><th class="pb-4 text-left">المجموع</th></tr></thead>
|
|
<tbody class="divide-y divide-white/5">${i.lines.map(l => `<tr><td class="py-4 text-slate-300">${l.description}</td><td class="py-4 text-center">${l.quantity}</td><td class="py-4 text-left font-bold text-primary">${l.line_total} JOD</td></tr>`).join('')}</tbody>
|
|
</table>
|
|
<div class="pt-6 border-t border-white/10 space-y-3">
|
|
<div class="flex justify-between"><span>المجموع الفرعي</span><span>${i.subtotal} JOD</span></div>
|
|
<div class="flex justify-between text-yellow-500"><span>الضريبة</span><span>${i.tax_amount} JOD</span></div>
|
|
<div class="flex justify-between text-3xl font-black pt-4 text-white"><span>الإجمالي الكلي</span><span class="text-primary">${i.grand_total} JOD</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function renderRiskMonitor() {
|
|
document.getElementById('page-title').textContent = 'مراقبة المخاطر';
|
|
document.getElementById('content').innerHTML = `
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
|
|
<div class="glass p-10 rounded-[3rem]">
|
|
<h3 class="text-xl font-bold mb-8">تحليل الالتزام</h3>
|
|
<div class="space-y-6">
|
|
<div class="p-8 bg-emerald-500/5 border border-primary/20 rounded-3xl text-center">
|
|
<p class="text-slate-400 text-sm mb-2">مستوى الخطورة</p>
|
|
<p class="text-4xl font-black text-emerald-500 tracking-widest">منخفض جداً</p>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="p-6 bg-white/5 rounded-3xl border border-white/5"><p class="text-xs text-slate-500">دقة الذكاء الاصطناعي</p><p class="text-2xl font-bold">99.8%</p></div>
|
|
<div class="p-6 bg-white/5 rounded-3xl border border-white/5"><p class="text-xs text-slate-500">فواتير مرفوضة</p><p class="text-2xl font-bold text-red-400">0</p></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass p-10 rounded-[3rem]">
|
|
<h3 class="text-xl font-bold mb-8">قواعد التدقيق الفعالة</h3>
|
|
<div class="space-y-4">
|
|
${['تطابق الرقم الضريبي', 'صحة احتساب الضريبة (16%)', 'تسلسل أرقام الفواتير', 'الحد الزمني للرفع (3 أيام)'].map(r => `
|
|
<div class="flex items-center gap-4 p-5 bg-white/5 rounded-3xl border border-white/5">
|
|
<div class="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary">✓</div>
|
|
<span class="font-bold">${r}</span>
|
|
<span class="mr-auto text-[10px] text-primary font-black uppercase">Active</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function renderSettings() {
|
|
document.getElementById('page-title').textContent = 'الإعدادات والأمان';
|
|
const res = await API.get('/auth/me');
|
|
const u = res.data;
|
|
document.getElementById('content').innerHTML = `
|
|
<div class="max-w-4xl mx-auto space-y-10">
|
|
<div class="glass p-10 rounded-[3rem] border-t-8 border-t-primary">
|
|
<div class="flex justify-between items-center mb-8">
|
|
<div><h3 class="text-2xl font-black mb-1">التحقق بخطوتين (2FA)</h3><p class="text-slate-400">تأمين إضافي لحسابك باستخدام Authenticator.</p></div>
|
|
<div class="px-4 py-2 bg-white/5 rounded-2xl text-xs font-bold border border-white/10 ${u.totp_enabled?'text-primary':'text-slate-500'}">${u.totp_enabled?'مُفعّل':'غير مُفعّل'}</div>
|
|
</div>
|
|
<div id="2fa-area">
|
|
${u.totp_enabled ? `
|
|
<button onclick="disable2FA()" class="px-8 py-3 bg-red-500/10 text-red-400 hover:bg-red-500/20 rounded-2xl transition font-bold border border-red-500/20">تعطيل الحماية</button>
|
|
` : `
|
|
<button onclick="start2FA()" class="btn-primary">تفعيل الآن</button>
|
|
`}
|
|
</div>
|
|
</div>
|
|
<div class="glass p-10 rounded-[3rem] border-t-8 border-t-indigo-500">
|
|
<div class="flex justify-between items-center mb-8">
|
|
<div><h3 class="text-2xl font-black mb-1">مفاتيح API</h3><p class="text-slate-400">للربط مع تطبيقات الموبايل والأنظمة الخارجية.</p></div>
|
|
<button onclick="createApiKey()" class="btn-primary bg-indigo-600 hover:bg-indigo-700 shadow-indigo-500/20">إنشاء مفتاح جديد</button>
|
|
</div>
|
|
<div id="api-keys-list" class="space-y-4"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
loadApiKeys();
|
|
}
|
|
|
|
// ── Auth & Init ──────────────────────────────────────────
|
|
async function renderLogin() {
|
|
const auth = document.getElementById('auth-container');
|
|
auth.classList.remove('hidden');
|
|
auth.innerHTML = `
|
|
<div class="glass p-12 rounded-[3rem] w-full max-w-md shadow-2xl border-t-8 border-t-primary animate-in">
|
|
<div class="text-center mb-10">
|
|
<h2 class="text-4xl font-black mb-3">مرحباً بك</h2>
|
|
<p class="text-slate-500">سجل الدخول لمنصة مُصادَق</p>
|
|
</div>
|
|
<form id="login-form" class="space-y-6">
|
|
<input type="email" id="email" class="w-full" placeholder="البريد الإلكتروني" required>
|
|
<input type="password" id="password" class="w-full" placeholder="كلمة المرور" required>
|
|
<button type="submit" class="w-full btn-primary py-4 text-lg">دخول</button>
|
|
</form>
|
|
<div class="mt-8 text-center"><p class="text-sm text-slate-500">ليس لديك حساب؟ <a href="#" onclick="renderRegister()" class="text-primary font-bold">سجل الآن</a></p></div>
|
|
</div>
|
|
`;
|
|
document.getElementById('login-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
const res = await API.post('/auth/login', { email: e.target.email.value, password: e.target.password.value });
|
|
if (res.requires_2fa) {
|
|
render2FAChallenge(res.temp_token);
|
|
} else {
|
|
saveAuth(res.data);
|
|
}
|
|
} catch (err) { showToast(err.error?.message_ar || 'بيانات الدخول غير صحيحة', 'error'); }
|
|
};
|
|
}
|
|
|
|
function saveAuth(data) {
|
|
localStorage.setItem('access_token', data.access_token);
|
|
localStorage.setItem('user_role', data.user.role);
|
|
localStorage.setItem('user_name', data.user.name);
|
|
window.location.reload();
|
|
}
|
|
|
|
function initApp() {
|
|
const role = localStorage.getItem('user_role');
|
|
if (localStorage.getItem('access_token')) {
|
|
document.getElementById('sidebar').classList.remove('translate-x-full');
|
|
document.getElementById('main-content').classList.replace('opacity-0', 'opacity-100');
|
|
|
|
// RBAC UI Logic
|
|
if (role !== 'super_admin' && role !== 'admin') {
|
|
document.getElementById('nav-companies')?.classList.add('hidden');
|
|
document.getElementById('nav-users')?.classList.add('hidden');
|
|
document.getElementById('nav-risk-monitor')?.classList.add('hidden');
|
|
}
|
|
if (role === 'viewer') {
|
|
document.getElementById('nav-upload-invoice')?.classList.add('hidden');
|
|
}
|
|
if (role === 'super_admin') {
|
|
document.getElementById('nav-admin')?.classList.remove('hidden');
|
|
}
|
|
|
|
navigateTo('dashboard');
|
|
} else { renderLogin(); }
|
|
}
|
|
|
|
// Helpers for 2FA, API Keys, Modals...
|
|
async function start2FA() {
|
|
const area = document.getElementById('2fa-area');
|
|
const res = await API.post('/auth/2fa/enable', {});
|
|
const { secret, qr_url } = res.data;
|
|
area.innerHTML = `
|
|
<div class="flex gap-8 items-center bg-black/20 p-6 rounded-3xl border border-white/5">
|
|
<div class="bg-white p-2 rounded-2xl"><img src="${qr_url}" class="w-32 h-32"></div>
|
|
<div class="space-y-4">
|
|
<p class="text-sm">امسح الرمز أعلاه، ثم أدخل كود التحقق:</p>
|
|
<div class="flex gap-3"><input id="2fa-code" class="w-32 text-center font-mono text-xl tracking-widest" maxlength="6" placeholder="000000"><button onclick="confirm2FA('${secret}')" class="btn-primary">تأكيد</button></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
async function confirm2FA(secret) {
|
|
try { await API.post('/auth/2fa/verify', { secret, code: document.getElementById('2fa-code').value }); showToast('تم التفعيل!'); renderSettings(); } catch(e) { showToast('كود غير صحيح', 'error'); }
|
|
}
|
|
async function disable2FA() { if(confirm('هل أنت متأكد؟')) { await API.post('/auth/2fa/disable', {}); renderSettings(); } }
|
|
|
|
async function loadApiKeys() {
|
|
const res = await API.get('/api-keys');
|
|
document.getElementById('api-keys-list').innerHTML = res.data.map(k => `
|
|
<div class="flex justify-between items-center p-5 bg-black/20 rounded-3xl border border-white/5">
|
|
<div><p class="font-bold">${k.name}</p><p class="text-xs text-slate-500 font-mono">ID: ${k.id}</p></div>
|
|
<span class="text-[10px] text-primary font-bold px-3 py-1 bg-primary/10 rounded-full border border-primary/20">Active</span>
|
|
</div>
|
|
`).join('') || '<p class="text-center text-slate-500 py-4">لا توجد مفاتيح</p>';
|
|
}
|
|
async function createApiKey() {
|
|
const name = prompt('اسم المفتاح:'); if(!name) return;
|
|
try { const res = await API.post('/api-keys', { name }); alert(`احفظ مفتاحك الآن، لن يظهر مجدداً:\n\n${res.data.key}`); loadApiKeys(); } catch(e) { showToast('فشل إنشاء المفتاح', 'error'); }
|
|
}
|
|
|
|
async function submitToJoFotara(id) {
|
|
try { await API.post(`/invoices/${id}/submit`, {}); showToast('تم إرسال الفاتورة للطابور'); renderInvoiceDetail(id); } catch(e) { showToast(e.error?.message_ar || 'فشل الإرسال', 'error'); }
|
|
}
|
|
|
|
function showAddInvoiceModal() {
|
|
const m = document.getElementById('modals');
|
|
m.classList.replace('hidden', 'flex');
|
|
m.innerHTML = `<div class="glass p-10 rounded-[3rem] w-full max-w-md animate-in">
|
|
<h3 class="text-2xl font-black mb-8">رفع فاتورة جديدة</h3>
|
|
<form id="up-form" class="space-y-6">
|
|
<select id="up-comp" class="w-full" required><option value="">اختر الشركة...</option></select>
|
|
<div class="p-10 border-2 border-dashed border-white/10 rounded-3xl text-center bg-white/5"><input type="file" id="up-file" class="text-xs" required></div>
|
|
<div class="flex gap-4"><button type="button" onclick="document.getElementById('modals').classList.replace('flex','hidden')" class="flex-1 py-3 bg-white/5 rounded-2xl">إلغاء</button><button type="submit" class="flex-1 btn-primary">رفع ومعالجة</button></div>
|
|
</form>
|
|
</div>`;
|
|
API.get('/companies').then(r => {
|
|
document.getElementById('up-comp').innerHTML += r.data.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
|
|
});
|
|
document.getElementById('up-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const fd = new FormData(); fd.append('company_id', e.target['up-comp'].value); fd.append('invoice', e.target['up-file'].files[0]);
|
|
try {
|
|
const b = e.target.querySelector('button[type="submit"]'); b.disabled = true; b.textContent = 'جاري الرفع...';
|
|
await API.upload('/invoices/upload', fd);
|
|
showToast('تم الرفع بنجاح'); m.classList.replace('flex','hidden'); navigateTo('invoices');
|
|
} catch(err) { showToast(err.error?.message_ar || 'فشل الرفع', 'error'); }
|
|
};
|
|
}
|
|
|
|
function showJoFotaraModal(cid) {
|
|
const m = document.getElementById('modals');
|
|
m.classList.replace('hidden', 'flex');
|
|
m.innerHTML = `<div class="glass p-10 rounded-[3rem] w-full max-w-md animate-in">
|
|
<h3 class="text-2xl font-black mb-8">إعدادات JoFotara</h3>
|
|
<form id="jo-form" class="space-y-6">
|
|
<input type="text" id="jo-id" class="w-full" placeholder="Client ID" required>
|
|
<input type="password" id="jo-sec" class="w-full" placeholder="Secret Key" required>
|
|
<div class="flex gap-4"><button type="button" onclick="document.getElementById('modals').classList.replace('flex','hidden')" class="flex-1 py-3 bg-white/5 rounded-2xl">إلغاء</button><button type="submit" class="flex-1 btn-primary">حفظ الربط</button></div>
|
|
</form>
|
|
</div>`;
|
|
document.getElementById('jo-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
await API.post(`/companies/${cid}/jofotara`, { client_id: e.target['jo-id'].value, secret_key: e.target['jo-sec'].value });
|
|
showToast('تم تحديث البيانات'); m.classList.replace('flex','hidden'); renderCompanies();
|
|
} catch(e) { showToast('فشل التحديث', 'error'); }
|
|
};
|
|
}
|
|
|
|
async function renderRegister() {
|
|
const auth = document.getElementById('auth-container');
|
|
auth.innerHTML = `
|
|
<div class="glass p-12 rounded-[3rem] w-full max-w-md shadow-2xl border-t-8 border-t-emerald-500 animate-in">
|
|
<div class="text-center mb-10">
|
|
<h2 class="text-4xl font-black mb-3">إنشاء حساب</h2>
|
|
<p class="text-slate-500">انضم لمنصة مُصادَق اليوم</p>
|
|
</div>
|
|
<form id="reg-form" class="space-y-4">
|
|
<input type="text" id="reg-name" class="w-full" placeholder="الاسم الكامل" required>
|
|
<input type="email" id="reg-email" class="w-full" placeholder="البريد الإلكتروني" required>
|
|
<input type="password" id="reg-pass" class="w-full" placeholder="كلمة المرور" required>
|
|
<button type="submit" class="w-full btn-primary py-4 text-lg">إنشاء الحساب</button>
|
|
</form>
|
|
<div class="mt-8 text-center"><p class="text-sm text-slate-500">لديك حساب بالفعل؟ <a href="#" onclick="renderLogin()" class="text-primary font-bold">دخول</a></p></div>
|
|
</div>
|
|
`;
|
|
document.getElementById('reg-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
const res = await API.post('/auth/register', {
|
|
name: document.getElementById('reg-name').value,
|
|
email: document.getElementById('reg-email').value,
|
|
password: document.getElementById('reg-pass').value
|
|
});
|
|
saveAuth(res.data);
|
|
} catch (err) { showToast(err.error?.message_ar || 'فشل إنشاء الحساب', 'error'); }
|
|
};
|
|
}
|
|
|
|
function render2FAChallenge(tempToken) {
|
|
const auth = document.getElementById('auth-container');
|
|
auth.innerHTML = `
|
|
<div class="glass p-12 rounded-[3rem] w-full max-w-md shadow-2xl border-t-8 border-t-yellow-500 animate-in text-center">
|
|
<h2 class="text-3xl font-black mb-6">التحقق الثنائي</h2>
|
|
<p class="text-slate-400 mb-8">أدخل الكود من تطبيق المصادقة</p>
|
|
<input type="text" id="challenge-code" class="w-full text-center text-4xl tracking-[1rem] font-mono mb-8" maxlength="6" autofocus>
|
|
<button id="verify-btn" class="w-full btn-primary py-4">تحقق ودخول</button>
|
|
</div>
|
|
`;
|
|
document.getElementById('verify-btn').onclick = async () => {
|
|
try {
|
|
const code = document.getElementById('challenge-code').value;
|
|
const res = await fetch('index.php?route=/api/v1/auth/2fa/verify', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${tempToken}` },
|
|
body: JSON.stringify({ code })
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok) saveAuth({ access_token: tempToken, user: data.user });
|
|
else showToast('كود غير صحيح', 'error');
|
|
} catch(e) { showToast('خطأ في النظام', 'error'); }
|
|
};
|
|
}
|
|
|
|
function showAddCompanyModal() {
|
|
const m = document.getElementById('modals');
|
|
m.classList.replace('hidden', 'flex');
|
|
m.innerHTML = `<div class="glass p-10 rounded-[3rem] w-full max-w-md animate-in">
|
|
<h3 class="text-2xl font-black mb-8">إضافة شركة جديدة</h3>
|
|
<form id="comp-form" class="space-y-6">
|
|
<input type="text" id="c-name" class="w-full" placeholder="اسم الشركة" required>
|
|
<input type="text" id="c-tin" class="w-full" placeholder="الرقم الضريبي" required maxlength="10">
|
|
<div class="flex gap-4"><button type="button" onclick="document.getElementById('modals').classList.replace('flex','hidden')" class="flex-1 py-3 bg-white/5 rounded-2xl">إلغاء</button><button type="submit" class="flex-1 btn-primary">إضافة</button></div>
|
|
</form>
|
|
</div>`;
|
|
document.getElementById('comp-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
await API.post('/companies', { name: document.getElementById('c-name').value, tax_identification_number: document.getElementById('c-tin').value });
|
|
showToast('تمت إضافة الشركة'); m.classList.replace('flex','hidden'); renderCompanies();
|
|
} catch(e) { showToast('فشل الإضافة', 'error'); }
|
|
};
|
|
}
|
|
|
|
initApp();
|
|
</script>
|
|
</body>
|
|
</html>
|