Update: 2026-05-04 20:03:11

This commit is contained in:
Hamza-Ayed
2026-05-04 20:03:11 +03:00
parent 691305340a
commit 3ea64d59ce
6 changed files with 243 additions and 350 deletions

View File

@@ -10,7 +10,7 @@ use App\Middleware\AuthMiddleware;
// 1. Auth Check
$decoded = AuthMiddleware::check();
if (!in_array($decoded['role'], [ 'super_admin'])) {
if (!in_array($decoded['role'], ['super_admin', 'admin'])) {
json_error('Unauthorized to modify JoFotara settings', 403);
}

View File

@@ -28,27 +28,27 @@ try {
}
// 3. Decrypt fields
foreach ($companies as &$company) {
// Decrypt Name
$decryptedName = Encryption::decrypt($company['name']);
$company['name'] = $decryptedName !== false ? $decryptedName : $company['name'];
$dec = function($val) {
if (empty($val)) return '';
$result = \App\Core\Encryption::decrypt((string)$val);
return ($result !== false && $result !== null && $result !== '') ? $result : (string)$val;
};
// Decrypt Name EN
foreach ($companies as &$company) {
$company['name'] = $dec($company['name']);
if (!empty($company['name_en'])) {
$decryptedNameEn = Encryption::decrypt($company['name_en']);
$company['name_en'] = $decryptedNameEn !== false ? $decryptedNameEn : $company['name_en'];
$company['name_en'] = $dec($company['name_en']);
}
// Redact JoFotara secrets if returned to UI (or just don't return them)
unset($company['jofotara_client_id_encrypted']);
if (isset($company['tenant_name'])) {
$company['tenant_name'] = $dec($company['tenant_name']);
}
// Redact JoFotara secrets
$company['jofotara_client_id_encrypted'] = !empty($company['jofotara_client_id_encrypted']);
unset($company['jofotara_secret_key_encrypted']);
unset($company['certificate_password_encrypted']);
// Decrypt Tenant Name (if exists)
if (isset($company['tenant_name'])) {
$decTenantName = Encryption::decrypt($company['tenant_name']);
$company['tenant_name'] = $decTenantName !== false ? $decTenantName : $company['tenant_name'];
}
}
json_success($companies);

View File

@@ -4,6 +4,7 @@
*/
use App\Core\Database;
use App\Core\Encryption;
use App\Middleware\AuthMiddleware;
// 1. Auth Check
@@ -14,25 +15,39 @@ $companyId = $_GET['id'] ?? null;
if (!$companyId) json_error('Company ID is required', 422);
$tenantId = $decoded['tenant_id'];
$role = $decoded['role'];
try {
// 2. Permission Check
$stmt = $db->prepare("SELECT id, name, tax_identification_number, is_active,
(jofotara_client_id_encrypted IS NOT NULL) as is_jofotara_connected,
jofotara_income_source_sequence
FROM companies WHERE id = ? AND tenant_id = ?");
$stmt->execute([$companyId, $tenantId]);
if ($role === 'super_admin') {
$stmt = $db->prepare("SELECT id, name, tax_identification_number, is_active,
(jofotara_client_id_encrypted IS NOT NULL) as is_jofotara_connected,
jofotara_income_source_sequence
FROM companies WHERE id = ?");
$stmt->execute([$companyId]);
} else {
$stmt = $db->prepare("SELECT id, name, tax_identification_number, is_active,
(jofotara_client_id_encrypted IS NOT NULL) as is_jofotara_connected,
jofotara_income_source_sequence
FROM companies WHERE id = ? AND tenant_id = ?");
$stmt->execute([$companyId, $tenantId]);
}
$company = $stmt->fetch();
if (!$company) json_error('Company not found', 404);
// 3. Monthly Invoice Stats
// Decrypt company name
$dec = Encryption::decrypt($company['name']);
$company['name'] = ($dec !== false && $dec !== '') ? $dec : $company['name'];
// 3. Monthly Invoice Stats (including tax)
$stmtStats = $db->prepare("
SELECT
DATE_FORMAT(invoice_date, '%Y-%m') as month,
COUNT(*) as total_invoices,
SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as approved_count,
SUM(grand_total) as total_amount
COALESCE(SUM(grand_total), 0) as total_amount,
COALESCE(SUM(tax_amount), 0) as total_tax
FROM invoices
WHERE company_id = ? AND deleted_at IS NULL
GROUP BY month
@@ -46,8 +61,8 @@ try {
$stmtTotals = $db->prepare("
SELECT
COUNT(*) as total_invoices,
SUM(grand_total) as total_amount,
SUM(tax_amount) as total_tax,
COALESCE(SUM(grand_total), 0) as total_amount,
COALESCE(SUM(tax_amount), 0) as total_tax,
SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as approved_count
FROM invoices
WHERE company_id = ? AND deleted_at IS NULL

View File

@@ -62,7 +62,7 @@ try {
// 3. Decrypt sensitive fields for display (Robustly)
$dec = function($val) {
if (empty($val)) return '';
$result = Encryption::decrypt((string)$val);
$result = \App\Core\Encryption::decrypt((string)$val);
return ($result !== false && $result !== null && $result !== '') ? $result : (string)$val;
};
@@ -71,9 +71,16 @@ try {
$inv['supplier_tin'] = $dec($inv['supplier_tin']);
$inv['buyer_name'] = $dec($inv['buyer_name']);
// Note: company_name and tenant_name from JOIN are usually plaintext
// Only decrypt if you are absolutely sure they are encrypted in the source table.
// For companies.name, it's plaintext.
if (!empty($inv['company_name'])) {
$inv['company_name'] = $dec($inv['company_name']);
}
if (!empty($inv['tenant_name'])) {
$inv['tenant_name'] = $dec($inv['tenant_name']);
}
}
if (empty($invoices)) {
error_log("INVOICES LIST: No invoices found for role: $role, tenant_id: $tenantId");
}
json_success($invoices);

View File

@@ -42,21 +42,25 @@ try {
$users = $stmt->fetchAll();
// 3. Decrypt data and format
$dec = function($val) {
if (empty($val)) return '';
$result = \App\Core\Encryption::decrypt((string)$val);
return ($result !== false && $result !== null && $result !== '') ? $result : (string)$val;
};
foreach ($users as &$user) {
// Decrypt User Name/Email
$decryptedName = Encryption::decrypt($user['name']);
$user['name'] = $decryptedName !== false ? $decryptedName : $user['name'];
$decryptedEmail = Encryption::decrypt($user['email']);
$user['email'] = $decryptedEmail !== false ? $decryptedEmail : $user['email'];
// Decrypt Tenant Name (if exists)
$user['name'] = $dec($user['name']);
$user['email'] = $dec($user['email']);
if (!empty($user['tenant_name'])) {
$decryptedTenantName = Encryption::decrypt($user['tenant_name']);
$user['tenant_name'] = $decryptedTenantName !== false ? $decryptedTenantName : $user['tenant_name'];
$user['tenant_name'] = $dec($user['tenant_name']);
}
}
if (empty($users)) {
error_log("USERS LIST: No users found for role: $role, tenant_id: $tenantId");
}
json_success($users);
} catch (\Exception $e) {

View File

@@ -91,8 +91,7 @@
<p class="text-gray-500 mt-2 text-sm" x-text="subtitle()"></p>
</div>
<div class="flex items-center gap-4">
<button x-show="page==='tenants'" @click="showAddTenantModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-6 py-3 rounded-xl text-sm font-bold shadow-lg transition-all active:scale-95 btn-glow"> إضافة مكتب</button>
<button x-show="page==='users'" @click="showAddModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-6 py-3 rounded-xl text-sm font-bold shadow-lg transition-all active:scale-95 btn-glow"> إضافة مستخدم</button>
<button x-show="page==='users'" @click="showAddUserModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-6 py-3 rounded-xl text-sm font-bold shadow-lg transition-all active:scale-95 btn-glow"> إضافة مستخدم</button>
<button x-show="page==='companies'" @click="showAddCompanyModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-6 py-3 rounded-xl text-sm font-bold shadow-lg transition-all active:scale-95 btn-glow"> إضافة شركة</button>
<button x-show="page==='invoices'" @click="showUploadModal = true" class="bg-emerald-600 hover:bg-emerald-500 px-6 py-3 rounded-xl text-sm font-bold shadow-lg transition-all active:scale-95 btn-glow">📤 رفع فواتير</button>
</div>
@@ -120,7 +119,7 @@
</div>
</div>
<!-- Companies Table with Stats -->
<!-- Companies Table -->
<div x-show="page === 'companies'">
<div class="bg-surface border border-gray-800 rounded-3xl overflow-hidden shadow-2xl glass">
<table class="w-full text-right divide-y divide-gray-800">
@@ -128,7 +127,7 @@
<tr>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">الشركة</th>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">الأرقام الرسمية</th>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">العنوان</th>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">الفوترة الحكومية</th>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">الإحصائيات</th>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">إجراءات</th>
</tr>
@@ -137,19 +136,65 @@
<tr x-show="companies.length === 0"><td colspan="5" class="p-20 text-center text-gray-600 text-lg">لا توجد شركات مسجلة</td></tr>
<template x-for="c in companies" :key="c.id">
<tr class="hover:bg-white/[0.02] transition-colors group">
<td class="p-6"><p class="font-bold text-emerald-500 text-lg" x-text="c.name"></p></td>
<td class="p-6">
<p class="text-xs text-gray-400">TIN: <span class="font-mono text-gray-200" x-text="c.tax_identification_number"></span></p>
<p class="text-xs text-gray-400">CRN: <span class="font-mono text-gray-200" x-text="c.commercial_registration_number"></span></p>
<p class="font-bold text-emerald-500 text-lg" x-text="c.name"></p>
<p class="text-xs text-gray-500" x-text="c.address"></p>
</td>
<td class="p-6 text-xs font-mono">
<p>TIN: <span x-text="c.tax_identification_number"></span></p>
<p>CRN: <span x-text="c.commercial_registration_number || '-'"></span></p>
</td>
<td class="p-6">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="c.jofotara_client_id_encrypted ? 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]' : 'bg-gray-700'"></span>
<button @click="openConnectModal(c)" class="text-xs font-bold hover:underline" :class="c.jofotara_client_id_encrypted ? 'text-emerald-400' : 'text-gray-400'">
<span x-text="c.jofotara_client_id_encrypted ? '✅ تم الربط (تعديل)' : '🔗 ربط الفوترة'"></span>
</button>
</div>
</td>
<td class="p-6 text-sm text-gray-500" x-text="c.address"></td>
<td class="p-6">
<button @click="showCompanyStats(c.id)" class="text-xs bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 px-4 py-2 rounded-xl border border-emerald-500/20 transition-all font-bold">📊 عرض التقارير</button>
</td>
<td class="p-6">
<div class="flex gap-2">
<button @click="confirmDeleteCompany(c)" class="text-gray-500 hover:text-red-500 p-2.5 rounded-xl hover:bg-red-500/10 transition">🗑️</button>
</div>
<button @click="confirmDeleteCompany(c)" class="text-gray-500 hover:text-red-500 p-2.5 rounded-xl hover:bg-red-500/10 transition">🗑️</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Users Table -->
<div x-show="page === 'users'">
<div class="bg-surface border border-gray-800 rounded-3xl overflow-hidden shadow-2xl glass">
<table class="w-full text-right divide-y divide-gray-800">
<thead class="bg-gray-950/50">
<tr>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">المستخدم</th>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">المكتب</th>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">الدور</th>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">الحالة</th>
<th class="p-6 text-xs font-bold text-gray-500 uppercase">إجراءات</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800/50">
<tr x-show="users.length === 0"><td colspan="5" class="p-20 text-center text-gray-600 text-lg">لا يوجد مستخدمون مسجلون</td></tr>
<template x-for="u in users" :key="u.id">
<tr class="hover:bg-white/[0.02] transition-colors">
<td class="p-6">
<p class="font-bold text-emerald-500" x-text="u.name"></p>
<p class="text-xs text-gray-500 font-mono" x-text="u.email"></p>
</td>
<td class="p-6 text-sm text-gray-400" x-text="u.tenant_name || '-'"></td>
<td class="p-6">
<span class="px-3 py-1 bg-gray-800/50 border border-gray-700 rounded-full text-[10px] font-bold uppercase tracking-wide" x-text="u.role"></span>
</td>
<td class="p-6">
<span class="w-2.5 h-2.5 rounded-full inline-block" :class="u.is_active?'bg-emerald-400':'bg-red-400'"></span>
</td>
<td class="p-6">
<button x-show="u.id !== user.id" @click="confirmDeleteUser(u)" class="text-gray-500 hover:text-red-500 p-2.5 transition">🗑️</button>
</td>
</tr>
</template>
@@ -174,24 +219,20 @@
<tbody class="divide-y divide-gray-800/50">
<tr x-show="invoices.length === 0"><td colspan="5" class="p-20 text-center text-gray-600 text-lg">لا توجد فواتير مرفوعة</td></tr>
<template x-for="inv in invoices" :key="inv.id">
<tr class="hover:bg-white/[0.02] transition-all group">
<tr class="hover:bg-white/[0.02] transition-all">
<td class="p-6">
<p class="font-bold text-emerald-500" x-text="inv.company_name"></p>
<p class="text-sm font-medium text-gray-200 mt-1" x-text="inv.supplier_name"></p>
<p class="text-[10px] text-gray-500 mt-0.5" x-text="inv.supplier_tin"></p>
</td>
<td class="p-6 text-sm text-gray-400 font-mono" x-text="inv.invoice_date || '-'"></td>
<td class="p-6">
<span class="font-mono text-lg font-bold" x-text="parseFloat(inv.grand_total).toLocaleString()"></span>
<span class="text-[10px] text-gray-600 mr-1">JOD</span>
</td>
<td class="p-6 font-mono font-bold" x-text="parseFloat(inv.grand_total).toLocaleString() + ' JOD'"></td>
<td class="p-6">
<span class="px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-wider"
:class="inv.status==='extracted'?'bg-blue-900/40 text-blue-400':(inv.status==='approved'?'bg-emerald-900/40 text-emerald-400':'bg-gray-800 text-gray-400')"
x-text="inv.status"></span>
</td>
<td class="p-6">
<button @click="viewInvoice(inv.id)" class="text-gray-500 hover:text-emerald-400 p-3 rounded-2xl hover:bg-emerald-400/10 transition-all active:scale-95">👁️ عرض</button>
<button @click="viewInvoice(inv.id)" class="text-gray-500 hover:text-emerald-400 p-3 rounded-2xl hover:bg-emerald-400/10 transition-all">👁️ عرض</button>
</td>
</tr>
</template>
@@ -232,24 +273,24 @@
</div>
</div>
<h4 class="text-sm font-bold text-gray-500 uppercase mb-6 tracking-widest">تحليل الأداء الشهري</h4>
<div class="bg-gray-950/30 border border-gray-800 rounded-3xl overflow-hidden">
<h4 class="text-sm font-bold text-gray-500 uppercase mb-6 tracking-widest">التحليل الشهري للفواتير والضرائب</h4>
<div class="bg-gray-950/30 border border-gray-800 rounded-3xl overflow-hidden max-h-64 overflow-y-auto">
<table class="w-full text-right text-xs">
<thead class="bg-gray-900/50">
<thead class="bg-gray-900/50 sticky top-0">
<tr>
<th class="p-5 text-gray-500 font-bold">الشهر</th>
<th class="p-5 text-gray-500 font-bold">عدد الفواتير</th>
<th class="p-5 text-gray-500 font-bold">مجموع الضريبة</th>
<th class="p-5 text-gray-500 font-bold text-emerald-500">المجموع النهائي</th>
<th class="p-5 text-gray-500 font-bold">الضريبة المستحقة</th>
<th class="p-5 text-gray-500 font-bold text-emerald-500">الإجمالي النهائي</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
<template x-for="month in companyStats?.monthly" :key="month.month">
<tr class="hover:bg-white/[0.02]">
<td class="p-5 font-mono font-bold text-gray-200" x-text="month.month"></td>
<td class="p-5" x-text="month.total_invoices"></td>
<td class="p-5 text-yellow-500/80 font-mono" x-text="parseFloat(month.total_tax || 0).toLocaleString()"></td>
<td class="p-5 font-bold text-emerald-500 font-mono text-sm" x-text="parseFloat(month.total_amount).toLocaleString() + ' JOD'"></td>
<template x-for="m in companyStats?.monthly" :key="m.month">
<tr>
<td class="p-5 font-mono text-gray-200" x-text="m.month"></td>
<td class="p-5" x-text="m.total_invoices"></td>
<td class="p-5 text-yellow-500 font-mono" x-text="parseFloat(m.total_tax || 0).toLocaleString() + ' JOD'"></td>
<td class="p-5 font-bold text-emerald-500 font-mono" x-text="parseFloat(m.total_amount || 0).toLocaleString() + ' JOD'"></td>
</tr>
</template>
</tbody>
@@ -258,161 +299,65 @@
</div>
</div>
<!-- Full Invoice View Modal -->
<div x-show="showViewModal" x-cloak class="fixed inset-0 bg-black/95 flex items-center justify-center p-6 z-[100]">
<div class="bg-surface border border-gray-800 w-full h-full max-w-7xl rounded-[40px] shadow-2xl flex overflow-hidden glass-elevated">
<!-- Left: File Preview -->
<div class="w-1/2 bg-black/40 border-l border-gray-800 flex flex-col relative">
<div class="p-6 border-b border-gray-800 flex justify-between items-center bg-gray-950/50">
<span class="text-xs font-bold text-gray-500 uppercase tracking-widest">المستند الأصلي</span>
<a :href="currentInvoice?.file_url + '&token=' + token()" target="_blank" class="text-[10px] bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 px-4 py-2 rounded-xl border border-emerald-500/20 transition-all font-bold">تحميل 📥</a>
<!-- Add User Modal -->
<div x-show="showAddUserModal" x-cloak class="fixed inset-0 bg-black/90 flex items-center justify-center p-6 z-[120]">
<div class="bg-surface border border-gray-800 w-full max-w-lg p-10 rounded-[40px] shadow-2xl glass-elevated">
<h3 class="text-2xl font-bold mb-8">إضافة مستخدم جديد</h3>
<form @submit.prevent="createUser" class="space-y-6">
<input type="text" x-model="newUser.name" placeholder="الاسم الكامل" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none" required>
<input type="email" x-model="newUser.email" placeholder="البريد الإلكتروني" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none" required>
<input type="password" x-model="newUser.password" placeholder="كلمة المرور" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none" required>
<select x-model="newUser.role" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none" required>
<option value="accountant">محاسب</option>
<option value="viewer">مشاهد فقط</option>
<option x-show="user?.role === 'super_admin'" value="admin">مدير مكتب</option>
</select>
<div class="flex gap-4 pt-4">
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-4 rounded-2xl font-bold transition-all btn-glow" :disabled="isBusy">حفظ المستخدم</button>
<button type="button" @click="showAddUserModal = false" class="px-8 py-4 border border-gray-800 rounded-2xl">إلغاء</button>
</div>
<div class="flex-1 overflow-auto p-8 flex items-start justify-center scrollbar-hide">
<template x-if="currentInvoice?.original_file_path?.toLowerCase().endsWith('.pdf')">
<iframe :src="currentInvoice?.file_url + '&token=' + token()" class="w-full h-full rounded-2xl shadow-2xl" frameborder="0"></iframe>
</template>
<template x-if="!currentInvoice?.original_file_path?.toLowerCase().endsWith('.pdf')">
<img :src="currentInvoice?.file_url + '&token=' + token()" @error="$el.src='https://placehold.co/600x800?text=Error+Loading+Image'" class="max-w-full rounded-2xl shadow-2xl border border-white/5">
</template>
</div>
</div>
<!-- Right: Extracted Data -->
<div class="w-1/2 flex flex-col">
<div class="p-8 border-b border-gray-800 flex justify-between items-start bg-emerald-500/5">
<div>
<h3 class="text-2xl font-bold text-white">تفاصيل الفاتورة المستخرجة</h3>
<p class="text-[10px] text-emerald-500/70 mt-2 uppercase tracking-tighter">التحليل الذكي (Gemini 1.5 Flash)</p>
</div>
<button @click="showViewModal = false" class="text-gray-500 hover:text-white text-3xl transition"></button>
</div>
<div class="flex-1 overflow-y-auto p-8 space-y-10 scrollbar-hide">
<!-- Supplier Info -->
<div class="grid grid-cols-2 gap-8">
<div class="p-6 bg-gray-950/50 border border-gray-800 rounded-3xl hover:border-emerald-500/30 transition-all">
<label class="block text-[10px] text-gray-500 font-bold uppercase mb-3">المورد</label>
<p class="text-lg font-bold text-emerald-400" x-text="currentInvoice?.supplier_name"></p>
<p class="text-xs text-gray-500 mt-2 font-mono">TIN: <span class="text-gray-300" x-text="currentInvoice?.supplier_tin"></span></p>
<p class="text-xs text-gray-500 mt-1" x-text="currentInvoice?.supplier_address"></p>
</div>
<div class="p-6 bg-gray-950/50 border border-gray-800 rounded-3xl hover:border-blue-500/30 transition-all">
<label class="block text-[10px] text-gray-500 font-bold uppercase mb-3">بيانات الفاتورة</label>
<p class="text-lg font-bold font-mono" x-text="currentInvoice?.invoice_number"></p>
<p class="text-xs text-gray-400 mt-2 font-mono" x-text="currentInvoice?.invoice_date"></p>
<p class="text-[10px] text-gray-600 mt-2 uppercase font-bold" x-text="currentInvoice?.invoice_type + ' / ' + currentInvoice?.invoice_category"></p>
</div>
</div>
<!-- Line Items -->
<div>
<label class="block text-[10px] text-gray-500 font-bold uppercase mb-4 tracking-widest">بنود الفاتورة</label>
<div class="bg-gray-950/30 border border-gray-800 rounded-3xl overflow-hidden">
<table class="w-full text-right text-xs">
<thead class="bg-gray-900/50">
<tr>
<th class="p-4 text-gray-500 font-bold">#</th>
<th class="p-4 text-gray-500 font-bold">الوصف</th>
<th class="p-4 text-gray-500 font-bold">الكمية</th>
<th class="p-4 text-gray-500 font-bold">السعر</th>
<th class="p-4 text-gray-500 font-bold text-emerald-500">الإجمالي</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
<template x-for="item in currentInvoice?.items" :key="item.id">
<tr class="hover:bg-white/[0.02] transition-colors">
<td class="p-4 text-gray-600 font-mono" x-text="item.line_number"></td>
<td class="p-4 font-medium text-gray-200" x-text="item.description"></td>
<td class="p-4 font-mono" x-text="parseFloat(item.quantity).toLocaleString()"></td>
<td class="p-4 font-mono" x-text="parseFloat(item.unit_price).toLocaleString()"></td>
<td class="p-4 font-bold text-emerald-500 font-mono" x-text="parseFloat(item.line_total).toLocaleString()"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Totals Section -->
<div class="flex justify-end">
<div class="w-80 space-y-4 p-8 bg-emerald-500/5 border border-emerald-500/20 rounded-3xl glass">
<div class="flex justify-between text-xs">
<span class="text-gray-500 font-bold uppercase">المجموع الفرعي</span>
<span class="font-mono text-gray-200" x-text="parseFloat(currentInvoice?.subtotal || 0).toLocaleString()"></span>
</div>
<div class="flex justify-between text-xs">
<span class="text-gray-500 font-bold uppercase">ضريبة المبيعات</span>
<span class="font-mono text-yellow-500/80" x-text="parseFloat(currentInvoice?.tax_amount || 0).toLocaleString()"></span>
</div>
<div class="border-t border-emerald-500/20 pt-4 flex justify-between items-center">
<span class="text-sm font-bold text-emerald-400 uppercase">الإجمالي النهائي</span>
<span class="text-2xl font-bold text-emerald-400 font-mono" x-text="parseFloat(currentInvoice?.grand_total || 0).toLocaleString() + ' JOD'"></span>
</div>
</div>
</div>
<!-- QR / Certification Display -->
<template x-if="currentInvoice?.qr_code || currentInvoice?.jofotara?.qr_image_uri">
<div class="p-8 bg-emerald-950/20 border border-emerald-500/30 rounded-[32px] flex items-center gap-8 glass">
<div class="bg-white p-3 rounded-2xl shadow-2xl transform hover:scale-105 transition-transform">
<img :src="currentInvoice?.jofotara?.qr_image_uri || ('data:image/png;base64,' + generateQRPng(currentInvoice.qr_code))" class="w-32 h-32" alt="QR Code">
</div>
<div class="space-y-3 flex-1">
<h4 class="text-emerald-400 font-bold text-lg" x-text="currentInvoice?.jofotara ? '✅ فاتورة معتمدة رسمياً' : '✅ تم الاعتماد محلياً'"></h4>
<p class="text-xs text-gray-500 font-mono">الرقم الموحد: <span class="text-gray-200" x-text="currentInvoice?.jofotara?.uuid || currentInvoice?.id"></span></p>
<p class="text-xs text-gray-500">تاريخ الرفع: <span class="text-gray-300" x-text="currentInvoice?.jofotara?.submitted_at || currentInvoice?.updated_at"></span></p>
<div class="pt-2" x-show="currentInvoice?.jofotara">
<a :href="'/index.php?route=v1/invoices/download_xml&id=' + currentInvoice.id + '&token=' + token()"
class="inline-flex items-center gap-2 text-xs bg-emerald-600/20 hover:bg-emerald-600/30 text-emerald-400 px-5 py-2.5 rounded-xl border border-emerald-500/30 transition-all font-bold">
📥 تحميل ملف XML الرسمي
</a>
</div>
</div>
</div>
</template>
</div>
<div class="p-8 bg-gray-950/50 border-t border-gray-800 flex gap-4">
<template x-if="currentInvoice?.status === 'extracted'">
<button @click="approveInvoice(currentInvoice.id)"
class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-4 rounded-2xl font-bold transition-all flex items-center justify-center gap-3 disabled:opacity-50 btn-glow"
:disabled="isApproving">
<span x-show="!isApproving"> اعتماد الفاتورة وتوليد الـ QR الرسمي</span>
<span x-show="isApproving" class="flex items-center gap-2">جارِ المعالجة... </span>
</button>
</template>
<button @click="showViewModal = false" class="px-10 py-4 border border-gray-800 rounded-2xl hover:bg-gray-800 transition text-sm font-bold">إغلاق</button>
</div>
</div>
</form>
</div>
</div>
<!-- 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-6 z-50">
<div class="bg-surface border border-gray-800 w-full max-w-lg p-10 rounded-[40px] shadow-2xl glass-elevated" @click.away="showUploadModal = false">
<h3 class="text-3xl font-bold mb-3">رفع فواتير 📤</h3>
<p class="text-gray-500 text-sm mb-10">سيقوم النظام بتحليل الفاتورة آلياً</p>
<form @submit.prevent="uploadInvoice" class="space-y-8">
<!-- Add Company Modal -->
<div x-show="showAddCompanyModal" x-cloak class="fixed inset-0 bg-black/90 flex items-center justify-center p-6 z-[120]">
<div class="bg-surface border border-gray-800 w-full max-w-lg p-10 rounded-[40px] shadow-2xl glass-elevated">
<h3 class="text-2xl font-bold mb-8">إضافة شركة جديدة</h3>
<form @submit.prevent="createCompany" class="space-y-6">
<input type="text" x-model="newCompany.name" placeholder="اسم الشركة (بالعربي)" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none" required>
<input type="text" x-model="newCompany.tax_identification_number" placeholder="الرقم الضريبي (TIN)" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none" required>
<input type="text" x-model="newCompany.commercial_registration_number" placeholder="رقم السجل التجاري" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none">
<input type="text" x-model="newCompany.address" placeholder="العنوان" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none">
<div class="flex gap-4 pt-4">
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-4 rounded-2xl font-bold transition-all btn-glow" :disabled="isBusy">إضافة الشركة</button>
<button type="button" @click="showAddCompanyModal = false" class="px-8 py-4 border border-gray-800 rounded-2xl">إلغاء</button>
</div>
</form>
</div>
</div>
<!-- JoFotara Connect Modal -->
<div x-show="showConnectModal" x-cloak class="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center p-6 z-[130]">
<div class="bg-surface border border-gray-800 w-full max-w-lg p-10 rounded-[40px] shadow-2xl glass-elevated">
<h3 class="text-2xl font-bold mb-4">ربط نظام الفوترة الحكومي</h3>
<p class="text-gray-500 text-sm mb-8" x-text="'الشركة: ' + currentCompany?.name"></p>
<form @submit.prevent="connectJoFotara" class="space-y-6">
<div>
<label class="block text-[10px] text-gray-500 font-bold uppercase mb-3">اختر الشركة</label>
<select x-model="uploadData.company_id" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none focus:ring-2 focus:ring-emerald-500/20 transition-all cursor-pointer" required>
<option value="">-- اختر الشركة --</option>
<template x-for="c in companies" :key="c.id"><option :value="c.id" x-text="c.name"></option></template>
</select>
<label class="block text-xs font-bold text-gray-500 uppercase mb-2">Client ID</label>
<input type="text" x-model="connectData.client_id" placeholder="أدخل Client ID" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none font-mono text-sm" required>
</div>
<div class="border-2 border-dashed border-gray-800 rounded-[32px] p-16 text-center hover:border-emerald-500/50 transition-all cursor-pointer group relative">
<input type="file" @change="handleFile" class="absolute inset-0 opacity-0 cursor-pointer" required>
<div class="space-y-4">
<span class="text-5xl block group-hover:scale-110 transition-transform">📄</span>
<p class="text-sm font-bold text-gray-400" x-text="selectedFile ? selectedFile.name : 'اختر ملف الفاتورة'"></p>
</div>
<div>
<label class="block text-xs font-bold text-gray-500 uppercase mb-2">Secret Key</label>
<input type="password" x-model="connectData.secret_key" placeholder="أدخل Secret Key" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none font-mono text-sm" required>
</div>
<div class="pt-4 flex gap-4">
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-4 rounded-2xl font-bold shadow-lg transition-all active:scale-95 disabled:opacity-50 btn-glow" :disabled="isUploading">
<span x-show="!isUploading">بدء المعالجة الذكية</span>
<span x-show="isUploading">جارِ التحليل... </span>
</button>
<button type="button" @click="showUploadModal = false" class="px-8 py-4 border border-gray-800 rounded-2xl hover:bg-gray-800 transition-all font-bold">إلغاء</button>
<div>
<label class="block text-xs font-bold text-gray-500 uppercase mb-2">تسلسل مصدر الدخل</label>
<input type="text" x-model="connectData.income_source_sequence" placeholder="مثال: 1" class="w-full bg-gray-950 border border-gray-800 p-4 rounded-2xl outline-none font-mono text-sm">
</div>
<div class="flex gap-4 pt-4">
<button type="submit" class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-4 rounded-2xl font-bold transition-all btn-glow" :disabled="isBusy">حفظ وتفعيل الربط</button>
<button type="button" @click="showConnectModal = false" class="px-8 py-4 border border-gray-800 rounded-2xl">إلغاء</button>
</div>
</form>
</div>
@@ -424,167 +369,89 @@
Alpine.data('app', () => ({
user: JSON.parse(localStorage.getItem('user')),
page: 'dashboard',
users: [],
companies: [],
tenants: [],
invoices: [],
users: [], companies: [], invoices: [], tenants: [],
stats: { total: 0, pending: 0, approved: 0 },
showAddModal: false,
showAddCompanyModal: false,
showAddTenantModal: false,
showUploadModal: false,
showViewModal: false,
showCompanyStatsModal: false,
isUploading: false,
isApproving: false,
globalError: '',
showAddUserModal: false, showAddCompanyModal: false, showConnectModal: false,
showUploadModal: false, showViewModal: false, showCompanyStatsModal: false,
isBusy: false, globalError: '',
newUser: { name: '', email: '', password: '', role: 'employee', tenant_id: '' },
newUser: { name: '', email: '', password: '', role: 'accountant' },
newCompany: { name: '', tax_identification_number: '', commercial_registration_number: '', address: '' },
connectData: { client_id: '', secret_key: '', income_source_sequence: '1' },
uploadData: { company_id: '' },
selectedFile: null,
currentInvoice: null,
companyStats: null,
currentCompany: null, currentInvoice: null, companyStats: null,
init() {
if (!this.user) { window.location.href = '/login.php'; return; }
this.loadAll();
},
setPage(p) {
this.page = p;
this.loadAll();
},
setPage(p) { this.page = p; this.loadAll(); },
title() { return { dashboard: 'لوحة التحكم', users: 'المستخدمون', companies: 'الشركات', invoices: 'إدارة الفواتير' }[this.page] || ''; },
subtitle() { return { dashboard: 'نظرة شاملة على الأداء', users: 'إدارة الصلاحيات والوصول', companies: 'إدارة بيانات الشركات والضرائب', invoices: 'معالجة واعتماد الفواتير المستخرجة' }[this.page] || ''; },
showError(msg) { this.globalError = msg; setTimeout(() => this.globalError = '', 8000); },
subtitle() { return { dashboard: 'نظرة شاملة', users: 'إدارة الفريق', companies: 'إدارة المؤسسات والربط الضريبي', invoices: 'معالجة الفواتير' }[this.page] || ''; },
token() { return localStorage.getItem('access_token'); },
showError(msg) { this.globalError = msg; setTimeout(() => this.globalError = '', 8000); },
async apiRequest(route, method = 'GET', body = null) {
const options = {
method: method,
headers: {
'Authorization': 'Bearer ' + this.token(),
'Content-Type': 'application/json'
}
};
if (body) options.body = JSON.stringify(body);
try {
const options = {
method,
headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' }
};
if (body) options.body = JSON.stringify(body);
const res = await fetch('/index.php?route=' + route, options);
// Session Expired logic
if (res.status === 401) {
const json = await res.json();
if (json.code === 'TOKEN_EXPIRED') {
localStorage.clear();
window.location.href = '/login.php';
return null;
}
}
const json = await res.json();
if (res.status === 401) { localStorage.clear(); window.location.href = '/login.php'; return null; }
return json.success ? json.data : (this.showError(json.message), null);
} catch (e) {
this.showError('فشل الاتصال بالخادم الرئيسي');
return null;
}
} catch (e) { this.showError('فشل الاتصال بالخادم'); return null; }
},
async apiGet(route) { return this.apiRequest(route); },
async loadAll() {
this.loadStats();
this.loadCompanies();
if (this.page === 'users') this.loadUsers();
if (this.page === 'invoices') this.loadInvoices();
if (this.page === 'tenants') this.loadTenants();
this.stats = await this.apiRequest('v1/dashboard/stats') || { total: 0, pending: 0, approved: 0 };
this.companies = await this.apiRequest('v1/companies') || [];
if (this.page === 'users') this.users = await this.apiRequest('v1/users') || [];
if (this.page === 'invoices') this.invoices = await this.apiRequest('v1/invoices') || [];
},
async loadUsers() { this.users = await this.apiGet('v1/users') || []; },
async loadCompanies() { this.companies = await this.apiGet('v1/companies') || []; },
async loadInvoices() { this.invoices = await this.apiGet('v1/invoices') || []; },
async loadStats() { this.stats = await this.apiGet('v1/dashboard/stats') || {}; },
async loadTenants() { this.tenants = await this.apiGet('v1/tenants') || []; },
async createUser() {
this.isBusy = true;
const res = await this.apiRequest('v1/users/create', 'POST', this.newUser);
if (res) { this.showAddUserModal = false; this.loadAll(); alert('تم إضافة المستخدم بنجاح'); }
this.isBusy = false;
},
async createCompany() {
this.isBusy = true;
const res = await this.apiRequest('v1/companies/create', 'POST', this.newCompany);
if (res) { this.showAddCompanyModal = false; this.loadAll(); alert('تم إضافة الشركة بنجاح'); }
this.isBusy = false;
},
openConnectModal(company) {
this.currentCompany = company;
this.connectData.client_id = '';
this.connectData.secret_key = '';
this.showConnectModal = true;
},
async connectJoFotara() {
this.isBusy = true;
const res = await this.apiRequest('v1/companies/connect', 'POST', {
id: this.currentCompany.id,
...this.connectData
});
if (res) { this.showConnectModal = false; this.loadAll(); alert('تم تفعيل الربط الضريبي بنجاح'); }
this.isBusy = false;
},
async showCompanyStats(id) {
const data = await this.apiGet('v1/companies/stats&id=' + id);
if (data) {
this.companyStats = data;
this.showCompanyStatsModal = true;
}
this.companyStats = await this.apiRequest('v1/companies/stats&id=' + id);
if (this.companyStats) this.showCompanyStatsModal = true;
},
async viewInvoice(id) {
const data = await this.apiGet('v1/invoices/view&id=' + id);
if (data) {
this.currentInvoice = data;
this.showViewModal = true;
}
},
async approveInvoice(id) {
if (!confirm('هل أنت متأكد من اعتماد الفاتورة؟')) return;
this.isApproving = true;
try {
const data = await this.apiRequest('v1/invoices/approve', 'POST', { id: id });
if (data) {
alert(data.message || '✅ تم الاعتماد بنجاح!');
this.viewInvoice(id);
this.loadInvoices();
this.loadStats();
}
} catch (e) {
this.showError('خطأ تقني: ' + e.message);
} finally {
this.isApproving = false;
}
},
generateQRPng(base64Tlv) {
if (!base64Tlv) return '';
try {
const qr = new QRious({
value: base64Tlv,
size: 300,
level: 'M'
});
return qr.toDataURL().split(',')[1];
} catch (e) {
return '';
}
},
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();
this.isUploading = false;
if (json.success) {
this.showUploadModal = false;
this.loadInvoices();
this.viewInvoice(json.data.id);
} else {
this.showError(json.message);
}
} catch (e) {
this.isUploading = false;
this.showError('فشل عملية الرفع');
}
this.currentInvoice = await this.apiRequest('v1/invoices/view&id=' + id);
if (this.currentInvoice) this.showViewModal = true;
},
logout() { localStorage.clear(); window.location.href = '/login.php'; }