Update: 2026-05-04 17:59:11

This commit is contained in:
Hamza-Ayed
2026-05-04 17:59:11 +03:00
parent 98c4b922be
commit 51ae81a9fa
5 changed files with 237 additions and 185 deletions

View File

@@ -31,7 +31,7 @@ try {
$stmt->execute($params);
$total = $stmt->fetchColumn();
$stmt = $db->prepare("SELECT COUNT(*) FROM invoices $where AND status = 'pending'");
$stmt = $db->prepare("SELECT COUNT(*) FROM invoices $where AND status = 'extracted'");
$stmt->execute($params);
$pending = $stmt->fetchColumn();

View File

@@ -40,32 +40,39 @@ try {
$invoice['items'] = $stmtLines->fetchAll();
// 3. Decrypt Company Keys for JoFotara
$clientId = \App\Core\Encryption::decrypt($invoice['jofotara_client_id_encrypted']);
$secretKey = \App\Core\Encryption::decrypt($invoice['jofotara_secret_key_encrypted']);
$clientId = \App\Core\Encryption::decrypt($invoice['jofotara_client_id_encrypted'] ?? '');
$secretKey = \App\Core\Encryption::decrypt($invoice['jofotara_secret_key_encrypted'] ?? '');
if (!$clientId || !$secretKey) {
throw new \Exception("JoFotara credentials missing for company: " . $invoice['company_name']);
$jofotara = new JoFotara();
$apiResponse = ['success' => false];
$xmlContent = null;
// 4. Try JoFotara Submission if credentials exist
if ($clientId && $secretKey) {
$companyData = [
'name' => $invoice['company_name'],
'tax_identification_number' => $invoice['company_tin'],
'address' => $invoice['company_address']
];
// Decrypt Buyer Info for XML
$invoice['buyer_name'] = \App\Core\Encryption::decrypt($invoice['buyer_name'] ?? '') ?: ($invoice['buyer_name'] ?? '');
$invoice['buyer_tin'] = \App\Core\Encryption::decrypt($invoice['buyer_tin'] ?? '') ?: ($invoice['buyer_tin'] ?? '');
$xmlContent = $jofotara->generateXML($invoice, $companyData);
$apiResponse = $jofotara->submitInvoice($xmlContent, $clientId, $secretKey);
}
// Decrypt Buyer Info
$invoice['buyer_name'] = \App\Core\Encryption::decrypt($invoice['buyer_name']) ?: '';
$invoice['buyer_tin'] = \App\Core\Encryption::decrypt($invoice['buyer_tin']) ?: '';
// 5. Fallback: Generate Local QR if API failed or no credentials
$qrCode = $apiResponse['qrCode'] ?? $jofotara->generateQRCode([
'supplier_name' => $invoice['company_name'],
'supplier_tin' => $invoice['company_tin'],
'invoice_date' => $invoice['invoice_date'],
'grand_total' => $invoice['grand_total'],
'tax_amount' => $invoice['tax_amount']
]);
// 4. Initialize JoFotara Service
$jofotara = new JoFotara();
// 5. Generate UBL 2.1 XML
$companyData = [
'name' => $invoice['company_name'],
'tax_identification_number' => $invoice['company_tin'],
'address' => $invoice['company_address']
];
$xmlContent = $jofotara->generateXML($invoice, $companyData);
// 6. Submit to JoFotara API
$apiResponse = $jofotara->submitInvoice($xmlContent, $clientId, $secretKey);
// 7. Record Submission (Audit Log)
// 6. Record Submission (Audit Log)
$submissionId = \App\Core\Database::generateUuid();
$stmtSub = $db->prepare("
INSERT INTO jofotara_submissions
@@ -74,41 +81,38 @@ try {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
");
$status = $apiResponse['success'] ? 'accepted' : 'rejected';
$status = $apiResponse['success'] ? 'accepted' : 'error';
$stmtSub->execute([
$submissionId,
$id,
$invoice['company_id'],
$invoice['tenant_id'],
$xmlContent,
hash('sha256', $xmlContent),
$xmlContent ? hash('sha256', $xmlContent) : null,
$apiResponse['uuid'] ?? null,
$apiResponse['qrCode'] ?? null,
$qrCode,
$apiResponse['_http_code'] ?? '0',
json_encode($apiResponse['raw'] ?? []),
json_encode($apiResponse['raw'] ?? ['info' => 'Local approval / No credentials']),
$status
]);
if (!$apiResponse['success']) {
throw new \Exception("JoFotara Rejection: " . ($apiResponse['error'] ?? 'Unknown Error'));
}
// 8. Update Invoice
// 7. Update Invoice
$updateStmt = $db->prepare("
UPDATE invoices SET status = 'approved', jofotara_uuid = ?, qr_code = ?, updated_at = NOW() WHERE id = ?
");
$updateStmt->execute([$apiResponse['uuid'], $apiResponse['qrCode'], $id]);
$updateStmt->execute([$apiResponse['uuid'] ?? null, $qrCode, $id]);
$db->commit();
json_success([
'message' => 'Approved and submitted to JoFotara.',
'uuid' => $apiResponse['uuid'],
'qr_code' => $apiResponse['qrCode']
'message' => $apiResponse['success'] ? 'تم الاعتماد والإرسال إلى جوفوترة بنجاح' : 'تم الاعتماد محلياً (نظام جوفوترة غير متصل)',
'uuid' => $apiResponse['uuid'] ?? null,
'qr_code' => $qrCode,
'is_api_success' => $apiResponse['success']
]);
} catch (\Exception $e) {
$db->rollBack();
if ($db->inTransaction()) $db->rollBack();
error_log("JoFotara Approve Error: " . $e->getMessage());
json_error('Failed to approve invoice: ' . $e->getMessage(), 500);
json_error('خطأ غير متوقع: ' . $e->getMessage(), 500);
}

View File

@@ -60,18 +60,20 @@ try {
$invoices = $stmt->fetchAll();
// 3. Decrypt sensitive fields for display (Robustly)
$decrypt = fn($val) => Encryption::decrypt($val ?? '') ?: ($val ?? '-');
$dec = function($val) {
if (empty($val)) return '';
$result = Encryption::decrypt((string)$val);
return ($result !== false && $result !== null && $result !== '') ? $result : (string)$val;
};
foreach ($invoices as &$inv) {
$inv['supplier_name'] = $decrypt($inv['supplier_name']);
$inv['supplier_tin'] = $decrypt($inv['supplier_tin']);
$inv['buyer_name'] = $decrypt($inv['buyer_name']);
$inv['supplier_name'] = $dec($inv['supplier_name']);
$inv['supplier_tin'] = $dec($inv['supplier_tin']);
$inv['buyer_name'] = $dec($inv['buyer_name']);
if (!empty($inv['company_name'])) {
$inv['company_name'] = $decrypt($inv['company_name']);
}
if (!empty($inv['tenant_name'])) {
$inv['tenant_name'] = $decrypt($inv['tenant_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.
}
json_success($invoices);

View File

@@ -7,51 +7,63 @@ use App\Core\Database;
use App\Core\Encryption;
use App\Middleware\AuthMiddleware;
// 1. Auth Check
$decoded = AuthMiddleware::check();
$db = Database::getInstance();
// 2. Validate Request
$id = $_GET['id'] ?? null;
if (!$id) json_error('Invoice ID is required', 422);
// 3. Permission Check (Multi-Tenant Isolation)
$tenantId = $decoded['tenant_id'];
$role = $decoded['role'];
try {
$stmt = $db->prepare("
SELECT i.*, c.name as company_name
FROM invoices i
JOIN companies c ON i.company_id = c.id
WHERE i.id = ? AND i.tenant_id = ?
");
$stmt->execute([$id, $tenantId]);
$invoice = $stmt->fetch();
// 1. Fetch Invoice (Super Admin sees all, others are tenant-scoped)
if ($role === 'super_admin') {
$stmt = $db->prepare("
SELECT i.*, c.name as company_name
FROM invoices i
JOIN companies c ON i.company_id = c.id
WHERE i.id = ?
");
$stmt->execute([$id]);
} else {
$stmt = $db->prepare("
SELECT i.*, c.name as company_name
FROM invoices i
JOIN companies c ON i.company_id = c.id
WHERE i.id = ? AND i.tenant_id = ?
");
$stmt->execute([$id, $tenantId]);
}
$invoice = $stmt->fetch();
if (!$invoice) json_error('Invoice not found or access denied', 404);
// 4. Fetch Line Items
// 2. Fetch Line Items
$stmtLines = $db->prepare("SELECT * FROM invoice_lines WHERE invoice_id = ? ORDER BY line_number ASC");
$stmtLines->execute([$id]);
$invoice['items'] = $stmtLines->fetchAll();
// 5. Decrypt Fields (Robustly)
$decrypt = fn($val) => Encryption::decrypt($val ?? '') ?: $val;
// 3. Decrypt all encrypted fields — robust: if decryption fails, keep original value
$dec = function($val) {
if (empty($val)) return '';
$result = \App\Core\Encryption::decrypt((string)$val);
return ($result !== false && $result !== null && $result !== '') ? $result : (string)$val;
};
$invoice['supplier_tin'] = $decrypt($invoice['supplier_tin']);
$invoice['supplier_name'] = $decrypt($invoice['supplier_name']);
$invoice['supplier_address'] = $decrypt($invoice['supplier_address']);
$invoice['buyer_tin'] = $decrypt($invoice['buyer_tin']);
$invoice['buyer_name'] = $decrypt($invoice['buyer_name']);
$invoice['buyer_national_id'] = $decrypt($invoice['buyer_national_id']);
if (!empty($invoice['company_name'])) {
$invoice['company_name'] = $decrypt($invoice['company_name']);
}
$invoice['supplier_tin'] = $dec($invoice['supplier_tin']);
$invoice['supplier_name'] = $dec($invoice['supplier_name']);
$invoice['supplier_address'] = $dec($invoice['supplier_address']);
$invoice['buyer_tin'] = $dec($invoice['buyer_tin']);
$invoice['buyer_name'] = $dec($invoice['buyer_name']);
$invoice['buyer_national_id'] = $dec($invoice['buyer_national_id']);
// 6. Fetch JoFotara Submission Data
// company_name is stored plaintext in the companies table — no decryption needed
// $invoice['company_name'] is already plaintext from the JOIN
// 4. Fetch JoFotara Submission Data (latest accepted submission)
$stmtSub = $db->prepare("
SELECT jofotara_uuid, submitted_at, qr_code_raw, status as submission_status, response_body
SELECT jofotara_uuid, submitted_at, qr_code_raw, response_body
FROM jofotara_submissions
WHERE invoice_id = ? AND status = 'accepted'
ORDER BY created_at DESC LIMIT 1
@@ -59,16 +71,21 @@ try {
$stmtSub->execute([$id]);
$submission = $stmtSub->fetch();
$invoice['jofotara'] = $submission ? [
'uuid' => $submission['jofotara_uuid'],
'submitted_at' => $submission['submitted_at'],
'qr_image_uri' => $submission['qr_code_raw'] ? 'data:image/png;base64,' . $submission['qr_code_raw'] : null,
'has_xml' => true
] : null;
if ($submission) {
$invoice['jofotara'] = [
'uuid' => $submission['jofotara_uuid'],
'submitted_at' => $submission['submitted_at'],
'qr_image_uri' => $submission['qr_code_raw']
? 'data:image/png;base64,' . $submission['qr_code_raw']
: null,
'has_xml' => true,
];
} else {
$invoice['jofotara'] = null;
}
// 7. Generate Public URL for File
$token = Encryption::encrypt($invoice['original_file_path']);
$invoice['file_url'] = '/index.php?route=v1/invoices/file&file_token=' . urlencode($token);
// 5. Build the secure file URL using the invoice ID (file.php fetches path from DB)
$invoice['file_url'] = '/index.php?route=v1/invoices/file&id=' . urlencode($id);
json_success($invoice);

View File

@@ -144,7 +144,7 @@
</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-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">
@@ -165,11 +165,12 @@
<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="companies.length === 0"><td colspan="5" class="p-12 text-center text-gray-600">لا توجد شركات بعد</td></tr>
<tr x-show="companies.length === 0"><td colspan="6" class="p-12 text-center text-gray-600">لا توجد شركات بعد</td></tr>
<template x-for="c in companies" :key="c.id">
<tr class="hover:bg-white/[0.01] transition-colors group">
<td class="p-5"><p class="font-bold text-emerald-500" x-text="c.name"></p></td>
@@ -179,6 +180,9 @@
</td>
<td class="p-5 text-sm text-gray-500" x-text="c.address"></td>
<td class="p-5 text-xs text-gray-500" x-text="c.tenant_name || '-'"></td>
<td class="p-5">
<button @click="showCompanyStats(c.id)" class="text-xs bg-gray-800 hover:bg-emerald-600/20 text-emerald-400 px-3 py-1.5 rounded-lg border border-gray-700 hover:border-emerald-500/30 transition-all">📊 عرض</button>
</td>
<td class="p-5 flex gap-2">
<button x-show="user?.role === 'super_admin' || user?.role === 'admin'" @click="confirmDeleteCompany(c)" class="text-gray-500 hover:text-red-500 p-2 rounded-lg hover:bg-red-500/10 transition">🗑️</button>
</td>
@@ -225,6 +229,62 @@
</div>
</main>
<!-- Company Stats Modal -->
<div x-show="showCompanyStatsModal" x-cloak class="fixed inset-0 bg-black/90 backdrop-blur-md flex items-center justify-center p-4 z-[110]">
<div class="bg-surface border border-gray-800 w-full max-w-4xl p-10 rounded-3xl shadow-2xl glass" @click.away="showCompanyStatsModal = false">
<div class="flex justify-between items-center mb-10">
<div>
<h3 class="text-2xl font-bold" x-text="companyStats?.company?.name"></h3>
<p class="text-xs text-gray-500 mt-1 uppercase tracking-widest" x-text="'TIN: ' + companyStats?.company?.tax_identification_number"></p>
</div>
<button @click="showCompanyStatsModal = false" class="text-gray-500 hover:text-white text-2xl"></button>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-10">
<div class="p-6 bg-gray-950/50 border border-gray-800 rounded-2xl">
<p class="text-[10px] text-gray-500 font-bold uppercase mb-2">إجمالي الفواتير</p>
<p class="text-2xl font-bold" x-text="companyStats?.totals?.total_invoices || 0"></p>
</div>
<div class="p-6 bg-gray-950/50 border border-gray-800 rounded-2xl">
<p class="text-[10px] text-gray-500 font-bold uppercase mb-2">المجموع الكلي (JOD)</p>
<p class="text-2xl font-bold text-emerald-400" x-text="parseFloat(companyStats?.totals?.total_amount || 0).toLocaleString()"></p>
</div>
<div class="p-6 bg-gray-950/50 border border-gray-800 rounded-2xl">
<p class="text-[10px] text-gray-500 font-bold uppercase mb-2">إجمالي الضريبة</p>
<p class="text-2xl font-bold text-yellow-500" x-text="parseFloat(companyStats?.totals?.total_tax || 0).toLocaleString()"></p>
</div>
<div class="p-6 bg-gray-950/50 border border-gray-800 rounded-2xl">
<p class="text-[10px] text-gray-500 font-bold uppercase mb-2">فواتير معتمدة</p>
<p class="text-2xl font-bold text-blue-400" x-text="companyStats?.totals?.approved_count || 0"></p>
</div>
</div>
<h4 class="text-sm font-bold text-gray-500 uppercase mb-4 tracking-widest">الإحصائيات الشهرية (آخر 12 شهر)</h4>
<div class="bg-gray-950/30 border border-gray-800 rounded-2xl 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>
</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-4 font-mono font-bold" x-text="month.month"></td>
<td class="p-4" x-text="month.total_invoices"></td>
<td class="p-4 text-emerald-500" x-text="month.approved_count"></td>
<td class="p-4 font-bold" x-text="parseFloat(month.total_amount).toLocaleString() + ' JOD'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</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-3xl shadow-2xl flex overflow-hidden glass">
@@ -325,17 +385,17 @@
</div>
</div>
<!-- Official JoFotara Submission Display -->
<template x-if="currentInvoice?.jofotara">
<!-- Official / Local QR Display -->
<template x-if="currentInvoice?.qr_code || currentInvoice?.jofotara?.qr_image_uri">
<div class="p-6 bg-emerald-950/20 border border-emerald-500/30 rounded-2xl flex items-center gap-8">
<div class="bg-white p-2 rounded-lg shadow-xl">
<img :src="currentInvoice.jofotara.qr_image_uri" class="w-32 h-32" alt="QR Code">
<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-2 flex-1">
<h4 class="text-emerald-500 font-bold"> فاتورة معتمدة رسمياً</h4>
<p class="text-xs text-gray-400">الرقم الموحد (UUID): <span class="font-mono select-all text-gray-200" x-text="currentInvoice.jofotara.uuid"></span></p>
<p class="text-xs text-gray-400">تاريخ الرفع: <span x-text="currentInvoice.jofotara.submitted_at"></span></p>
<div class="pt-2 flex gap-3">
<h4 class="text-emerald-500 font-bold" x-text="currentInvoice?.jofotara ? '✅ فاتورة معتمدة رسمياً' : '✅ تم الاعتماد محلياً'"></h4>
<p class="text-xs text-gray-400">الرقم الموحد: <span class="font-mono select-all text-gray-200" x-text="currentInvoice?.jofotara?.uuid || currentInvoice?.id"></span></p>
<p class="text-xs text-gray-400">تاريخ الرفع: <span x-text="currentInvoice?.jofotara?.submitted_at || currentInvoice?.updated_at"></span></p>
<div class="pt-2 flex gap-3" 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-[10px] bg-emerald-600/20 hover:bg-emerald-600/40 text-emerald-400 px-4 py-2 rounded-lg transition-all border border-emerald-500/30">
⬇️ تحميل ملف XML الرسمي
@@ -352,73 +412,18 @@
class="flex-1 bg-emerald-600 hover:bg-emerald-500 py-3 rounded-xl font-bold transition flex items-center justify-center gap-2 disabled:opacity-50"
:disabled="isApproving">
<span x-show="!isApproving"> اعتماد الفاتورة وتوليد QR</span>
<span x-show="isApproving">جارِ الإرسال إلى جوفوترة... </span>
<span x-show="isApproving" class="flex items-center gap-2">
<svg class="animate-spin h-4 w-4 text-white" 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>
</template>
<template x-if="currentInvoice?.status === 'approved' && !currentInvoice.jofotara">
<div class="flex-1 flex flex-col items-center justify-center bg-gray-900 rounded-xl p-4 border border-emerald-500/20">
<span class="text-xs text-emerald-500 font-bold mb-2">تم الاعتماد محلياً</span>
</div>
</template>
<button @click="showViewModal = false" class="px-8 py-3 border border-gray-800 rounded-xl hover:bg-gray-800 transition text-sm">إغلاق</button>
</div>
</div>
</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-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">جارِ التحليل...</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 -->
<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-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><input type="text" x-model="newUser.name" placeholder="الاسم الكامل" class="w-full bg-gray-950 border border-gray-800 p-3 rounded-xl outline-none" required></div>
<div><input type="email" x-model="newUser.email" placeholder="البريد الإلكتروني" class="w-full bg-gray-950 border border-gray-800 p-3 rounded-xl outline-none" required></div>
<div><input type="password" x-model="newUser.password" placeholder="كلمة المرور" class="w-full bg-gray-950 border border-gray-800 p-3 rounded-xl outline-none" required></div>
<div>
<select x-model="newUser.role" class="w-full bg-gray-950 border border-gray-800 p-3 rounded-xl outline-none">
<option value="employee">موظف</option><option value="accountant">محاسب</option><option value="admin">مدير مكتب</option>
</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>
@@ -437,6 +442,7 @@
showAddTenantModal: false,
showUploadModal: false,
showViewModal: false,
showCompanyStatsModal: false,
isUploading: false,
isApproving: false,
globalError: '',
@@ -445,6 +451,7 @@
uploadData: { company_id: '' },
selectedFile: null,
currentInvoice: null,
companyStats: null,
init() {
if (!this.user) { window.location.href = '/login.php'; return; }
@@ -472,20 +479,24 @@
};
if (body) options.body = JSON.stringify(body);
const res = await fetch('/index.php?route=' + route, options);
// Session Expired Check
if (res.status === 401) {
const json = await res.json();
if (json.code === 'TOKEN_EXPIRED') {
localStorage.clear();
window.location.href = '/login.php';
return null;
try {
const res = await fetch('/index.php?route=' + route, options);
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();
return json.success ? json.data : (this.showError(json.message), null);
const json = await res.json();
return json.success ? json.data : (this.showError(json.message), null);
} catch (e) {
this.showError('فشل الاتصال بالخادم');
return null;
}
},
async apiGet(route) { return this.apiRequest(route); },
@@ -504,6 +515,14 @@
async loadStats() { this.stats = await this.apiGet('v1/dashboard/stats') || {}; },
async loadTenants() { this.tenants = await this.apiGet('v1/tenants') || []; },
async showCompanyStats(id) {
const data = await this.apiGet('v1/companies/stats&id=' + id);
if (data) {
this.companyStats = data;
this.showCompanyStatsModal = true;
}
},
async viewInvoice(id) {
const data = await this.apiGet('v1/invoices/view&id=' + id);
if (data) {
@@ -513,14 +532,15 @@
},
async approveInvoice(id) {
if (!confirm('هل أنت متأكد من اعتماد الفاتورة وإرسالها إلى جوفوترة؟')) return;
if (!confirm('هل أنت متأكد من اعتماد الفاتورة؟')) return;
this.isApproving = true;
try {
const data = await this.apiRequest('v1/invoices/approve', 'POST', { id: id });
if (data) {
alert('✅ تم الاعتماد بنجاح!');
this.viewInvoice(id); // Reload to show QR
alert(data.message || '✅ تم الاعتماد بنجاح!');
this.viewInvoice(id);
this.loadInvoices();
this.loadStats();
}
} catch (e) {
this.showError('خطأ غير متوقع: ' + e.message);
@@ -531,12 +551,16 @@
generateQRPng(base64Tlv) {
if (!base64Tlv) return '';
const qr = new QRious({
value: base64Tlv,
size: 200,
level: 'M'
});
return qr.toDataURL().split(',')[1];
try {
const qr = new QRious({
value: base64Tlv,
size: 200,
level: 'M'
});
return qr.toDataURL().split(',')[1];
} catch (e) {
return '';
}
},
handleFile(e) { this.selectedFile = e.target.files[0]; },
@@ -548,19 +572,24 @@
formData.append('company_id', this.uploadData.company_id);
formData.append('invoice', this.selectedFile);
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);
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('فشل رفع الملف');
}
},