Files
musadaq-saas/public/shell.php
2026-05-04 01:46:58 +03:00

413 lines
28 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
:root {
--emerald: #10b981;
--bg-base: #080c14;
--bg-surface: #0d1424;
--border-default: rgba(255,255,255,0.1);
--text-primary: #f0f6fc;
}
body { font-family: 'IBM Plex Sans Arabic', sans-serif; background-color: var(--bg-base); color: var(--text-primary); }
[x-cloak] { display: none !important; }
.glass { background: rgba(13, 20, 36, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.05); }
</style>
</head>
<body x-data="app" x-init="init()">
<!-- Global Error Toast -->
<div x-show="globalError" x-cloak
class="fixed top-4 left-1/2 -translate-x-1/2 z-[100] bg-red-900/90 border border-red-500 text-white px-6 py-3 rounded-lg shadow-2xl max-w-lg text-center glass"
x-text="globalError"
x-transition
@click="globalError = ''">
</div>
<div class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<aside class="w-64 bg-surface border-l border-gray-800 flex flex-col">
<div class="p-6">
<h1 class="text-2xl font-bold text-emerald-500 tracking-tight">مُصادَق</h1>
<p class="text-[10px] text-gray-500 mt-1 uppercase tracking-widest">Enterprise SaaS</p>
</div>
<nav class="flex-1 px-4 space-y-2 mt-4">
<a href="#" @click="setPage('dashboard')" class="flex items-center gap-3 p-3 rounded-lg transition-all" :class="page==='dashboard'?'bg-emerald-500/10 text-emerald-500 shadow-[inset_0_0_20px_rgba(16,185,129,0.05)]':'text-gray-400 hover:bg-gray-800'">
<span class="text-xl">📊</span>
<span class="font-medium">لوحة التحكم</span>
</a>
<a x-show="user?.role === 'super_admin'" href="#" @click="setPage('tenants')" class="flex items-center gap-3 p-3 rounded-lg transition-all" :class="page==='tenants'?'bg-emerald-500/10 text-emerald-500':'text-gray-400 hover:bg-gray-800'">
<span class="text-xl">🏢</span>
<span class="font-medium">المكاتب المحاسبية</span>
</a>
<a x-show="user?.role !== 'viewer'" href="#" @click="setPage('companies')" class="flex items-center gap-3 p-3 rounded-lg transition-all" :class="page==='companies'?'bg-emerald-500/10 text-emerald-500':'text-gray-400 hover:bg-gray-800'">
<span class="text-xl">🏭</span>
<span class="font-medium">الشركات</span>
</a>
<a x-show="user?.role !== 'viewer'" href="#" @click="setPage('invoices')" class="flex items-center gap-3 p-3 rounded-lg transition-all" :class="page==='invoices'?'bg-emerald-500/10 text-emerald-500':'text-gray-400 hover:bg-gray-800'">
<span class="text-xl">📄</span>
<span class="font-medium">الفواتير</span>
</a>
<a x-show="user?.role === 'super_admin' || user?.role === 'admin'" href="#" @click="setPage('users')" class="flex items-center gap-3 p-3 rounded-lg transition-all" :class="page==='users'?'bg-emerald-500/10 text-emerald-500':'text-gray-400 hover:bg-gray-800'">
<span class="text-xl">👥</span>
<span class="font-medium">المستخدمون</span>
</a>
</nav>
<div class="p-6 border-t border-gray-800">
<div class="flex items-center gap-3 mb-4">
<div class="w-8 h-8 rounded-full bg-emerald-500/20 flex items-center justify-center text-emerald-500 font-bold text-xs" x-text="user?.name?.[0]"></div>
<div class="truncate">
<p class="text-xs font-bold truncate" x-text="user?.name"></p>
<p class="text-[10px] text-gray-500 uppercase" x-text="user?.role"></p>
</div>
</div>
<button @click="logout()" class="w-full text-right text-red-400 text-xs font-bold hover:text-red-300 transition">🚪 تسجيل الخروج</button>
</div>
</aside>
<!-- Main -->
<main class="flex-1 overflow-y-auto p-10 bg-[#060910]">
<header class="mb-10 flex justify-between items-end">
<div>
<h2 class="text-3xl font-bold tracking-tight" x-text="title()"></h2>
<p class="text-gray-500 mt-1 text-sm" x-text="subtitle()"></p>
</div>
<div class="flex items-center gap-3">
<button x-show="page==='tenants'" @click="showAddTenantModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-5 py-2.5 rounded-lg text-sm font-bold shadow-lg shadow-emerald-900/20 transition-all active:scale-95"> إضافة مكتب</button>
<button x-show="page==='users'" @click="showAddModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-5 py-2.5 rounded-lg text-sm font-bold shadow-lg shadow-emerald-900/20 transition-all active:scale-95"> إضافة مستخدم</button>
<button x-show="page==='companies'" @click="showAddCompanyModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-5 py-2.5 rounded-lg text-sm font-bold shadow-lg shadow-emerald-900/20 transition-all active:scale-95"> إضافة شركة</button>
<button x-show="page==='invoices'" @click="showUploadModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-5 py-2.5 rounded-lg text-sm font-bold shadow-lg shadow-emerald-900/20 transition-all active:scale-95">📤 رفع فواتير</button>
</div>
</header>
<div id="content" x-transition>
<!-- Dashboard -->
<div x-show="page === 'dashboard'">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="p-8 bg-surface border border-gray-800 rounded-2xl shadow-xl relative overflow-hidden group">
<div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity"><span class="text-4xl">📄</span></div>
<p class="text-gray-500 text-xs uppercase font-bold tracking-widest">إجمالي الفواتير</p>
<p class="text-4xl font-bold mt-3" x-text="stats.total || 0"></p>
</div>
<div class="p-8 bg-surface border border-gray-800 rounded-2xl shadow-xl relative overflow-hidden group">
<div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity"><span class="text-4xl text-yellow-500"></span></div>
<p class="text-gray-500 text-xs uppercase font-bold tracking-widest">قيد المعالجة</p>
<p class="text-4xl font-bold mt-3 text-yellow-500" x-text="stats.pending || 0"></p>
</div>
<div class="p-8 bg-surface border border-gray-800 rounded-2xl shadow-xl relative overflow-hidden group">
<div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity"><span class="text-4xl text-emerald-500"></span></div>
<p class="text-gray-500 text-xs uppercase font-bold tracking-widest">تم الاعتماد</p>
<p class="text-4xl font-bold mt-3 text-emerald-500" x-text="stats.approved || 0"></p>
</div>
</div>
</div>
<!-- Invoices -->
<div x-show="page === 'invoices'">
<div class="bg-surface border border-gray-800 rounded-2xl overflow-hidden shadow-2xl">
<table class="w-full text-right border-collapse">
<thead class="bg-gray-900/50">
<tr>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">الشركة</th>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">المورد</th>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">التاريخ</th>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">المجموع</th>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">الحالة</th>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">إجراءات</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
<tr x-show="invoices.length === 0"><td colspan="6" class="p-12 text-center text-gray-600">لا توجد فواتير بعد</td></tr>
<template x-for="inv in invoices" :key="inv.id">
<tr class="hover:bg-white/[0.02] transition-colors group">
<td class="p-5">
<p class="font-bold text-emerald-500" x-text="inv.company_name"></p>
</td>
<td class="p-5">
<p class="text-sm font-medium" x-text="inv.supplier_name"></p>
<p class="text-[10px] text-gray-500 mt-1" x-text="inv.supplier_tin"></p>
</td>
<td class="p-5 text-sm text-gray-400" x-text="inv.invoice_date || '-'"></td>
<td class="p-5">
<span class="font-mono text-sm" x-text="parseFloat(inv.grand_total).toLocaleString()"></span>
<span class="text-[10px] text-gray-600 mr-1">JOD</span>
</td>
<td class="p-5">
<span class="px-2 py-1 rounded-md text-[10px] font-bold uppercase"
:class="inv.status==='extracted'?'bg-blue-900/30 text-blue-400':(inv.status==='approved'?'bg-emerald-900/30 text-emerald-400':'bg-gray-800 text-gray-400')"
x-text="inv.status"></span>
</td>
<td class="p-5">
<button @click="viewInvoice(inv)" class="text-gray-500 hover:text-white p-2 rounded-lg hover:bg-gray-800 transition">👁️</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Users List -->
<div x-show="page === 'users'">
<div class="bg-surface border border-gray-800 rounded-2xl overflow-hidden shadow-2xl">
<table class="w-full text-right divide-y divide-gray-800">
<thead class="bg-gray-900/50">
<tr>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">المستخدم</th>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">المكتب</th>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">الدور</th>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">الحالة</th>
<th class="p-5 text-xs font-bold text-gray-500 uppercase">إجراءات</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
<tr x-show="users.length === 0"><td colspan="5" class="p-12 text-center text-gray-600">لا يوجد مستخدمون بعد</td></tr>
<template x-for="u in users" :key="u.id">
<tr class="hover:bg-white/[0.01] transition-colors group">
<td class="p-5">
<p class="font-bold text-emerald-500" x-text="u.name"></p>
<p class="text-xs text-gray-500" x-text="u.email"></p>
</td>
<td class="p-5 text-sm text-gray-400" x-text="u.tenant_name || '-'"></td>
<td class="p-5"><span class="px-2 py-1 bg-gray-800 rounded text-[10px] font-bold uppercase" x-text="u.role"></span></td>
<td class="p-5"><span class="w-2 h-2 rounded-full inline-block" :class="u.is_active?'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]':'bg-red-500'"></span></td>
<td class="p-5 flex gap-2">
<button x-show="u.id !== user.id" @click="confirmDeleteUser(u)" class="text-gray-500 hover:text-red-500 p-2 rounded-lg hover:bg-red-500/10 transition">🗑️</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- Upload Invoice Modal -->
<div x-show="showUploadModal" x-cloak class="fixed inset-0 bg-black/90 backdrop-blur-md flex items-center justify-center p-4 z-50">
<div class="bg-surface border border-gray-800 w-full max-w-lg p-10 rounded-3xl shadow-2xl glass" @click.away="showUploadModal = false">
<h3 class="text-2xl font-bold mb-2">رفع فواتير جديدة 📤</h3>
<p class="text-gray-500 text-sm mb-8">سيقوم النظام باستخراج البيانات آلياً باستخدام الذكاء الاصطناعي</p>
<form @submit.prevent="uploadInvoice" class="space-y-6">
<div>
<label class="block text-xs font-bold text-gray-500 uppercase mb-2 tracking-widest">اختر الشركة</label>
<select x-model="uploadData.company_id" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-xl outline-none focus:ring-2 focus:ring-emerald-500/20 transition-all" required>
<option value="">-- اختر الشركة --</option>
<template x-for="c in companies" :key="c.id">
<option :value="c.id" x-text="c.name"></option>
</template>
</select>
</div>
<div class="border-2 border-dashed border-gray-800 rounded-2xl p-12 text-center hover:border-emerald-500/50 transition-colors relative cursor-pointer group">
<input type="file" @change="handleFile" class="absolute inset-0 opacity-0 cursor-pointer" required>
<div class="space-y-2">
<span class="text-4xl block group-hover:scale-110 transition-transform">📄</span>
<p class="text-sm font-bold text-gray-400" x-text="selectedFile ? selectedFile.name : 'اسحب الملف هنا أو اضغط للاختيار'"></p>
<p class="text-[10px] text-gray-600 uppercase">PDF, PNG, JPG (Max 5MB)</p>
</div>
</div>
<div class="pt-4 flex gap-4">
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-4 rounded-xl font-bold shadow-lg shadow-emerald-900/20 transition-all active:scale-95 disabled:opacity-50" :disabled="isUploading">
<span x-show="!isUploading">بدء المعالجة الذكية</span>
<span x-show="isUploading" class="flex items-center justify-center gap-2">
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
جارِ التحليل...
</span>
</button>
<button type="button" @click="showUploadModal = false" class="px-8 py-4 border border-gray-800 rounded-xl hover:bg-gray-800 transition-all">إلغاء</button>
</div>
</form>
</div>
</div>
<!-- Add User Modal (Simplified for turn) -->
<div x-show="showAddModal" x-cloak class="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50">
<div class="bg-surface border border-gray-800 w-full max-w-md p-8 rounded-3xl shadow-2xl glass" @click.away="showAddModal = false">
<h3 class="text-xl font-bold mb-6">إضافة مستخدم جديد 👥</h3>
<form @submit.prevent="createUser" class="space-y-4">
<div>
<label class="block text-[10px] font-bold text-gray-500 uppercase mb-1 tracking-widest">الاسم الكامل</label>
<input type="text" x-model="newUser.name" class="w-full bg-gray-950 border border-gray-800 p-3 rounded-xl outline-none focus:border-emerald-500" required>
</div>
<div>
<label class="block text-[10px] font-bold text-gray-500 uppercase mb-1 tracking-widest">البريد الإلكتروني</label>
<input type="email" x-model="newUser.email" class="w-full bg-gray-950 border border-gray-800 p-3 rounded-xl outline-none focus:border-emerald-500" required>
</div>
<div>
<label class="block text-[10px] font-bold text-gray-500 uppercase mb-1 tracking-widest">كلمة المرور</label>
<input type="password" x-model="newUser.password" class="w-full bg-gray-950 border border-gray-800 p-3 rounded-xl outline-none focus:border-emerald-500" required>
</div>
<div>
<label class="block text-[10px] font-bold text-gray-500 uppercase mb-1 tracking-widest">الدور</label>
<select x-model="newUser.role" class="w-full bg-gray-950 border border-gray-800 p-3 rounded-xl outline-none focus:border-emerald-500">
<option value="employee">موظف</option>
<option value="accountant">محاسب</option>
<option value="admin">مدير مكتب</option>
</select>
</div>
<div x-show="user?.role === 'super_admin'">
<label class="block text-[10px] font-bold text-gray-500 uppercase mb-1 tracking-widest">تعيين لمكتب</label>
<select x-model="newUser.tenant_id" class="w-full bg-gray-950 border border-gray-800 p-3 rounded-xl outline-none focus:border-emerald-500">
<option value="">-- اختر المكتب --</option>
<template x-for="t in tenants" :key="t.id">
<option :value="t.id" x-text="t.name"></option>
</template>
</select>
</div>
<div class="pt-4 flex gap-3">
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3.5 rounded-xl font-bold transition">حفظ المستخدم</button>
<button type="button" @click="showAddModal = false" class="px-6 py-3.5 border border-gray-800 rounded-xl hover:bg-gray-800 transition">إلغاء</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('app', () => ({
user: JSON.parse(localStorage.getItem('user')),
page: 'dashboard',
users: [],
companies: [],
tenants: [],
invoices: [],
stats: { total: 0, pending: 0, approved: 0 },
showAddModal: false,
showAddCompanyModal: false,
showAddTenantModal: false,
showUploadModal: false,
isUploading: false,
globalError: '',
newUser: { name: '', email: '', password: '', role: 'employee', tenant_id: '' },
newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '', tenant_id: '' },
newTenant: { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' },
uploadData: { company_id: '' },
selectedFile: null,
init() {
if (!this.user) { window.location.href = '/login.php'; return; }
this.loadAll();
},
setPage(p) {
this.page = p;
this.loadAll();
},
title() {
return { dashboard: 'نظرة عامة', users: 'إدارة المستخدمين', companies: 'الشركات والعملاء', tenants: 'المكاتب المحاسبية', invoices: 'الفواتير والمستندات' }[this.page] || '';
},
subtitle() {
return { dashboard: 'تحليلات المنصة وحالة العمليات الحالية', users: 'إدارة أدوار الوصول والصلاحيات للموظفين', invoices: 'إدارة الفواتير المرفوعة والمعالجة الذكية' }[this.page] || '';
},
showError(msg) {
this.globalError = msg;
setTimeout(() => this.globalError = '', 6000);
},
token() { return localStorage.getItem('access_token'); },
async apiGet(route) {
try {
const res = await fetch('/index.php?route=' + route, {
headers: { 'Authorization': 'Bearer ' + this.token() }
});
if (res.status === 401) { this.logout(); return null; }
const json = await res.json();
if (!json.success) { this.showError(json.message); return null; }
return json.data;
} catch (e) { this.showError('خطأ في الاتصال بالسيرفر'); return null; }
},
async apiPost(route, body) {
try {
const res = await fetch('/index.php?route=' + route, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.token() },
body: JSON.stringify(body)
});
const json = await res.json();
if (!json.success) this.showError(json.message);
return json;
} catch (e) { this.showError('خطأ في الاتصال بالسيرفر'); return { success: false }; }
},
loadAll() {
this.loadStats();
this.loadCompanies();
if (this.page === 'users') this.loadUsers();
if (this.page === 'tenants') this.loadTenants();
if (this.page === 'invoices') this.loadInvoices();
},
async loadUsers() { const d = await this.apiGet('v1/users'); if(d) this.users = d; },
async loadCompanies() { const d = await this.apiGet('v1/companies'); if(d) this.companies = d; },
async loadInvoices() { const d = await this.apiGet('v1/invoices'); if(d) this.invoices = d; },
async loadStats() { const d = await this.apiGet('v1/dashboard/stats'); if(d) this.stats = d; },
async loadTenants() { const d = await this.apiGet('v1/tenants'); if(d) this.tenants = d; },
async createUser() {
const res = await this.apiPost('v1/users/create', this.newUser);
if (res.success) { this.showAddModal = false; this.newUser = { name: '', email: '', password: '', role: 'employee', tenant_id: '' }; this.loadUsers(); }
},
async confirmDeleteUser(u) {
if (!confirm(`هل أنت متأكد من حذف المستخدم ${u.name}؟`)) return;
const res = await this.apiPost('v1/users/delete', { id: u.id });
if (res.success) this.loadUsers();
},
handleFile(e) { this.selectedFile = e.target.files[0]; },
async uploadInvoice() {
if (!this.selectedFile) return alert('الرجاء اختيار ملف');
this.isUploading = true;
const formData = new FormData();
formData.append('company_id', this.uploadData.company_id);
formData.append('invoice', this.selectedFile);
try {
const res = await fetch('/index.php?route=v1/invoices/upload', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + this.token() },
body: formData
});
const json = await res.json();
if (json.success) {
this.showUploadModal = false;
this.selectedFile = null;
this.loadInvoices();
alert('تم الرفع والمعالجة بنجاح');
} else {
this.showError(json.message);
}
} catch (e) {
this.showError('خطأ أثناء الرفع');
} finally {
this.isUploading = false;
}
},
logout() { localStorage.clear(); window.location.href = '/login.php'; }
}));
});
</script>
</body>
</html>