1228 lines
82 KiB
PHP
1228 lines
82 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="ar" dir="rtl" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>مُصادَق — أتمتة الفوترة الضريبية</title>
|
|
|
|
<!-- Styles -->
|
|
<link rel="stylesheet" href="assets/css/app.css">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: '#10b981',
|
|
'primary-dark': '#059669',
|
|
dark: '#0a0f1a',
|
|
card: '#0f172a'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
.nav-link.active { background: rgba(16, 185, 129, 0.1); color: #10b981; border-right: 3px solid #10b981; }
|
|
.glass-panel { background: rgba(255, 255, 255, 0.03); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); }
|
|
</style>
|
|
</head>
|
|
<body class="bg-dark text-slate-100 antialiased overflow-x-hidden">
|
|
|
|
<!-- App Wrapper -->
|
|
<div id="app" class="flex h-screen overflow-hidden">
|
|
|
|
<!-- Sidebar -->
|
|
<aside id="sidebar" class="w-64 glass-panel flex-shrink-0 hidden flex-col transition-all duration-300">
|
|
<div class="h-20 flex items-center gap-3 px-6 border-b border-white/10">
|
|
<div class="w-10 h-10 bg-primary rounded-xl flex items-center justify-center shadow-lg shadow-primary/20">
|
|
<span class="text-white font-bold text-xl">م</span>
|
|
</div>
|
|
<h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-slate-400">مُصادَق</h1>
|
|
</div>
|
|
<nav class="flex-1 py-6 space-y-2">
|
|
<a href="#" onclick="navigateTo('dashboard')" id="nav-dashboard" class="nav-link flex items-center gap-3 px-6 py-3 text-slate-400 hover:text-white transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
|
|
لوحة التحكم
|
|
</a>
|
|
<a href="#" onclick="navigateTo('companies')" id="nav-companies" class="nav-link flex items-center gap-3 px-6 py-3 text-slate-400 hover:text-white transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"></path></svg>
|
|
الشركات
|
|
</a>
|
|
<a href="#" onclick="navigateTo('invoices')" id="nav-invoices" class="nav-link flex items-center gap-3 px-6 py-3 text-slate-400 hover:text-white transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
|
الفواتير المرفوعة
|
|
</a>
|
|
<a href="#" onclick="navigateTo('users')" id="nav-users" class="nav-link flex items-center gap-3 px-6 py-3 text-slate-400 hover:text-white transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
|
المستخدمين
|
|
</a>
|
|
<a href="#" onclick="navigateTo('risk-monitor')" id="nav-risk-monitor" class="nav-link flex items-center gap-3 px-6 py-3 text-slate-400 hover:text-white transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
|
مراقبة المخاطر
|
|
</a>
|
|
<a href="#" onclick="navigateTo('settings')" id="nav-settings" class="nav-link flex items-center gap-3 px-6 py-3 text-slate-400 hover:text-white transition-colors">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
|
الإعدادات
|
|
</a>
|
|
<a href="#" onclick="navigateTo('admin')" id="nav-admin" class="nav-link flex items-center gap-3 px-6 py-3 text-red-400 hover:text-red-300 transition-colors hidden">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
|
|
إدارة النظام (Super)
|
|
</a>
|
|
</nav>
|
|
<div class="p-6 border-t border-white/10">
|
|
<button onclick="logout()" class="flex items-center gap-3 text-red-400 hover:text-red-300 transition-colors w-full">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
|
|
تسجيل الخروج
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Area -->
|
|
<div class="flex-1 flex flex-col h-screen overflow-hidden">
|
|
<!-- Header -->
|
|
<header id="header" class="h-20 glass-panel flex items-center justify-between px-8 flex-shrink-0 hidden">
|
|
<h2 id="page-title" class="text-2xl font-bold">لوحة التحكم</h2>
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-10 h-10 rounded-full bg-slate-800 border border-white/10 flex items-center justify-center">
|
|
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Content -->
|
|
<main class="flex-1 overflow-y-auto p-8 relative">
|
|
<div id="page-content" class="max-w-7xl mx-auto pb-24"></div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- AI Floating Assistant -->
|
|
<div id="ai-container" class="fixed bottom-8 left-8 z-[60] hidden">
|
|
<button onclick="document.getElementById('ai-chat').classList.toggle('hidden')" class="w-16 h-16 bg-gradient-to-tr from-primary to-emerald-400 rounded-full flex items-center justify-center shadow-2xl shadow-primary/40 hover:scale-110 transition-transform">
|
|
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path></svg>
|
|
</button>
|
|
|
|
<!-- Chat Popover -->
|
|
<div id="ai-chat" class="hidden absolute bottom-20 left-0 w-80 glass-panel rounded-3xl p-6 shadow-2xl">
|
|
<h4 class="font-bold mb-4 flex items-center gap-2">
|
|
<span class="w-2 h-2 bg-primary rounded-full animate-pulse"></span>
|
|
مُساعد مُصادَق الذكي
|
|
</h4>
|
|
<div id="ai-answer" class="bg-black/20 rounded-xl p-3 mb-4 min-h-[60px] text-sm text-slate-300">كيف يمكنني مساعدتك اليوم؟</div>
|
|
<div class="relative">
|
|
<input type="text" id="ai-query" class="w-full bg-white/10 border border-white/10 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-primary" placeholder="اسأل عن فواتيرك...">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast Container -->
|
|
<div id="toast-container" class="fixed top-8 left-1/2 -translate-x-1/2 z-[200] space-y-4 pointer-events-none"></div>
|
|
|
|
<!-- Modals Container -->
|
|
<div id="modals"></div>
|
|
|
|
<script>
|
|
// ══════════════════════════════════════════════════════════
|
|
// مُصادَق — API Client
|
|
// ══════════════════════════════════════════════════════════
|
|
const API = {
|
|
baseUrl: 'index.php?route=/api/v1',
|
|
accessToken: localStorage.getItem('access_token'),
|
|
|
|
async _request(method, path, body = null, isFormData = false) {
|
|
const headers = { 'Accept': 'application/json' };
|
|
if (this.accessToken) headers['Authorization'] = `Bearer ${this.accessToken}`;
|
|
if (!isFormData && 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;
|
|
},
|
|
|
|
get(path) { return this._request('GET', path); },
|
|
post(path, body) { return this._request('POST', path, body); },
|
|
upload(path, formData) { return this._request('POST', path, formData, true); }
|
|
};
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// SPA Engine & Views
|
|
// ══════════════════════════════════════════════════════════
|
|
const isLoggedIn = () => !!localStorage.getItem('access_token');
|
|
const contentDiv = document.getElementById('page-content');
|
|
let currentChart = null;
|
|
|
|
function showToast(message, type = 'success') {
|
|
const container = document.getElementById('toast-container');
|
|
const toast = document.createElement('div');
|
|
const colors = {
|
|
success: 'bg-emerald-500 shadow-emerald-500/20',
|
|
error: 'bg-red-500 shadow-red-500/20',
|
|
warning: 'bg-yellow-500 shadow-yellow-500/20'
|
|
};
|
|
|
|
toast.className = `px-8 py-4 rounded-2xl text-white font-bold shadow-2xl transition-all duration-500 translate-y-10 opacity-0 pointer-events-auto flex items-center gap-3 ${colors[type]}`;
|
|
toast.innerHTML = `
|
|
<span class="text-xl">${type === 'success' ? '✓' : type === 'error' ? '✕' : '!'}</span>
|
|
<span>${message}</span>
|
|
`;
|
|
|
|
container.appendChild(toast);
|
|
setTimeout(() => {
|
|
toast.classList.remove('translate-y-10', 'opacity-0');
|
|
}, 10);
|
|
|
|
setTimeout(() => {
|
|
toast.classList.add('opacity-0', '-translate-y-10');
|
|
setTimeout(() => toast.remove(), 500);
|
|
}, 4000);
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('user_role');
|
|
API.accessToken = null;
|
|
initApp();
|
|
}
|
|
|
|
async function navigateTo(page) {
|
|
document.querySelectorAll('.nav-link').forEach(el => el.classList.remove('active'));
|
|
const activeLink = document.getElementById(`nav-${page}`);
|
|
if (activeLink) activeLink.classList.add('active');
|
|
|
|
contentDiv.innerHTML = '<div class="flex justify-center mt-20"><div class="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div></div>';
|
|
|
|
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 renderAdminStats();
|
|
}
|
|
|
|
// ── Users View ───────────────────────────────────────────
|
|
async function renderUsers() {
|
|
document.getElementById('page-title').textContent = 'إدارة المستخدمين';
|
|
|
|
// Check RBAC
|
|
const role = localStorage.getItem('user_role');
|
|
if (role !== 'super_admin' && role !== 'admin') {
|
|
contentDiv.innerHTML = `<div class="text-red-400 p-8 glass-panel rounded-3xl text-center">عذراً، ليس لديك صلاحية للوصول إلى هذه الصفحة.</div>`;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await API.get('/users');
|
|
const users = res.data;
|
|
|
|
let html = `
|
|
<div class="flex justify-end mb-6">
|
|
<button onclick="showAddUserModal()" class="bg-primary hover:bg-primary-dark text-white px-6 py-2 rounded-xl transition-all shadow-lg flex items-center gap-2 font-bold">
|
|
+ إضافة مستخدم جديد
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
`;
|
|
|
|
if (users.length === 0) {
|
|
html += `<div class="col-span-full text-center py-12 text-slate-500 glass-panel rounded-3xl">لا يوجد مستخدمين مسجلين.</div>`;
|
|
} else {
|
|
users.forEach(user => {
|
|
let roleColor = 'text-slate-400';
|
|
let roleLabel = 'مستخدم';
|
|
|
|
if (user.role === 'super_admin') { roleColor = 'text-primary'; roleLabel = 'سوبر أدمن'; }
|
|
else if (user.role === 'admin') { roleColor = 'text-blue-400'; roleLabel = 'مدير شركة'; }
|
|
else if (user.role === 'accountant') { roleColor = 'text-purple-400'; roleLabel = 'محاسب'; }
|
|
else if (user.role === 'employee') { roleColor = 'text-orange-400'; roleLabel = 'موظف'; }
|
|
|
|
html += `
|
|
<div class="glass-panel p-6 rounded-3xl flex flex-col h-full border-t-4 border-t-primary">
|
|
<div class="flex items-center gap-4 mb-4">
|
|
<div class="w-12 h-12 rounded-full bg-black/40 flex items-center justify-center font-bold text-xl text-primary">
|
|
${user.name.charAt(0)}
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold">${user.name}</h3>
|
|
<p class="text-slate-400 text-sm">${user.email}</p>
|
|
</div>
|
|
</div>
|
|
<div class="mt-auto space-y-3">
|
|
<div class="flex items-center justify-between text-sm p-3 bg-black/20 rounded-xl border border-white/5">
|
|
<span class="text-slate-400">الصلاحية</span>
|
|
<span class="${roleColor} font-bold">${roleLabel}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between text-sm p-3 bg-black/20 rounded-xl border border-white/5">
|
|
<span class="text-slate-400">الحالة</span>
|
|
${user.is_active ? '<span class="text-emerald-400 font-bold">نشط</span>' : '<span class="text-red-400 font-bold">معطل</span>'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += `</div>`;
|
|
contentDiv.innerHTML = html;
|
|
} catch(err) {
|
|
contentDiv.innerHTML = `<div class="text-red-400 p-8 glass-panel rounded-3xl">خطأ في جلب المستخدمين: ${err.error?.message_ar || err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
async function showAddUserModal() {
|
|
const currentRole = localStorage.getItem('user_role');
|
|
let companies = [];
|
|
let companySelectHtml = '';
|
|
|
|
if (currentRole === 'super_admin') {
|
|
try {
|
|
const res = await API.get('/companies');
|
|
companies = res.data;
|
|
companySelectHtml = `
|
|
<select id="usr-company" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none">
|
|
<option value="">-- ربط بشركة (اختياري للسوبر أدمن) --</option>
|
|
${companies.map(c => `<option value="${c.id}">${c.name}</option>`).join('')}
|
|
</select>
|
|
`;
|
|
} catch (err) { console.error('Failed to fetch companies'); }
|
|
}
|
|
|
|
let optionsHtml = `
|
|
<option value="accountant">محاسب</option>
|
|
<option value="employee">موظف</option>
|
|
`;
|
|
if (currentRole === 'super_admin') {
|
|
optionsHtml += `<option value="admin">مدير شركة</option>`;
|
|
}
|
|
|
|
const modals = document.getElementById('modals');
|
|
modals.innerHTML = `
|
|
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 overflow-y-auto" id="user-modal">
|
|
<div class="glass-panel p-8 rounded-3xl w-full max-w-md border border-white/10 shadow-2xl my-auto">
|
|
<h3 class="text-2xl font-bold mb-6">إضافة مستخدم جديد</h3>
|
|
<form id="add-user-form" class="space-y-4">
|
|
<input type="text" id="usr-name" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="الاسم الكامل" required>
|
|
<input type="email" id="usr-email" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="البريد الإلكتروني" required>
|
|
<input type="password" id="usr-password" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="كلمة المرور" required>
|
|
<select id="usr-role" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" required>
|
|
${optionsHtml}
|
|
</select>
|
|
${companySelectHtml}
|
|
|
|
<div class="flex gap-3 mt-6 pt-4 border-t border-white/10">
|
|
<button type="button" onclick="document.getElementById('user-modal').remove()" class="flex-1 py-3 bg-white/5 hover:bg-white/10 rounded-xl transition">إلغاء</button>
|
|
<button type="submit" class="flex-1 py-3 bg-primary hover:bg-primary-dark text-white font-bold rounded-xl shadow-lg transition">إضافة المستخدم</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('add-user-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
const data = {
|
|
name: document.getElementById('usr-name').value,
|
|
email: document.getElementById('usr-email').value,
|
|
password: document.getElementById('usr-password').value,
|
|
role: document.getElementById('usr-role').value,
|
|
assigned_company_id: document.getElementById('usr-company')?.value || null
|
|
};
|
|
await API.post('/users', data);
|
|
document.getElementById('user-modal').remove();
|
|
renderUsers();
|
|
} catch(err) {
|
|
alert(err.error?.message_ar || err.error?.details?.message || err.message || 'حدث خطأ');
|
|
}
|
|
};
|
|
}
|
|
|
|
// ── Login View ───────────────────────────────────────────
|
|
function renderLogin() {
|
|
document.getElementById('sidebar').classList.add('hidden');
|
|
document.getElementById('header').classList.add('hidden');
|
|
document.getElementById('ai-container').classList.add('hidden');
|
|
|
|
contentDiv.innerHTML = `
|
|
<div class="flex flex-col items-center justify-center min-h-[80vh]">
|
|
<div class="w-full max-w-md p-10 glass-panel rounded-3xl shadow-2xl">
|
|
<div class="w-16 h-16 bg-primary rounded-2xl flex items-center justify-center shadow-lg shadow-primary/30 mx-auto mb-6">
|
|
<span class="text-white font-bold text-3xl">م</span>
|
|
</div>
|
|
<h2 class="text-3xl font-bold mb-2 text-center">مرحباً بك في مُصادَق</h2>
|
|
<p class="text-slate-400 text-center mb-8">سجل دخولك للوصول إلى منصة الفوترة</p>
|
|
|
|
<form id="login-form" class="space-y-5">
|
|
<input type="email" id="login-email" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="البريد الإلكتروني" required>
|
|
<input type="password" id="login-password" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="كلمة المرور" required>
|
|
<div id="login-error" class="text-red-400 text-sm text-center hidden p-2 bg-red-500/10 rounded-lg border border-red-500/20"></div>
|
|
<button type="submit" class="w-full bg-gradient-to-r from-primary to-emerald-400 hover:from-primary-dark hover:to-primary text-white font-bold py-3 rounded-xl shadow-lg transition-all">تسجيل الدخول</button>
|
|
</form>
|
|
<p class="mt-6 text-center text-slate-400 text-sm">ليس لديك حساب؟ <a href="#" onclick="renderRegister()" class="text-primary hover:underline">سجل شركتك الآن</a></p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('login-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const btn = e.target.querySelector('button');
|
|
btn.innerHTML = 'جاري التحقق...';
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const email = document.getElementById('login-email').value;
|
|
const password = document.getElementById('login-password').value;
|
|
const res = await API.post('/auth/login', { email, password });
|
|
localStorage.setItem('access_token', res.data.access_token);
|
|
localStorage.setItem('user_role', res.data.user.role);
|
|
API.accessToken = res.data.access_token;
|
|
initApp();
|
|
} catch (err) {
|
|
const errEl = document.getElementById('login-error');
|
|
errEl.textContent = err.error?.message_ar || err.error?.details?.message || err.message || 'خطأ في الدخول';
|
|
errEl.classList.remove('hidden');
|
|
btn.innerHTML = 'تسجيل الدخول';
|
|
btn.disabled = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
// ── Register View ───────────────────────────────────────────
|
|
function renderRegister() {
|
|
document.getElementById('sidebar').classList.add('hidden');
|
|
document.getElementById('header').classList.add('hidden');
|
|
document.getElementById('ai-container').classList.add('hidden');
|
|
|
|
contentDiv.innerHTML = `
|
|
<div class="flex flex-col items-center justify-center min-h-[80vh] py-10">
|
|
<div class="w-full max-w-lg p-10 glass-panel rounded-3xl shadow-2xl">
|
|
<div class="w-16 h-16 bg-gradient-to-br from-emerald-400 to-primary rounded-2xl flex items-center justify-center shadow-lg shadow-emerald-500/30 mx-auto mb-6">
|
|
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"></path></svg>
|
|
</div>
|
|
<h2 class="text-3xl font-bold mb-2 text-center">التسجيل في مُصادَق</h2>
|
|
<p class="text-slate-400 text-center mb-8">ابدأ تجربتك المجانية واربط مع جو-فواتير بثوانٍ</p>
|
|
|
|
<form id="register-form" class="space-y-4">
|
|
<input type="text" id="reg-tenant" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="اسم الشركة (المستأجر)" required>
|
|
<input type="text" id="reg-user" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="الاسم الكامل لمدير النظام" required>
|
|
<input type="email" id="reg-email" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="البريد الإلكتروني" required>
|
|
<input type="password" id="reg-password" class="w-full bg-black/20 border border-white/10 rounded-xl px-5 py-3 focus:outline-none focus:border-primary text-white" placeholder="كلمة المرور" required>
|
|
<div id="register-error" class="text-red-400 text-sm text-center hidden p-2 bg-red-500/10 rounded-lg border border-red-500/20"></div>
|
|
<button type="submit" class="w-full bg-gradient-to-r from-emerald-500 to-primary hover:from-primary hover:to-emerald-500 text-white font-bold py-3 rounded-xl shadow-lg transition-all mt-2">إنشاء حساب جديد</button>
|
|
</form>
|
|
<p class="mt-6 text-center text-slate-400 text-sm">لديك حساب بالفعل؟ <a href="#" onclick="renderLogin()" class="text-primary hover:underline">سجل الدخول</a></p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('register-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
const btn = e.target.querySelector('button');
|
|
btn.innerHTML = 'جاري إنشاء الحساب...';
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const data = {
|
|
tenant_name: document.getElementById('reg-tenant').value,
|
|
user_name: document.getElementById('reg-user').value,
|
|
email: document.getElementById('reg-email').value,
|
|
password: document.getElementById('reg-password').value
|
|
};
|
|
const res = await API.post('/auth/register', data);
|
|
localStorage.setItem('access_token', res.data.access_token);
|
|
localStorage.setItem('user_role', res.data.user.role);
|
|
API.accessToken = res.data.access_token;
|
|
initApp();
|
|
} catch (err) {
|
|
const errEl = document.getElementById('register-error');
|
|
errEl.textContent = err.error?.message_ar || err.error?.details?.message || err.message || 'خطأ في التسجيل';
|
|
errEl.classList.remove('hidden');
|
|
btn.innerHTML = 'إنشاء حساب جديد';
|
|
btn.disabled = false;
|
|
}
|
|
};
|
|
}
|
|
|
|
// ── Dashboard View ───────────────────────────────────────
|
|
async function renderDashboard() {
|
|
document.getElementById('page-title').textContent = 'لوحة التحكم السريعة';
|
|
try {
|
|
const res = await API.get('/dashboard');
|
|
const stats = res.data;
|
|
|
|
let html = `
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
<div class="glass-panel p-6 rounded-3xl border-t-4 border-t-primary shadow-xl bg-gradient-to-br from-black/40 to-transparent">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<p class="text-slate-400 font-bold">فواتير هذا الشهر</p>
|
|
<svg class="w-8 h-8 text-primary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
|
</div>
|
|
<h3 class="text-5xl font-black text-white">${stats.total_this_month}</h3>
|
|
</div>
|
|
<div class="glass-panel p-6 rounded-3xl border-t-4 border-t-emerald-500 shadow-xl bg-gradient-to-br from-black/40 to-transparent">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<p class="text-slate-400 font-bold">نسبة استهلاك الباقة</p>
|
|
<svg class="w-8 h-8 text-emerald-500 opacity-50" 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>
|
|
<h3 class="text-5xl font-black text-emerald-400">${stats.subscription_usage}%</h3>
|
|
</div>
|
|
<div class="glass-panel p-6 rounded-3xl flex flex-col justify-center gap-3 bg-gradient-to-br from-primary/10 to-transparent">
|
|
<h3 class="text-lg font-bold text-white mb-2">إجراءات سريعة</h3>
|
|
<button onclick="navigateTo('invoices')" class="w-full py-3 bg-white/10 hover:bg-white/20 rounded-xl transition border border-white/5 text-sm font-bold flex items-center justify-center gap-2">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
|
|
عرض الفواتير
|
|
</button>
|
|
<button onclick="showUploadInvoiceModal()" class="w-full py-3 bg-primary hover:bg-primary-dark rounded-xl transition text-white text-sm font-bold shadow-lg flex items-center justify-center gap-2">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
|
|
رفع فاتورة جديدة
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
<div class="glass-panel p-8 rounded-3xl shadow-2xl">
|
|
<h4 class="font-bold mb-6 text-xl flex items-center gap-2"><span class="w-2 h-6 bg-primary rounded-full"></span> أحدث الفواتير</h4>
|
|
<div class="space-y-4">
|
|
`;
|
|
|
|
if (stats.recent_invoices.length === 0) {
|
|
html += `<p class="text-slate-500 text-center py-8 bg-black/20 rounded-xl">لا توجد فواتير بعد</p>`;
|
|
} else {
|
|
stats.recent_invoices.forEach(inv => {
|
|
const statusColor = inv.status === 'approved' ? 'text-primary' : (inv.status === 'rejected' ? 'text-red-400' : 'text-yellow-400');
|
|
html += `
|
|
<div class="flex justify-between items-center p-5 bg-black/30 rounded-2xl border border-white/5 hover:border-white/10 transition-colors">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-10 h-10 rounded-full bg-white/5 flex items-center justify-center">
|
|
<svg class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
|
</div>
|
|
<div>
|
|
<p class="font-bold text-sm text-slate-200">${inv.id ? inv.id.substring(0,8) : ''}...</p>
|
|
<p class="text-xs text-slate-400 mt-1">${inv.company_name}</p>
|
|
</div>
|
|
</div>
|
|
<span class="${statusColor} text-xs font-bold bg-white/5 px-4 py-2 rounded-full tracking-wider border border-white/5">${inv.status}</span>
|
|
</div>`;
|
|
});
|
|
}
|
|
|
|
html += `
|
|
</div>
|
|
</div>
|
|
<div class="glass-panel p-8 rounded-3xl shadow-2xl flex flex-col items-center justify-center">
|
|
<h4 class="font-bold mb-6 text-xl self-start flex items-center gap-2"><span class="w-2 h-6 bg-primary rounded-full"></span> حالة الفواتير</h4>
|
|
<div class="w-full max-w-[250px] aspect-square relative">
|
|
<canvas id="statusChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
contentDiv.innerHTML = html;
|
|
|
|
// Render Chart
|
|
if (stats.status_distribution && stats.status_distribution.length > 0) {
|
|
const ctx = document.getElementById('statusChart').getContext('2d');
|
|
if (currentChart) currentChart.destroy();
|
|
currentChart = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: stats.status_distribution.map(s => s.status),
|
|
datasets: [{
|
|
data: stats.status_distribution.map(s => s.count),
|
|
backgroundColor: ['#10b981', '#fbbf24', '#f87171', '#60a5fa'],
|
|
borderWidth: 0,
|
|
hoverOffset: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { position: 'bottom', labels: { color: '#cbd5e1', font: { family: 'system-ui' } } }
|
|
},
|
|
cutout: '70%'
|
|
}
|
|
});
|
|
}
|
|
|
|
} catch (err) {
|
|
contentDiv.innerHTML = `<div class="text-red-400 p-4 glass-panel rounded-xl">خطأ في جلب الإحصائيات: ${err.error?.message_ar || err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
// ── Companies View ───────────────────────────────────────
|
|
async function renderCompanies() {
|
|
document.getElementById('page-title').textContent = 'إدارة الشركات';
|
|
try {
|
|
const res = await API.get('/companies');
|
|
const companies = res.data;
|
|
|
|
let html = `
|
|
<div class="flex justify-end mb-6">
|
|
<button onclick="showAddCompanyModal()" class="bg-primary hover:bg-primary-dark text-white px-6 py-2 rounded-xl transition-all shadow-lg flex items-center gap-2 font-bold">
|
|
+ إضافة شركة جديدة
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
`;
|
|
|
|
if (companies.length === 0) {
|
|
html += `<div class="col-span-full text-center py-12 text-slate-500 glass-panel rounded-3xl">لا توجد شركات مسجلة بعد.</div>`;
|
|
} else {
|
|
companies.forEach(company => {
|
|
html += `
|
|
<div class="glass-panel p-6 rounded-3xl flex flex-col h-full border-t-4 border-t-primary">
|
|
<h3 class="text-xl font-bold mb-1">${company.name}</h3>
|
|
<p class="text-slate-400 text-sm font-mono mb-4">الرقم الضريبي: ${company.tax_identification_number || company.tax_number || 'غير متوفر'}</p>
|
|
|
|
<div class="mt-auto space-y-3">
|
|
<div class="flex items-center justify-between text-sm p-3 bg-black/20 rounded-xl border border-white/5">
|
|
<span class="text-slate-400">بوابة JoFotara</span>
|
|
${company.jofotara_client_id_encrypted
|
|
? '<span class="text-primary font-bold">مربوط ✓</span>'
|
|
: '<span class="text-yellow-400 font-bold">غير مربوط ⚠</span>'}
|
|
</div>
|
|
<button onclick="showJoFotaraModal('${company.id}')" class="w-full py-2 bg-white/5 hover:bg-white/10 rounded-xl text-sm transition border border-white/10">إعدادات الربط</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += `</div>`;
|
|
contentDiv.innerHTML = html;
|
|
} catch (err) {
|
|
contentDiv.innerHTML = `<div class="text-red-400">خطأ في جلب الشركات</div>`;
|
|
}
|
|
}
|
|
|
|
// ── Invoices View ────────────────────────────────────────
|
|
async function renderInvoices() {
|
|
document.getElementById('page-title').textContent = 'سجل الفواتير';
|
|
try {
|
|
const res = await API.get('/invoices');
|
|
const invoices = res.data;
|
|
|
|
let html = `
|
|
<div class="flex justify-end mb-6">
|
|
<button onclick="showUploadInvoiceModal()" class="bg-primary hover:bg-primary-dark text-white px-6 py-2 rounded-xl transition-all shadow-lg flex items-center gap-2 font-bold">
|
|
رفع فاتورة يدوياً
|
|
</button>
|
|
</div>
|
|
<div class="glass-panel rounded-3xl overflow-hidden">
|
|
<table class="w-full text-right text-sm">
|
|
<thead class="bg-white/5 border-b border-white/10 text-slate-300">
|
|
<tr>
|
|
<th class="p-4">الرقم التسلسلي</th>
|
|
<th class="p-4">الشركة</th>
|
|
<th class="p-4">التاريخ</th>
|
|
<th class="p-4">الحالة</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-white/5">
|
|
`;
|
|
|
|
if (invoices.length === 0) {
|
|
html += `<tr><td colspan="4" class="p-8 text-center text-slate-500">لا توجد فواتير.</td></tr>`;
|
|
} else {
|
|
invoices.forEach(inv => {
|
|
const statusColor = inv.status === 'approved' ? 'text-primary' : (inv.status === 'rejected' ? 'text-red-400' : 'text-yellow-400');
|
|
html += `
|
|
<tr onclick="renderInvoiceDetail('${inv.id}')" class="hover:bg-white/5 transition-colors cursor-pointer group">
|
|
<td class="p-4 font-mono text-xs text-slate-300 group-hover:text-primary transition-colors">${inv.id}</td>
|
|
<td class="p-4 font-bold text-slate-200">${inv.company_id}</td>
|
|
<td class="p-4 text-slate-400">${new Date(inv.created_at).toLocaleDateString('ar-JO')}</td>
|
|
<td class="p-4 font-bold ${statusColor}">${inv.status}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += `</tbody></table></div>`;
|
|
contentDiv.innerHTML = html;
|
|
} catch (err) {
|
|
contentDiv.innerHTML = `<div class="text-red-400">خطأ في جلب الفواتير</div>`;
|
|
}
|
|
}
|
|
|
|
async function renderInvoiceDetail(id) {
|
|
document.getElementById('page-title').textContent = 'تفاصيل الفاتورة';
|
|
contentDiv.innerHTML = `<div class="flex items-center justify-center p-20"><div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div></div>`;
|
|
|
|
try {
|
|
const res = await API.get(`/invoices/${id}`);
|
|
const inv = res.data;
|
|
|
|
const statusColor = inv.status === 'approved' ? 'text-primary' : (inv.status === 'rejected' ? 'text-red-400' : 'text-yellow-400');
|
|
|
|
let linesHtml = '';
|
|
if (inv.lines && inv.lines.length > 0) {
|
|
inv.lines.forEach(line => {
|
|
linesHtml += `
|
|
<tr class="border-b border-white/5">
|
|
<td class="py-3 text-slate-300">${line.description || 'بدون وصف'}</td>
|
|
<td class="py-3 text-center">${line.quantity}</td>
|
|
<td class="py-3 text-center">${line.unit_price}</td>
|
|
<td class="py-3 text-left font-bold">${line.line_total}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
linesHtml = '<tr><td colspan="4" class="py-4 text-center text-slate-500">لا توجد بنود مستخرجة بعد</td></tr>';
|
|
}
|
|
|
|
contentDiv.innerHTML = `
|
|
<div class="flex flex-col lg:flex-row gap-6 h-[calc(100vh-200px)]">
|
|
<!-- Left side: Original File View (Placeholder for now, could be PDF viewer) -->
|
|
<div class="lg:w-1/2 glass-panel rounded-3xl overflow-hidden flex flex-col">
|
|
<div class="p-4 bg-white/5 border-b border-white/10 flex justify-between items-center">
|
|
<h4 class="font-bold">المستند الأصلي</h4>
|
|
<a href="${inv.original_file_path}" target="_blank" class="text-xs text-primary hover:underline">فتح في نافذة جديدة</a>
|
|
</div>
|
|
<div class="flex-1 bg-black/40 flex items-center justify-center text-slate-500 italic p-10 text-center">
|
|
${inv.original_file_path.endsWith('.pdf')
|
|
? `<iframe src="${inv.original_file_path}" class="w-full h-full border-none"></iframe>`
|
|
: `<img src="${inv.original_file_path}" class="max-w-full max-h-full object-contain" alt="Invoice Image">`
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right side: Extracted Data -->
|
|
<div class="lg:w-1/2 flex flex-col gap-6 overflow-y-auto pr-2 custom-scrollbar">
|
|
<div class="glass-panel p-6 rounded-3xl">
|
|
<div class="flex justify-between items-start mb-6">
|
|
<div>
|
|
<h4 class="text-lg font-bold mb-1">${inv.supplier_name || 'مورد غير معروف'}</h4>
|
|
<p class="text-sm text-slate-400">رقم الفاتورة: ${inv.invoice_number || '---'}</p>
|
|
</div>
|
|
<span class="px-4 py-1 rounded-full text-xs font-bold bg-white/5 ${statusColor} border border-current">
|
|
${inv.status.toUpperCase()}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4 mb-6">
|
|
<div class="p-4 bg-white/5 rounded-2xl border border-white/5">
|
|
<p class="text-xs text-slate-500 mb-1">تاريخ الفاتورة</p>
|
|
<p class="font-bold">${inv.invoice_date || '---'}</p>
|
|
</div>
|
|
<div class="p-4 bg-white/5 rounded-2xl border border-white/5">
|
|
<p class="text-xs text-slate-500 mb-1">الرقم الضريبي (المورد)</p>
|
|
<p class="font-bold font-mono">${inv.supplier_tin || '---'}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h5 class="font-bold mb-3 text-sm">بنود الفاتورة</h5>
|
|
<table class="w-full text-sm mb-6">
|
|
<thead class="text-slate-500 text-xs border-b border-white/10">
|
|
<tr>
|
|
<th class="py-2 text-right">الوصف</th>
|
|
<th class="py-2 text-center">الكمية</th>
|
|
<th class="py-2 text-center">السعر</th>
|
|
<th class="py-2 text-left">المجموع</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${linesHtml}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="space-y-2 pt-4 border-t border-white/10">
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-slate-400">المجموع الفرعي</span>
|
|
<span class="font-bold">${inv.subtotal} JOD</span>
|
|
</div>
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-slate-400">الضريبة</span>
|
|
<span class="font-bold">${inv.tax_amount} JOD</span>
|
|
</div>
|
|
<div class="flex justify-between text-lg pt-2 border-t border-white/5">
|
|
<span class="font-bold">الإجمالي</span>
|
|
<span class="font-black text-primary">${inv.grand_total} JOD</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-4 mb-10">
|
|
<button onclick="renderInvoices()" class="flex-1 py-3 bg-white/5 hover:bg-white/10 rounded-2xl transition font-bold border border-white/10">عودة</button>
|
|
${inv.status === 'extracted' ? `
|
|
<button onclick="submitToJoFotara('${inv.id}')" class="flex-[2] py-3 bg-gradient-to-r from-primary to-emerald-400 hover:shadow-primary/20 hover:shadow-xl rounded-2xl text-white font-bold transition-all">إرسال لـ جو-فواتير</button>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} catch (err) {
|
|
contentDiv.innerHTML = `<div class="text-red-400 p-10">خطأ في تحميل تفاصيل الفاتورة: ${err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
async function submitToJoFotara(id) {
|
|
try {
|
|
// We'll need a POST /api/v1/invoices/{id}/submit endpoint
|
|
const res = await API.post(`/invoices/${id}/submit`, {});
|
|
alert('تم إرسال الفاتورة للطابور بنجاح. سيتم تحديث الحالة تلقائياً.');
|
|
renderInvoiceDetail(id);
|
|
} catch (err) {
|
|
alert(err.error?.message_ar || 'فشل الإرسال: تأكد من إعدادات الربط للشركة.');
|
|
}
|
|
}
|
|
|
|
// ── Risk Monitor View ────────────────────────────────────
|
|
async function renderRiskMonitor() {
|
|
document.getElementById('page-title').textContent = 'مراقبة المخاطر والالتزام';
|
|
try {
|
|
contentDiv.innerHTML = `
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
<div class="glass-panel p-8 rounded-3xl">
|
|
<h3 class="text-xl font-bold mb-6 flex items-center gap-2">
|
|
<svg class="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
|
|
تحليل المخاطر الحالي
|
|
</h3>
|
|
<div class="space-y-6">
|
|
<div class="p-6 bg-black/20 rounded-2xl border border-white/5">
|
|
<div class="flex justify-between items-center mb-2">
|
|
<span class="text-slate-400">مؤشر الالتزام العام</span>
|
|
<span class="text-primary font-bold text-lg">94%</span>
|
|
</div>
|
|
<div class="w-full bg-white/10 h-2 rounded-full overflow-hidden">
|
|
<div class="bg-primary h-full" style="width: 94%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="p-4 bg-white/5 rounded-xl text-center">
|
|
<p class="text-xs text-slate-500 mb-1">فواتير مرفوضة</p>
|
|
<p class="text-2xl font-bold text-red-400">2</p>
|
|
</div>
|
|
<div class="p-4 bg-white/5 rounded-xl text-center">
|
|
<p class="text-xs text-slate-500 mb-1">تنبيهات حرجة</p>
|
|
<p class="text-2xl font-bold text-yellow-400">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="glass-panel p-8 rounded-3xl">
|
|
<h3 class="text-xl font-bold mb-6">عوامل الخطورة المكتشفة</h3>
|
|
<div class="space-y-4">
|
|
<div class="flex items-start gap-4 p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-2xl">
|
|
<svg class="w-6 h-6 text-yellow-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
|
<div>
|
|
<p class="font-bold text-yellow-400">تأخير في الرفع</p>
|
|
<p class="text-sm text-slate-400">تم رصد 5 فواتير تم رفعها بعد أكثر من 3 أيام من تاريخ الإصدار.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-start gap-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-2xl">
|
|
<svg class="w-6 h-6 text-emerald-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
<div>
|
|
<p class="font-bold text-emerald-400">دقة البيانات</p>
|
|
<p class="text-sm text-slate-400">نسبة تطابق البيانات المستخرجة مع القواعد الضريبية بلغت 100% هذا الشهر.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} catch (err) {
|
|
contentDiv.innerHTML = `<div class="text-red-400">خطأ في تحميل بيانات المخاطر</div>`;
|
|
}
|
|
}
|
|
|
|
// ── Settings View ────────────────────────────────────────
|
|
async function renderSettings() {
|
|
document.getElementById('page-title').textContent = 'الإعدادات الشخصية والأمان';
|
|
|
|
contentDiv.innerHTML = `
|
|
<div class="max-w-4xl space-y-8">
|
|
<!-- 2FA Section -->
|
|
<div class="glass-panel p-8 rounded-3xl border-t-4 border-t-primary">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h3 class="text-2xl font-bold mb-2">التحقق بخطوتين (2FA)</h3>
|
|
<p class="text-slate-400">أضف طبقة حماية إضافية لحسابك باستخدام تطبيق التحقق.</p>
|
|
</div>
|
|
<div id="2fa-status" class="px-4 py-2 bg-white/5 rounded-xl border border-white/10 text-sm">
|
|
جاري التحقق...
|
|
</div>
|
|
</div>
|
|
<div id="2fa-content" class="space-y-4">
|
|
<!-- Dynamic Content -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Keys Section -->
|
|
<div class="glass-panel p-8 rounded-3xl border-t-4 border-t-blue-500">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h3 class="text-2xl font-bold mb-2">مفاتيح API</h3>
|
|
<p class="text-slate-400">استخدم هذه المفاتيح للربط مع تطبيقات خارجية أو الموبايل.</p>
|
|
</div>
|
|
<button onclick="showCreateApiKeyModal()" class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-xl font-bold transition">إنشاء مفتاح جديد</button>
|
|
</div>
|
|
<div id="api-keys-list" class="space-y-3">
|
|
<!-- Dynamic Content -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
update2FAStatus();
|
|
loadApiKeys();
|
|
}
|
|
|
|
async function update2FAStatus() {
|
|
try {
|
|
const res = await API.get('/auth/me');
|
|
const user = res.data;
|
|
const statusEl = document.getElementById('2fa-status');
|
|
const contentEl = document.getElementById('2fa-content');
|
|
|
|
if (user.totp_enabled) {
|
|
statusEl.innerHTML = '<span class="text-primary font-bold">● مفعل</span>';
|
|
contentEl.innerHTML = `
|
|
<p class="text-sm text-slate-400 mb-4">حسابك محمي حالياً بالتحقق الثنائي.</p>
|
|
<button onclick="disable2FA()" class="px-6 py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 rounded-xl transition font-bold">تعطيل التحقق الثنائي</button>
|
|
`;
|
|
} else {
|
|
statusEl.innerHTML = '<span class="text-slate-500 font-bold">○ غير مفعل</span>';
|
|
contentEl.innerHTML = `
|
|
<p class="text-sm text-slate-400 mb-4">ننصح بتفعيل التحقق الثنائي لحماية بياناتك المالية.</p>
|
|
<button onclick="start2FASetup()" class="px-6 py-2 bg-primary hover:bg-primary-dark text-white rounded-xl transition font-bold shadow-lg">تفعيل الآن</button>
|
|
`;
|
|
}
|
|
} catch (err) { console.error(err); }
|
|
}
|
|
|
|
async function start2FASetup() {
|
|
const contentEl = document.getElementById('2fa-content');
|
|
contentEl.innerHTML = '<div class="animate-pulse text-slate-400">جاري إنشاء رمز الأمان...</div>';
|
|
|
|
try {
|
|
const res = await API.post('/auth/2fa/enable', {});
|
|
const { secret, qr_url } = res.data;
|
|
|
|
contentEl.innerHTML = `
|
|
<div class="flex flex-col md:flex-row gap-8 items-center bg-black/20 p-6 rounded-2xl border border-white/5">
|
|
<div class="bg-white p-2 rounded-xl">
|
|
<img src="${qr_url}" alt="QR Code" class="w-40 h-40">
|
|
</div>
|
|
<div class="flex-1 space-y-4">
|
|
<p class="text-sm text-slate-300">1. قم بمسح الرمز أعلاه باستخدام تطبيق Google Authenticator أو ما يماثله.</p>
|
|
<p class="text-sm text-slate-300">2. أدخل الرمز المكون من 6 أرقام للتأكيد:</p>
|
|
<div class="flex gap-2">
|
|
<input type="text" id="2fa-code" maxlength="6" class="bg-black/40 border border-white/10 rounded-xl px-4 py-2 w-32 text-center font-mono text-xl tracking-widest focus:border-primary outline-none" placeholder="000000">
|
|
<button onclick="verify2FA('${secret}')" class="px-6 py-2 bg-primary hover:bg-primary-dark text-white rounded-xl font-bold transition">تأكيد الربط</button>
|
|
</div>
|
|
<p class="text-[10px] text-slate-500">رمز الأمان اليدوي: <code class="bg-black/40 px-2 py-1 rounded">${secret}</code></p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} catch (err) {
|
|
alert('فشل تفعيل التحقق الثنائي');
|
|
update2FAStatus();
|
|
}
|
|
}
|
|
|
|
async function verify2FA(secret) {
|
|
const code = document.getElementById('2fa-code').value;
|
|
if (code.length !== 6) return;
|
|
|
|
try {
|
|
await API.post('/auth/2fa/verify', { secret, code });
|
|
alert('تم التفعيل بنجاح!');
|
|
update2FAStatus();
|
|
} catch (err) {
|
|
alert(err.error?.message_ar || 'الرمز غير صحيح');
|
|
}
|
|
}
|
|
|
|
async function disable2FA() {
|
|
if (!confirm('هل أنت متأكد من تعطيل التحقق الثنائي؟ سيقل أمان حسابك بشكل ملحوظ.')) return;
|
|
try {
|
|
await API.post('/auth/2fa/disable', {});
|
|
update2FAStatus();
|
|
} catch (err) { alert('فشل التعطيل'); }
|
|
}
|
|
|
|
async function loadApiKeys() {
|
|
const listEl = document.getElementById('api-keys-list');
|
|
try {
|
|
const res = await API.get('/api-keys');
|
|
const keys = res.data;
|
|
|
|
if (keys.length === 0) {
|
|
listEl.innerHTML = '<p class="text-slate-500 text-center py-4">لا توجد مفاتيح API حالياً.</p>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = keys.map(k => `
|
|
<div class="flex justify-between items-center p-4 bg-black/20 rounded-2xl border border-white/5">
|
|
<div>
|
|
<p class="font-bold text-slate-200">${k.name}</p>
|
|
<p class="text-xs text-slate-500 font-mono">Prefix: ${k.prefix} • Created: ${new Date(k.created_at).toLocaleDateString()}</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<span class="px-3 py-1 bg-emerald-500/10 text-emerald-400 text-[10px] rounded-full border border-emerald-500/20">Active</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (err) { listEl.innerHTML = '<p class="text-red-400">خطأ في تحميل المفاتيح</p>'; }
|
|
}
|
|
|
|
async function showCreateApiKeyModal() {
|
|
const name = prompt('أدخل اسماً لهذا المفتاح (مثلاً: Flutter App):');
|
|
if (!name) return;
|
|
try {
|
|
const res = await API.post('/api-keys', { name });
|
|
alert(`تم إنشاء المفتاح بنجاح!\n\nمهم جداً: هذا هو المفتاح السري، قم بحفظه الآن لأنه لن يظهر مرة أخرى:\n\n${res.data.key}`);
|
|
loadApiKeys();
|
|
} catch (err) { alert('فشل إنشاء المفتاح'); }
|
|
}
|
|
|
|
// ── Admin Stats View ─────────────────────────────────────
|
|
async function renderAdminStats() {
|
|
document.getElementById('page-title').textContent = 'إدارة المنصة (Super Admin)';
|
|
try {
|
|
const res = await API.get('/admin/stats');
|
|
const stats = res.data;
|
|
|
|
contentDiv.innerHTML = `
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
<div class="glass-panel p-6 rounded-3xl">
|
|
<p class="text-slate-400 text-sm mb-1">إجمالي المستأجرين</p>
|
|
<p class="text-3xl font-black text-primary">${stats.total_tenants}</p>
|
|
</div>
|
|
<div class="glass-panel p-6 rounded-3xl">
|
|
<p class="text-slate-400 text-sm mb-1">إجمالي الفواتير</p>
|
|
<p class="text-3xl font-black text-blue-400">${stats.total_invoices}</p>
|
|
</div>
|
|
<div class="glass-panel p-6 rounded-3xl">
|
|
<p class="text-slate-400 text-sm mb-1">حالة النظام</p>
|
|
<p class="text-xl font-bold ${stats.system_health.redis === 'ok' ? 'text-emerald-400' : 'text-red-400'}">
|
|
Redis: ${stats.system_health.redis.toUpperCase()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="glass-panel p-8 rounded-3xl">
|
|
<h3 class="text-xl font-bold mb-6">إدارة المستأجرين (قريباً)</h3>
|
|
<p class="text-slate-500 text-sm italic">سيتم عرض قائمة الشركات وإحصائيات الاستهلاك لكل منها هنا.</p>
|
|
</div>
|
|
`;
|
|
} catch (err) {
|
|
contentDiv.innerHTML = `<div class="text-red-400 p-8 glass-panel rounded-3xl">عذراً، هذه الصفحة للمشرفين فقط.</div>`;
|
|
}
|
|
}
|
|
|
|
// ── Modals & Actions ─────────────────────────────────────
|
|
function showAddCompanyModal() {
|
|
const modals = document.getElementById('modals');
|
|
modals.innerHTML = `
|
|
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4 overflow-y-auto" id="company-modal">
|
|
<div class="glass-panel p-8 rounded-3xl w-full max-w-lg border border-white/10 shadow-2xl my-auto">
|
|
<h3 class="text-2xl font-bold mb-6">إضافة شركة جديدة</h3>
|
|
<form id="add-company-form" class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<input type="text" id="comp-name" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="الاسم (عربي) *" required>
|
|
<input type="text" id="comp-name-en" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="الاسم (إنجليزي)">
|
|
<input type="text" id="comp-tax" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="الرقم الضريبي *" required>
|
|
<input type="text" id="comp-cr" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="السجل التجاري">
|
|
<input type="text" id="comp-city" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="المدينة">
|
|
<input type="text" id="comp-address" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="العنوان">
|
|
<input type="email" id="comp-email" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="البريد الإلكتروني">
|
|
<input type="text" id="comp-phone" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" placeholder="رقم الهاتف">
|
|
</div>
|
|
<div class="flex gap-3 mt-6 pt-4 border-t border-white/10">
|
|
<button type="button" onclick="document.getElementById('company-modal').remove()" class="flex-1 py-3 bg-white/5 hover:bg-white/10 rounded-xl transition">إلغاء</button>
|
|
<button type="submit" class="flex-1 py-3 bg-primary hover:bg-primary-dark text-white font-bold rounded-xl shadow-lg transition">حفظ الشركة</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('add-company-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
const data = {
|
|
name: document.getElementById('comp-name').value,
|
|
name_en: document.getElementById('comp-name-en').value || null,
|
|
tax_identification_number: document.getElementById('comp-tax').value,
|
|
commercial_registration_number: document.getElementById('comp-cr').value || null,
|
|
city: document.getElementById('comp-city').value || null,
|
|
address: document.getElementById('comp-address').value || null,
|
|
contact_email: document.getElementById('comp-email').value || null,
|
|
contact_phone: document.getElementById('comp-phone').value || null
|
|
};
|
|
await API.post('/companies', data);
|
|
document.getElementById('company-modal').remove();
|
|
renderCompanies();
|
|
} catch(err) {
|
|
alert(err.error?.message_ar || err.error?.details?.message || err.message || 'حدث خطأ');
|
|
}
|
|
};
|
|
}
|
|
|
|
function showJoFotaraModal(companyId) {
|
|
const modals = document.getElementById('modals');
|
|
modals.innerHTML = `
|
|
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" id="jofotara-modal">
|
|
<div class="glass-panel p-8 rounded-3xl w-full max-w-md border border-white/10 shadow-2xl">
|
|
<h3 class="text-2xl font-bold mb-6">إعدادات الربط مع JoFotara</h3>
|
|
<form id="jofotara-form" class="space-y-4">
|
|
<input type="text" id="jf-client-id" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none font-mono" placeholder="Client ID" required>
|
|
<input type="password" id="jf-secret-key" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none font-mono" placeholder="Secret Key" required>
|
|
<div class="flex gap-3 mt-6">
|
|
<button type="button" onclick="document.getElementById('jofotara-modal').remove()" class="flex-1 py-3 bg-white/5 hover:bg-white/10 rounded-xl transition">إلغاء</button>
|
|
<button type="submit" class="flex-1 py-3 bg-primary hover:bg-primary-dark text-white font-bold rounded-xl shadow-lg transition">حفظ الإعدادات</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('jofotara-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
const client_id = document.getElementById('jf-client-id').value;
|
|
const secret_key = document.getElementById('jf-secret-key').value;
|
|
// Note: We need a PUT endpoint to update Jofotara credentials.
|
|
// Our API has: PUT /api/v1/companies/{id}/jofotara
|
|
const res = await fetch(`${API.baseUrl}/companies/${companyId}/jofotara`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${API.accessToken}`
|
|
},
|
|
body: JSON.stringify({ client_id, secret_key })
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw data;
|
|
|
|
document.getElementById('jofotara-modal').remove();
|
|
renderCompanies();
|
|
} catch(err) {
|
|
alert(err.error?.message_ar || err.error?.details?.message || 'حدث خطأ أثناء حفظ الإعدادات');
|
|
}
|
|
};
|
|
}
|
|
|
|
async function showUploadInvoiceModal() {
|
|
try {
|
|
// Fetch companies to populate the select dropdown
|
|
const res = await API.get('/companies');
|
|
const companies = res.data;
|
|
|
|
let optionsHtml = '<option value="" disabled selected>-- اختر الشركة --</option>';
|
|
companies.forEach(c => {
|
|
optionsHtml += `<option value="${c.id}">${c.name} (${c.tax_identification_number || ''})</option>`;
|
|
});
|
|
|
|
const modals = document.getElementById('modals');
|
|
modals.innerHTML = `
|
|
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center" id="invoice-modal">
|
|
<div class="glass-panel p-8 rounded-3xl w-full max-w-md border border-white/10 shadow-2xl">
|
|
<h3 class="text-2xl font-bold mb-6">رفع فاتورة للتدقيق</h3>
|
|
<form id="upload-invoice-form" class="space-y-4" enctype="multipart/form-data">
|
|
<select id="inv-comp-id" class="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-white focus:border-primary outline-none" required>
|
|
${optionsHtml}
|
|
</select>
|
|
<div class="p-4 border-2 border-dashed border-white/20 rounded-xl bg-black/10 text-center">
|
|
<p class="text-sm text-slate-400 mb-2">اختر ملف الفاتورة (JSON/XML/PDF)</p>
|
|
<input type="file" id="inv-file" class="text-sm w-full" required>
|
|
</div>
|
|
<div class="flex gap-3 mt-6">
|
|
<button type="button" onclick="document.getElementById('invoice-modal').remove()" class="flex-1 py-3 bg-white/5 hover:bg-white/10 rounded-xl transition">إلغاء</button>
|
|
<button type="submit" class="flex-1 py-3 bg-primary hover:bg-primary-dark text-white font-bold rounded-xl shadow-lg transition">تحقق ورفع</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('upload-invoice-form').onsubmit = async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
const companyId = document.getElementById('inv-comp-id').value;
|
|
const fileInput = document.getElementById('inv-file');
|
|
|
|
const formData = new FormData();
|
|
formData.append('company_id', companyId);
|
|
formData.append('invoice', fileInput.files[0]);
|
|
|
|
const btn = e.target.querySelector('button[type="submit"]');
|
|
btn.textContent = 'جاري التحقق...';
|
|
btn.disabled = true;
|
|
|
|
await API.upload('/invoices/upload', formData);
|
|
document.getElementById('invoice-modal').remove();
|
|
renderInvoices();
|
|
} catch(err) {
|
|
alert(err.error?.message_ar || 'صيغة البيانات غير صحيحة أو حدث خطأ ضريبي.');
|
|
e.target.querySelector('button[type="submit"]').textContent = 'تحقق ورفع';
|
|
e.target.querySelector('button[type="submit"]').disabled = false;
|
|
}
|
|
};
|
|
} catch(err) {
|
|
alert('خطأ في جلب بيانات الشركات المربوطة بك.');
|
|
}
|
|
}
|
|
|
|
// ── Init Engine ──────────────────────────────────────────
|
|
function initApp() {
|
|
if (isLoggedIn()) {
|
|
document.getElementById('sidebar').classList.remove('hidden');
|
|
document.getElementById('header').classList.remove('hidden');
|
|
document.getElementById('ai-container').classList.remove('hidden');
|
|
document.getElementById('sidebar').classList.add('flex');
|
|
document.getElementById('header').classList.add('flex');
|
|
|
|
// Hide Users menu if not admin or super_admin
|
|
// Nav link visibility based on roles
|
|
const role = localStorage.getItem('user_role');
|
|
const usersNav = document.getElementById('nav-users');
|
|
const adminNav = document.getElementById('nav-admin');
|
|
const riskNav = document.getElementById('nav-risk-monitor');
|
|
|
|
if (usersNav) usersNav.style.display = (role === 'super_admin' || role === 'admin') ? 'flex' : 'none';
|
|
if (adminNav) adminNav.style.display = (role === 'super_admin') ? 'flex' : 'none';
|
|
if (riskNav) riskNav.style.display = (role !== 'employee') ? 'flex' : 'none';
|
|
|
|
// AI Chat Listener
|
|
document.getElementById('ai-query').onkeydown = async (e) => {
|
|
if (e.key === 'Enter') {
|
|
const q = e.target.value;
|
|
if (!q) return;
|
|
e.target.value = '';
|
|
document.getElementById('ai-answer').textContent = 'جاري التحليل...';
|
|
try {
|
|
const res = await API.post('/ai/query', { query: q });
|
|
document.getElementById('ai-answer').textContent = res.data.answer;
|
|
} catch (err) {
|
|
document.getElementById('ai-answer').textContent = 'حدث خطأ في الاتصال بالمساعد الذكي.';
|
|
}
|
|
}
|
|
};
|
|
|
|
navigateTo('dashboard');
|
|
showToast(`مرحباً بك مجدداً، ${localStorage.getItem('user_name') || 'مدير النظام'}`);
|
|
} else {
|
|
renderLogin();
|
|
}
|
|
}
|
|
|
|
// Start
|
|
initApp();
|
|
</script>
|
|
</body>
|
|
</html>
|