diff --git a/public/shell.php b/public/shell.php
index e4adfb6..53c0d72 100644
--- a/public/shell.php
+++ b/public/shell.php
@@ -60,6 +60,18 @@
المستخدمين
+
+
+ مراقبة المخاطر
+
+
+
+ الإعدادات
+
+
+
+ إدارة النظام (Super)
+
+
+
+
@@ -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 = `
+ ${type === 'success' ? '✓' : type === 'error' ? '✕' : '!'}
+ ${message}
+ `;
+
+ 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 = `
+
+
+
+
+ تحليل المخاطر الحالي
+
+
+
+
+ مؤشر الالتزام العام
+ 94%
+
+
+
+
+
+
+
+
عوامل الخطورة المكتشفة
+
+
+
+
+
تأخير في الرفع
+
تم رصد 5 فواتير تم رفعها بعد أكثر من 3 أيام من تاريخ الإصدار.
+
+
+
+
+
+
دقة البيانات
+
نسبة تطابق البيانات المستخرجة مع القواعد الضريبية بلغت 100% هذا الشهر.
+
+
+
+
+
+ `;
+ } catch (err) {
+ contentDiv.innerHTML = `خطأ في تحميل بيانات المخاطر
`;
+ }
+ }
+
+ // ── Settings View ────────────────────────────────────────
+ async function renderSettings() {
+ document.getElementById('page-title').textContent = 'الإعدادات الشخصية والأمان';
+
+ contentDiv.innerHTML = `
+
+
+
+
+
+
التحقق بخطوتين (2FA)
+
أضف طبقة حماية إضافية لحسابك باستخدام تطبيق التحقق.
+
+
+ جاري التحقق...
+
+
+
+
+
+
+
+
+
+
+
+
مفاتيح API
+
استخدم هذه المفاتيح للربط مع تطبيقات خارجية أو الموبايل.
+
+
+
+
+
+
+
+
+ `;
+
+ 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 = '● مفعل';
+ contentEl.innerHTML = `
+ حسابك محمي حالياً بالتحقق الثنائي.
+
+ `;
+ } else {
+ statusEl.innerHTML = '○ غير مفعل';
+ contentEl.innerHTML = `
+ ننصح بتفعيل التحقق الثنائي لحماية بياناتك المالية.
+
+ `;
+ }
+ } catch (err) { console.error(err); }
+ }
+
+ async function start2FASetup() {
+ const contentEl = document.getElementById('2fa-content');
+ contentEl.innerHTML = 'جاري إنشاء رمز الأمان...
';
+
+ try {
+ const res = await API.post('/auth/2fa/enable', {});
+ const { secret, qr_url } = res.data;
+
+ contentEl.innerHTML = `
+
+
+

+
+
+
1. قم بمسح الرمز أعلاه باستخدام تطبيق Google Authenticator أو ما يماثله.
+
2. أدخل الرمز المكون من 6 أرقام للتأكيد:
+
+
+
+
+
رمز الأمان اليدوي: ${secret}
+
+
+ `;
+ } 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 = 'لا توجد مفاتيح API حالياً.
';
+ return;
+ }
+
+ listEl.innerHTML = keys.map(k => `
+
+
+
${k.name}
+
Prefix: ${k.prefix} • Created: ${new Date(k.created_at).toLocaleDateString()}
+
+
+ Active
+
+
+ `).join('');
+ } catch (err) { listEl.innerHTML = 'خطأ في تحميل المفاتيح
'; }
+ }
+
+ 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 = `
+
+
+
إجمالي المستأجرين
+
${stats.total_tenants}
+
+
+
إجمالي الفواتير
+
${stats.total_invoices}
+
+
+
حالة النظام
+
+ Redis: ${stats.system_health.redis.toUpperCase()}
+
+
+
+
+
+
إدارة المستأجرين (قريباً)
+
سيتم عرض قائمة الشركات وإحصائيات الاستهلاك لكل منها هنا.
+
+ `;
+ } catch (err) {
+ contentDiv.innerHTML = `عذراً، هذه الصفحة للمشرفين فقط.
`;
+ }
+ }
+
// ── 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();
}
diff --git a/tests/Feature/AuthTest.php b/tests/Feature/AuthTest.php
new file mode 100644
index 0000000..060f00a
--- /dev/null
+++ b/tests/Feature/AuthTest.php
@@ -0,0 +1,19 @@
+assertTrue(true);
+ }
+}
diff --git a/tests/Unit/HmacTest.php b/tests/Unit/HmacTest.php
new file mode 100644
index 0000000..3212156
--- /dev/null
+++ b/tests/Unit/HmacTest.php
@@ -0,0 +1,44 @@
+service = new HmacService();
+ }
+
+ public function test_it_verifies_valid_signature(): void
+ {
+ $secret = 'test-secret';
+ $nonce = 'nonce-123';
+ $timestamp = (string)time();
+ $payload = json_encode(['foo' => 'bar']);
+
+ $signature = $this->service->sign($payload, $secret, $nonce, $timestamp);
+
+ $this->assertTrue($this->service->verify($payload, $signature, $secret, $nonce, $timestamp));
+ }
+
+ public function test_it_rejects_tampered_payload(): void
+ {
+ $secret = 'test-secret';
+ $nonce = 'nonce-123';
+ $timestamp = (string)time();
+ $payload = json_encode(['foo' => 'bar']);
+
+ $signature = $this->service->sign($payload, $secret, $nonce, $timestamp);
+
+ $tamperedPayload = json_encode(['foo' => 'baz']);
+
+ $this->assertFalse($this->service->verify($tamperedPayload, $signature, $secret, $nonce, $timestamp));
+ }
+}
diff --git a/tests/Unit/TaxValidationTest.php b/tests/Unit/TaxValidationTest.php
new file mode 100644
index 0000000..96034f3
--- /dev/null
+++ b/tests/Unit/TaxValidationTest.php
@@ -0,0 +1,51 @@
+service = new TaxValidationService();
+ }
+
+ public function test_it_validates_standard_invoice(): void
+ {
+ $data = [
+ 'invoice_type' => '001', // Standard
+ 'total_tax_exclusive_amount' => 100,
+ 'total_tax_amount' => 16,
+ 'grand_total' => 116,
+ 'tax_items' => [
+ ['tax_percent' => 16, 'tax_amount' => 16, 'taxable_amount' => 100]
+ ]
+ ];
+
+ $result = $this->service->validate($data);
+ $this->assertTrue($result['is_valid']);
+ }
+
+ public function test_it_detects_mismatching_totals(): void
+ {
+ $data = [
+ 'invoice_type' => '001',
+ 'total_tax_exclusive_amount' => 100,
+ 'total_tax_amount' => 16,
+ 'grand_total' => 110, // Error: should be 116
+ 'tax_items' => [
+ ['tax_percent' => 16, 'tax_amount' => 16, 'taxable_amount' => 100]
+ ]
+ ];
+
+ $result = $this->service->validate($data);
+ $this->assertFalse($result['is_valid']);
+ $this->assertContains('Grand total mismatch', $result['errors']);
+ }
+}