🚀 مُصادَق: تحديث برمجي جديد 2026-05-03 13:45
This commit is contained in:
302
public/shell.php
302
public/shell.php
@@ -60,6 +60,18 @@
|
||||
<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">
|
||||
@@ -107,6 +119,9 @@
|
||||
</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>
|
||||
|
||||
@@ -148,6 +163,32 @@
|
||||
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');
|
||||
@@ -166,6 +207,9 @@
|
||||
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 ───────────────────────────────────────────
|
||||
@@ -733,6 +777,251 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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');
|
||||
@@ -898,13 +1187,15 @@
|
||||
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');
|
||||
if (role !== 'super_admin' && role !== 'admin') {
|
||||
if (usersNav) usersNav.style.display = 'none';
|
||||
} else {
|
||||
if (usersNav) usersNav.style.display = 'flex';
|
||||
}
|
||||
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) => {
|
||||
@@ -923,6 +1214,7 @@
|
||||
};
|
||||
|
||||
navigateTo('dashboard');
|
||||
showToast(`مرحباً بك مجدداً، ${localStorage.getItem('user_name') || 'مدير النظام'}`);
|
||||
} else {
|
||||
renderLogin();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user