Update: 2026-05-04 01:46:58
This commit is contained in:
80
app/modules_app/invoices/index.php
Normal file
80
app/modules_app/invoices/index.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoices List Endpoint (Role-Based & Tenant-Aware)
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
|
||||
// 1. Auth Check
|
||||
$decoded = AuthMiddleware::check();
|
||||
$db = Database::getInstance();
|
||||
|
||||
$tenantId = $decoded['tenant_id'];
|
||||
$userId = $decoded['user_id'];
|
||||
$role = $decoded['role'];
|
||||
|
||||
try {
|
||||
// 2. Build Query based on Role
|
||||
if ($role === 'super_admin') {
|
||||
// Super Admin sees ALL invoices
|
||||
$stmt = $db->query("
|
||||
SELECT i.*, t.name as tenant_name, c.name as company_name
|
||||
FROM invoices i
|
||||
LEFT JOIN tenants t ON i.tenant_id = t.id
|
||||
LEFT JOIN companies c ON i.company_id = c.id
|
||||
ORDER BY i.created_at DESC
|
||||
");
|
||||
} elseif ($role === 'admin') {
|
||||
// Admin sees all invoices in THEIR tenant
|
||||
$stmt = $db->prepare("
|
||||
SELECT i.*, c.name as company_name
|
||||
FROM invoices i
|
||||
LEFT JOIN companies c ON i.company_id = c.id
|
||||
WHERE i.tenant_id = ?
|
||||
ORDER BY i.created_at DESC
|
||||
");
|
||||
$stmt->execute([$tenantId]);
|
||||
} else {
|
||||
// Accountant/Viewer: Filter by assigned companies
|
||||
$stmtUser = $db->prepare("SELECT company_id FROM user_company_assignments WHERE user_id = ? AND is_active = 1");
|
||||
$stmtUser->execute([$userId]);
|
||||
$assignedCompanyIds = $stmtUser->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (empty($assignedCompanyIds)) {
|
||||
json_success([]);
|
||||
}
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($assignedCompanyIds), '?'));
|
||||
$stmt = $db->prepare("
|
||||
SELECT i.*, c.name as company_name
|
||||
FROM invoices i
|
||||
LEFT JOIN companies c ON i.company_id = c.id
|
||||
WHERE i.company_id IN ($placeholders)
|
||||
ORDER BY i.created_at DESC
|
||||
");
|
||||
$stmt->execute($assignedCompanyIds);
|
||||
}
|
||||
|
||||
$invoices = $stmt->fetchAll();
|
||||
|
||||
// 3. Decrypt sensitive fields for display
|
||||
foreach ($invoices as &$inv) {
|
||||
$inv['supplier_name'] = Encryption::decrypt($inv['supplier_name'] ?? '') ?: ($inv['supplier_name'] ?? '-');
|
||||
$inv['supplier_tin'] = Encryption::decrypt($inv['supplier_tin'] ?? '') ?: ($inv['supplier_tin'] ?? '-');
|
||||
$inv['buyer_name'] = Encryption::decrypt($inv['buyer_name'] ?? '') ?: ($inv['buyer_name'] ?? '-');
|
||||
|
||||
if (!empty($inv['company_name'])) {
|
||||
$inv['company_name'] = Encryption::decrypt($inv['company_name']) ?: $inv['company_name'];
|
||||
}
|
||||
if (!empty($inv['tenant_name'])) {
|
||||
$inv['tenant_name'] = Encryption::decrypt($inv['tenant_name']) ?: $inv['tenant_name'];
|
||||
}
|
||||
}
|
||||
|
||||
json_success($invoices);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
json_error('SQL Error in Invoices List: ' . $e->getMessage(), 500);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ $routes = [
|
||||
'v1/users/delete' => ['POST', 'users/delete.php'],
|
||||
'v1/companies' => ['GET', 'companies/index.php'],
|
||||
'v1/companies/create' => ['POST', 'companies/create.php'],
|
||||
'v1/invoices' => ['GET', 'invoices/index.php'],
|
||||
'v1/invoices/upload' => ['POST', 'invoices/upload.php'],
|
||||
'v1/dashboard/stats' => ['GET', 'dashboard/stats.php'],
|
||||
'v1/tenants' => ['GET', 'tenants/index.php'],
|
||||
|
||||
498
public/shell.php
498
public/shell.php
@@ -17,13 +17,14 @@
|
||||
}
|
||||
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"
|
||||
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 = ''">
|
||||
@@ -33,97 +34,118 @@
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-surface border-l border-gray-800 flex flex-col">
|
||||
<div class="p-6">
|
||||
<h1 class="text-xl font-bold text-emerald-500">مُصادَق</h1>
|
||||
<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">
|
||||
<a href="#" @click="page='dashboard'" class="block p-3 rounded hover:bg-gray-800" :class="page==='dashboard'?'bg-emerald-900/20 text-emerald-500':''">📊 لوحة التحكم</a>
|
||||
<a x-show="user?.role === 'super_admin'" href="#" @click="page='tenants'" class="block p-3 rounded hover:bg-gray-800" :class="page==='tenants'?'bg-emerald-900/20 text-emerald-500':''">🏢 المكاتب</a>
|
||||
<a x-show="user?.role !== 'viewer'" href="#" @click="page='companies'" class="block p-3 rounded hover:bg-gray-800" :class="page==='companies'?'bg-emerald-900/20 text-emerald-500':''">🏭 الشركات</a>
|
||||
<a x-show="user?.role === 'super_admin' || user?.role === 'admin'" href="#" @click="page='users'" class="block p-3 rounded hover:bg-gray-800" :class="page==='users'?'bg-emerald-900/20 text-emerald-500':''">👥 المستخدمون</a>
|
||||
<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">
|
||||
<button @click="logout()" class="w-full text-right text-red-400 text-sm">🚪 تسجيل الخروج</button>
|
||||
<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">
|
||||
<header class="mb-10 flex justify-between items-center">
|
||||
<h2 class="text-2xl font-bold" x-text="title()"></h2>
|
||||
<div class="flex items-center gap-4">
|
||||
<button x-show="page==='tenants' && user?.role === 'super_admin'" @click="showAddTenantModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-4 py-2 rounded text-sm font-bold transition">➕ إضافة مكتب</button>
|
||||
<button x-show="page==='users' && (user?.role === 'super_admin' || user?.role === 'admin')" @click="showAddModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-4 py-2 rounded text-sm font-bold transition">➕ إضافة مستخدم</button>
|
||||
<button x-show="page==='companies' && (user?.role === 'super_admin' || user?.role === 'admin')" @click="showAddCompanyModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-4 py-2 rounded text-sm font-bold transition">➕ إضافة شركة</button>
|
||||
<div class="text-sm text-gray-500" x-text="user?.name"></div>
|
||||
<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">
|
||||
<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-6 bg-surface border border-gray-800 rounded">
|
||||
<p class="text-gray-500 text-sm">إجمالي الفواتير</p>
|
||||
<p class="text-3xl font-bold mt-2" x-text="stats.total || 0"></p>
|
||||
<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-6 bg-surface border border-gray-800 rounded">
|
||||
<p class="text-gray-500 text-sm">قيد المعالجة</p>
|
||||
<p class="text-3xl font-bold mt-2 text-yellow-500" x-text="stats.pending || 0"></p>
|
||||
<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-6 bg-surface border border-gray-800 rounded">
|
||||
<p class="text-gray-500 text-sm">تم الاعتماد</p>
|
||||
<p class="text-3xl font-bold mt-2 text-emerald-500" x-text="stats.approved || 0"></p>
|
||||
<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>
|
||||
|
||||
<!-- Tenants List -->
|
||||
<div x-show="page === 'tenants'">
|
||||
<div class="bg-surface border border-gray-800 rounded overflow-hidden">
|
||||
<table class="w-full text-right">
|
||||
<!-- 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-4">اسم المكتب</th>
|
||||
<th class="p-4">البريد الإلكتروني</th>
|
||||
<th class="p-4">الهاتف</th>
|
||||
<th class="p-4">الحالة</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>
|
||||
<th class="p-5 text-xs font-bold text-gray-500 uppercase">إجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr x-show="tenants.length === 0"><td colspan="4" class="p-8 text-center text-gray-600">لا توجد مكاتب بعد</td></tr>
|
||||
<template x-for="t in tenants" :key="t.id">
|
||||
<tr class="border-t border-gray-800">
|
||||
<td class="p-4 font-bold text-emerald-500" x-text="t.name"></td>
|
||||
<td class="p-4" x-text="t.email"></td>
|
||||
<td class="p-4" x-text="t.phone || '-'"></td>
|
||||
<td class="p-4"><span class="px-2 py-1 bg-emerald-900/30 text-emerald-400 rounded text-xs" x-text="t.status"></span></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Companies List -->
|
||||
<div x-show="page === 'companies'">
|
||||
<div class="bg-surface border border-gray-800 rounded overflow-hidden">
|
||||
<table class="w-full text-right">
|
||||
<thead class="bg-gray-900/50">
|
||||
<tr>
|
||||
<th class="p-4">اسم الشركة</th>
|
||||
<th class="p-4">الرقم الضريبي</th>
|
||||
<th class="p-4">رقم التسجيل</th>
|
||||
<th class="p-4">تاريخ الإضافة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr x-show="companies.length === 0"><td colspan="4" class="p-8 text-center text-gray-600">لا توجد شركات بعد</td></tr>
|
||||
<template x-for="c in companies" :key="c.id">
|
||||
<tr class="border-t border-gray-800">
|
||||
<td class="p-4 font-bold text-emerald-500" x-text="c.name"></td>
|
||||
<td class="p-4" x-text="c.tax_identification_number"></td>
|
||||
<td class="p-4" x-text="c.commercial_registration_number"></td>
|
||||
<td class="p-4 text-xs text-gray-500" x-text="c.created_at"></td>
|
||||
<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>
|
||||
@@ -133,24 +155,31 @@
|
||||
|
||||
<!-- Users List -->
|
||||
<div x-show="page === 'users'">
|
||||
<div class="bg-surface border border-gray-800 rounded overflow-hidden">
|
||||
<table class="w-full text-right">
|
||||
<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-4">الاسم</th>
|
||||
<th class="p-4">البريد الإلكتروني</th>
|
||||
<th class="p-4">المكتب</th>
|
||||
<th class="p-4">الدور</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>
|
||||
<tr x-show="users.length === 0"><td colspan="4" class="p-8 text-center text-gray-600">لا يوجد مستخدمون بعد</td></tr>
|
||||
<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="border-t border-gray-800">
|
||||
<td class="p-4" x-text="u.name"></td>
|
||||
<td class="p-4" x-text="u.email"></td>
|
||||
<td class="p-4 text-xs text-gray-400" x-text="u.tenant_name || '-'"></td>
|
||||
<td class="p-4"><span class="px-2 py-1 bg-gray-800 rounded text-xs" x-text="u.role"></span></td>
|
||||
<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>
|
||||
@@ -160,34 +189,74 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Add User Modal -->
|
||||
<!-- 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-lg shadow-2xl" @click.away="showAddModal = false">
|
||||
<h3 class="text-xl font-bold mb-6">إضافة مستخدم جديد 👤</h3>
|
||||
<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-xs text-gray-500 uppercase mb-1">الاسم الكامل</label>
|
||||
<input type="text" x-model="newUser.name" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500" required>
|
||||
<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-xs text-gray-500 uppercase mb-1">البريد الإلكتروني</label>
|
||||
<input type="email" x-model="newUser.email" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500" required>
|
||||
<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-xs text-gray-500 uppercase mb-1">كلمة المرور</label>
|
||||
<input type="password" x-model="newUser.password" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500" required>
|
||||
<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-xs text-gray-500 uppercase mb-1">الدور</label>
|
||||
<select x-model="newUser.role" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
<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-xs text-gray-500 uppercase mb-1">تعيين لمكتب</label>
|
||||
<select x-model="newUser.tenant_id" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
<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>
|
||||
@@ -195,89 +264,13 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="pt-4 flex gap-3">
|
||||
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3 rounded font-bold transition">حفظ المستخدم</button>
|
||||
<button type="button" @click="showAddModal = false" class="px-6 py-3 border border-gray-800 rounded hover:bg-gray-800 transition">إلغاء</button>
|
||||
<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>
|
||||
|
||||
<!-- Add Tenant Modal -->
|
||||
<div x-show="showAddTenantModal" 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 rounded-lg p-6 w-full max-w-md shadow-2xl" @click.away="showAddTenantModal = false">
|
||||
<h3 class="text-xl font-bold mb-6 text-emerald-500">مكتب جديد + مدير</h3>
|
||||
<form @submit.prevent="createTenant" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">اسم المكتب</label>
|
||||
<input type="text" x-model="newTenant.name" required class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">بريد المكتب</label>
|
||||
<input type="email" x-model="newTenant.email" required class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">الهاتف</label>
|
||||
<input type="text" x-model="newTenant.phone" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<hr class="border-gray-800 my-4">
|
||||
<h4 class="text-sm font-bold text-gray-400">معلومات مدير المكتب (Admin)</h4>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">اسم المدير</label>
|
||||
<input type="text" x-model="newTenant.manager_name" required class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">بريد المدير (لتسجيل الدخول)</label>
|
||||
<input type="email" x-model="newTenant.manager_email" required class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">كلمة المرور</label>
|
||||
<input type="password" x-model="newTenant.manager_password" required class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 p-3 rounded font-bold transition">حفظ</button>
|
||||
<button type="button" @click="showAddTenantModal = false" class="flex-1 bg-gray-800 hover:bg-gray-700 p-3 rounded transition">إلغاء</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Company Modal -->
|
||||
<div x-show="showAddCompanyModal" 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-lg shadow-2xl" @click.away="showAddCompanyModal = false">
|
||||
<h3 class="text-xl font-bold mb-6">إنشاء شركة جديدة 🏭</h3>
|
||||
<form @submit.prevent="createCompany" class="space-y-4">
|
||||
<div x-show="user?.role === 'super_admin'">
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">المكتب المحاسبي</label>
|
||||
<select x-model="newCompany.tenant_id" class="w-full bg-gray-950 border border-gray-800 p-3 rounded 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>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">اسم الشركة</label>
|
||||
<input type="text" x-model="newCompany.name" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">الرقم الضريبي</label>
|
||||
<input type="text" x-model="newCompany.tax_identification_number" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">رقم التسجيل</label>
|
||||
<input type="text" x-model="newCompany.commercial_registration_number" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 uppercase mb-1">العنوان</label>
|
||||
<textarea x-model="newCompany.address" class="w-full bg-gray-950 border border-gray-800 p-3 rounded outline-none focus:border-emerald-500"></textarea>
|
||||
</div>
|
||||
<div class="pt-4 flex gap-3">
|
||||
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3 rounded font-bold transition">حفظ الشركة</button>
|
||||
<button type="button" @click="showAddCompanyModal = false" class="px-6 py-3 border border-gray-800 rounded hover:bg-gray-800 transition">إلغاء</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -288,40 +281,46 @@
|
||||
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.loadStats();
|
||||
this.loadCompanies();
|
||||
if (this.user.role === 'super_admin' || this.user.role === 'admin') {
|
||||
this.loadUsers();
|
||||
}
|
||||
if (this.user.role === 'super_admin') {
|
||||
this.loadTenants();
|
||||
}
|
||||
this.loadAll();
|
||||
},
|
||||
|
||||
setPage(p) {
|
||||
this.page = p;
|
||||
this.loadAll();
|
||||
},
|
||||
|
||||
title() {
|
||||
return { dashboard: 'نظرة عامة', users: 'المستخدمون', companies: 'الشركات', tenants: 'المكاتب المحاسبية' }[this.page] || '';
|
||||
return { dashboard: 'نظرة عامة', users: 'إدارة المستخدمين', companies: 'الشركات والعملاء', tenants: 'المكاتب المحاسبية', invoices: 'الفواتير والمستندات' }[this.page] || '';
|
||||
},
|
||||
|
||||
subtitle() {
|
||||
return { dashboard: 'تحليلات المنصة وحالة العمليات الحالية', users: 'إدارة أدوار الوصول والصلاحيات للموظفين', invoices: 'إدارة الفواتير المرفوعة والمعالجة الذكية' }[this.page] || '';
|
||||
},
|
||||
|
||||
showError(msg) {
|
||||
this.globalError = msg;
|
||||
console.error('[مُصادَق Error]', msg);
|
||||
setTimeout(() => this.globalError = '', 6000);
|
||||
},
|
||||
|
||||
token() {
|
||||
return localStorage.getItem('access_token');
|
||||
},
|
||||
token() { return localStorage.getItem('access_token'); },
|
||||
|
||||
async apiGet(route) {
|
||||
try {
|
||||
@@ -330,97 +329,82 @@
|
||||
});
|
||||
if (res.status === 401) { this.logout(); return null; }
|
||||
const json = await res.json();
|
||||
console.log('[API GET]', route, json);
|
||||
if (!json.success) {
|
||||
this.showError(json.message || 'خطأ في جلب البيانات');
|
||||
return null;
|
||||
}
|
||||
if (!json.success) { this.showError(json.message); return null; }
|
||||
return json.data;
|
||||
} catch (e) {
|
||||
this.showError('خطأ في الاتصال: ' + route + ' — ' + e.message);
|
||||
console.error('[API ERROR]', route, e);
|
||||
return null;
|
||||
}
|
||||
} 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()
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.token() },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const json = await res.json();
|
||||
console.log('[API POST]', route, json);
|
||||
if (!json.success) {
|
||||
this.showError(json.message || 'خطأ في حفظ البيانات');
|
||||
}
|
||||
if (!json.success) this.showError(json.message);
|
||||
return json;
|
||||
} catch (e) {
|
||||
this.showError('خطأ في الاتصال: ' + route + ' — ' + e.message);
|
||||
console.error('[API ERROR]', route, e);
|
||||
return { success: false };
|
||||
}
|
||||
} catch (e) { this.showError('خطأ في الاتصال بالسيرفر'); return { success: false }; }
|
||||
},
|
||||
|
||||
async loadUsers() {
|
||||
const data = await this.apiGet('v1/users');
|
||||
if (data) this.users = data;
|
||||
loadAll() {
|
||||
this.loadStats();
|
||||
this.loadCompanies();
|
||||
if (this.page === 'users') this.loadUsers();
|
||||
if (this.page === 'tenants') this.loadTenants();
|
||||
if (this.page === 'invoices') this.loadInvoices();
|
||||
},
|
||||
|
||||
async loadCompanies() {
|
||||
const data = await this.apiGet('v1/companies');
|
||||
if (data) this.companies = data;
|
||||
},
|
||||
|
||||
async loadStats() {
|
||||
const data = await this.apiGet('v1/dashboard/stats');
|
||||
if (data) this.stats = data;
|
||||
},
|
||||
|
||||
async loadTenants() {
|
||||
const data = await this.apiGet('v1/tenants');
|
||||
if (data) this.tenants = data;
|
||||
},
|
||||
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 json = await this.apiPost('v1/users/create', this.newUser);
|
||||
if (json.success) {
|
||||
this.showAddModal = false;
|
||||
this.newUser = { name: '', email: '', password: '', role: 'employee', tenant_id: '' };
|
||||
this.loadUsers();
|
||||
alert('تم إضافة المستخدم بنجاح');
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
async createCompany() {
|
||||
const json = await this.apiPost('v1/companies/create', this.newCompany);
|
||||
if (json.success) {
|
||||
this.showAddCompanyModal = false;
|
||||
this.newCompany = { name: '', tax_identification_number: '', commercial_registration_number: '', address: '', tenant_id: '' };
|
||||
this.loadCompanies();
|
||||
alert('تم إنشاء الشركة بنجاح');
|
||||
}
|
||||
},
|
||||
|
||||
async createTenant() {
|
||||
const json = await this.apiPost('v1/tenants/create', this.newTenant);
|
||||
if (json.success) {
|
||||
this.showAddTenantModal = false;
|
||||
this.newTenant = { name: '', email: '', phone: '', manager_name: '', manager_email: '', manager_password: '' };
|
||||
this.loadTenants();
|
||||
this.loadUsers();
|
||||
alert('تم إنشاء المكتب وتعيين المدير بنجاح!');
|
||||
}
|
||||
},
|
||||
|
||||
logout() {
|
||||
localStorage.clear();
|
||||
window.location.href = '/login.php';
|
||||
}
|
||||
logout() { localStorage.clear(); window.location.href = '/login.php'; }
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user