Update: 2026-05-11 01:09:54
This commit is contained in:
49
app/modules_app/invoices/delete.php
Normal file
49
app/modules_app/invoices/delete.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Delete Invoice
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\AuditLogger;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\RoleMiddleware;
|
||||
|
||||
$decoded = RoleMiddleware::require(['super_admin', 'admin', 'accountant']);
|
||||
$db = Database::getInstance();
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $data['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
json_error('Invoice ID is required', 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM invoices WHERE id = ? FOR UPDATE");
|
||||
$stmt->execute([$id]);
|
||||
$invoice = $stmt->fetch();
|
||||
|
||||
if (!$invoice) json_error('Invoice not found', 404);
|
||||
|
||||
// Super admin can delete anything. Others might only delete non-approved, but let's allow admin to delete.
|
||||
if ($decoded['role'] !== 'super_admin' && $invoice['tenant_id'] !== $decoded['tenant_id']) {
|
||||
json_error('Access denied', 403);
|
||||
}
|
||||
|
||||
$db->prepare("DELETE FROM invoice_lines WHERE invoice_id = ?")->execute([$id]);
|
||||
$db->prepare("DELETE FROM jofotara_submissions WHERE invoice_id = ?")->execute([$id]);
|
||||
$db->prepare("DELETE FROM invoices WHERE id = ?")->execute([$id]);
|
||||
|
||||
$db->commit();
|
||||
|
||||
AuditLogger::log('invoice.deleted', 'invoice', $id, null, null, $decoded);
|
||||
|
||||
json_success(null, 'تم حذف الفاتورة بنجاح');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
if ($db->inTransaction()) $db->rollBack();
|
||||
error_log("Invoice Delete Error: " . $e->getMessage());
|
||||
json_error('فشل في حذف الفاتورة', 500);
|
||||
}
|
||||
48
app/modules_app/invoices/reject.php
Normal file
48
app/modules_app/invoices/reject.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Reject Invoice
|
||||
*/
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\AuditLogger;
|
||||
use App\Middleware\AuthMiddleware;
|
||||
use App\Middleware\RoleMiddleware;
|
||||
|
||||
$decoded = RoleMiddleware::require(['super_admin', 'admin', 'accountant']);
|
||||
$db = Database::getInstance();
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $data['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
json_error('Invoice ID is required', 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$db->beginTransaction();
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM invoices WHERE id = ? FOR UPDATE");
|
||||
$stmt->execute([$id]);
|
||||
$invoice = $stmt->fetch();
|
||||
|
||||
if (!$invoice) json_error('Invoice not found', 404);
|
||||
if ($invoice['status'] === 'approved') json_error('لا يمكن رفض فاتورة معتمدة', 400);
|
||||
|
||||
$updateStmt = $db->prepare("UPDATE invoices SET status = 'rejected', updated_at = NOW() WHERE id = ?");
|
||||
$updateStmt->execute([$id]);
|
||||
|
||||
$db->commit();
|
||||
|
||||
AuditLogger::log('invoice.rejected', 'invoice', $id, [
|
||||
'old_status' => $invoice['status'],
|
||||
], [
|
||||
'new_status' => 'rejected',
|
||||
], $decoded);
|
||||
|
||||
json_success(null, 'تم رفض الفاتورة بنجاح');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
if ($db->inTransaction()) $db->rollBack();
|
||||
error_log("Invoice Reject Error: " . $e->getMessage());
|
||||
json_error('فشل في رفض الفاتورة', 500);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:get/get.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import '../../../core/services/upload_progress_service.dart';
|
||||
import '../../../core/utils/logger.dart';
|
||||
import '../../../core/utils/app_snackbar.dart';
|
||||
@@ -91,6 +92,11 @@ class ScannerController extends GetxController {
|
||||
capturedImages.add(originalFile);
|
||||
int index = capturedImages.length - 1;
|
||||
|
||||
if (imagePath.toLowerCase().endsWith('.pdf')) {
|
||||
AppLogger.print('Added PDF file, skipping image processing: $imagePath');
|
||||
return;
|
||||
}
|
||||
|
||||
ImageProcessingService.processInvoiceImage(originalFile)
|
||||
.then((processedFile) {
|
||||
if (processedFile != null && index < capturedImages.length) {
|
||||
@@ -102,6 +108,28 @@ class ScannerController extends GetxController {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> pickPdfFile() async {
|
||||
try {
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf'],
|
||||
allowMultiple: true,
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
for (var file in result.files) {
|
||||
if (file.path != null) {
|
||||
addImage(file.path!);
|
||||
}
|
||||
}
|
||||
AppSnackbar.showSuccess('تمت الإضافة', 'تم استيراد ملفات الفواتير بنجاح');
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.error('Failed to pick PDF', e);
|
||||
AppSnackbar.showError('خطأ', 'تعذر استيراد الملفات');
|
||||
}
|
||||
}
|
||||
|
||||
void removeImage(int index) {
|
||||
if (index >= 0 && index < capturedImages.length) {
|
||||
capturedImages.removeAt(index);
|
||||
|
||||
@@ -119,6 +119,18 @@ class ScannerView extends GetView<ScannerController> {
|
||||
state: state,
|
||||
children: [
|
||||
AwesomeFlashButton(state: state),
|
||||
const SizedBox(width: 16),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black45,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => controller.pickPdfFile(),
|
||||
icon: const Icon(Icons.picture_as_pdf, color: Colors.white),
|
||||
tooltip: 'استيراد PDF',
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: () => Get.back(),
|
||||
|
||||
@@ -353,6 +353,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.7"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -31,6 +31,7 @@ dependencies:
|
||||
camerawesome: ^2.0.0
|
||||
cunning_document_scanner: ^1.2.3
|
||||
image_picker: ^1.0.7
|
||||
file_picker: ^8.1.2
|
||||
|
||||
# ─── Image Processing ───────────────────────────────
|
||||
image: ^4.1.7
|
||||
|
||||
@@ -35,6 +35,8 @@ $routes = [
|
||||
'v1/invoices/download_xml' => ['GET', 'invoices/download_xml.php'],
|
||||
'v1/invoices/submit-jofotara' => ['POST', 'invoices/submit_jofotara.php'],
|
||||
'v1/invoices/update' => ['POST', 'invoices/update.php'],
|
||||
'v1/invoices/reject' => ['POST', 'invoices/reject.php'],
|
||||
'v1/invoices/delete' => ['POST', 'invoices/delete.php'],
|
||||
'v1/invoices/bulk-approve' => ['POST', 'invoices/bulk_approve.php'],
|
||||
'v1/invoices/export' => ['GET', 'invoices/export.php'],
|
||||
'v1/invoices/check-duplicate' => ['POST', 'invoices/check_duplicate.php'],
|
||||
|
||||
144
public/shell.php
144
public/shell.php
@@ -1759,6 +1759,9 @@
|
||||
<button @click="viewInvoice(inv.id)" class="btn-table-action btn-ta-navy">
|
||||
👁️ عرض
|
||||
</button>
|
||||
<button @click="deleteInvoice(inv.id)" class="btn-table-action btn-ta-red" style="margin-right:4px;">
|
||||
🗑️ حذف
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -2395,11 +2398,11 @@
|
||||
<!-- ── VIEW INVOICE MODAL ──────────────────────────── -->
|
||||
<div x-show="showViewModal" x-cloak class="modal-backdrop" @click.self="showViewModal = false">
|
||||
<div
|
||||
style="background:var(--bg-card); border-radius:22px; box-shadow:var(--shadow-lg); width:100%; max-width:900px; height:88vh; display:flex; overflow:hidden;">
|
||||
style="background:var(--bg-card); border-radius:22px; box-shadow:var(--shadow-lg); width:100%; max-width:95vw; height:95vh; display:flex; overflow:hidden;">
|
||||
|
||||
<!-- Document Preview -->
|
||||
<div
|
||||
style="flex:1; background:#F2F1FA; border-left:1px solid var(--border); position:relative; overflow:hidden;">
|
||||
style="flex:3; background:#F2F1FA; border-left:1px solid var(--border); position:relative; overflow:hidden;">
|
||||
<div style="position:absolute; top:12px; right:12px; z-index:10;">
|
||||
<span class="badge badge-navy">معاينة الملف</span>
|
||||
</div>
|
||||
@@ -2415,15 +2418,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Invoice Data Panel -->
|
||||
<div style="width:345px; flex-shrink:0; display:flex; flex-direction:column; overflow:hidden;">
|
||||
<div style="width:25%; min-width:320px; max-width:450px; flex-shrink:0; display:flex; flex-direction:column; overflow:hidden;">
|
||||
<!-- Header -->
|
||||
<div
|
||||
style="padding:18px 20px 14px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between;">
|
||||
<div>
|
||||
<div style="font-size:16px; font-weight:700; color:var(--text-1);">تفاصيل الفاتورة</div>
|
||||
<div style="font-size:16px; font-weight:700; color:var(--text-1); display:flex; gap:10px; align-items:center;">
|
||||
تفاصيل الفاتورة
|
||||
<button x-show="currentInvoice?.status === 'extracted' && !isEditingInvoice" @click="isEditingInvoice = true" class="btn-sm btn-ghost" style="font-size:11px; padding:2px 8px;">تعديل ✏️</button>
|
||||
<button x-show="isEditingInvoice" @click="updateInvoice" class="btn-sm btn-teal" style="font-size:11px; padding:2px 8px;">حفظ 💾</button>
|
||||
<button x-show="isEditingInvoice" @click="isEditingInvoice = false" class="btn-sm btn-ghost" style="font-size:11px; padding:2px 8px;">إلغاء ✕</button>
|
||||
</div>
|
||||
<span class="badge" style="margin-top:4px;"
|
||||
:class="currentInvoice?.status==='extracted' ? 'badge-blue' : (currentInvoice?.status==='approved' ? 'badge-teal' : 'badge-gray')"
|
||||
x-text="currentInvoice?.status === 'approved' ? '✓ مدققة' : (currentInvoice?.status === 'extracted' ? 'جاهزة للتدقيق' : currentInvoice?.status)">
|
||||
:class="currentInvoice?.status==='extracted' ? 'badge-blue' : (currentInvoice?.status==='approved' ? 'badge-teal' : (currentInvoice?.status==='rejected' ? 'badge-red' : 'badge-gray'))"
|
||||
x-text="currentInvoice?.status === 'approved' ? '✓ مدققة' : (currentInvoice?.status === 'extracted' ? 'جاهزة للتدقيق' : (currentInvoice?.status === 'rejected' ? 'مرفوضة' : currentInvoice?.status))">
|
||||
</span>
|
||||
</div>
|
||||
<button @click="showViewModal = false" class="modal-close-btn">✕</button>
|
||||
@@ -2455,26 +2463,35 @@
|
||||
|
||||
<div class="invoice-field-card">
|
||||
<div class="invoice-field-label">المورد (البائع)</div>
|
||||
<div class="invoice-field-value" x-text="currentInvoice?.supplier_name || 'غير متوفر'"></div>
|
||||
<div style="font-size:12px; color:var(--text-3); font-family:'Outfit',sans-serif; margin-top:4px;"
|
||||
<div x-show="!isEditingInvoice" class="invoice-field-value" x-text="currentInvoice?.supplier_name || 'غير متوفر'"></div>
|
||||
<input x-show="isEditingInvoice" type="text" x-model="currentInvoice.supplier_name" class="form-input" style="padding:4px; font-size:13px; margin-bottom:4px;" placeholder="اسم المورد">
|
||||
|
||||
<div x-show="!isEditingInvoice" style="font-size:12px; color:var(--text-3); font-family:'Outfit',sans-serif; margin-top:4px;"
|
||||
x-text="'TIN: ' + (currentInvoice?.supplier_tin || '—')"></div>
|
||||
<input x-show="isEditingInvoice" type="text" x-model="currentInvoice.supplier_tin" class="form-input" placeholder="الرقم الضريبي" style="padding:4px; font-size:12px;">
|
||||
</div>
|
||||
|
||||
<div class="invoice-field-card">
|
||||
<div class="invoice-field-label">رقم الفاتورة والتاريخ</div>
|
||||
<div class="invoice-field-value" x-text="currentInvoice?.invoice_number || '—'"></div>
|
||||
<div style="font-size:13px; color:var(--text-2); font-family:'Outfit',sans-serif; margin-top:3px;"
|
||||
<div x-show="!isEditingInvoice" class="invoice-field-value" x-text="currentInvoice?.invoice_number || '—'"></div>
|
||||
<input x-show="isEditingInvoice" type="text" x-model="currentInvoice.invoice_number" class="form-input" style="padding:4px; font-size:13px; margin-bottom:4px;" placeholder="رقم الفاتورة">
|
||||
|
||||
<div x-show="!isEditingInvoice" style="font-size:13px; color:var(--text-2); font-family:'Outfit',sans-serif; margin-top:3px;"
|
||||
x-text="currentInvoice?.invoice_date || '—'"></div>
|
||||
<input x-show="isEditingInvoice" type="date" x-model="currentInvoice.invoice_date" class="form-input" style="padding:4px; font-size:13px;">
|
||||
</div>
|
||||
|
||||
<div class="invoice-field-card"
|
||||
style="background:var(--green-subtle); border-color:rgba(5,150,105,0.2);">
|
||||
<div class="invoice-field-label" style="color:var(--green-mid);">المجموع الكلي</div>
|
||||
<div style="font-size:26px; font-weight:700; color:var(--green-mid); font-family:'El Messiri',sans-serif;"
|
||||
<div x-show="!isEditingInvoice" style="font-size:26px; font-weight:700; color:var(--green-mid); font-family:'El Messiri',sans-serif;"
|
||||
x-text="parseFloat(currentInvoice?.grand_total || 0).toLocaleString() + ' JOD'"></div>
|
||||
<div style="font-size:12px; color:var(--amber-mid); margin-top:4px; font-family:'Outfit',sans-serif;"
|
||||
<input x-show="isEditingInvoice" type="number" step="0.01" x-model="currentInvoice.grand_total" class="form-input" style="padding:4px; font-size:16px; font-weight:bold; color:var(--green-mid); margin-bottom:4px;" placeholder="المجموع">
|
||||
|
||||
<div x-show="!isEditingInvoice" style="font-size:12px; color:var(--amber-mid); margin-top:4px; font-family:'Outfit',sans-serif;"
|
||||
x-text="'الضريبة: ' + parseFloat(currentInvoice?.tax_amount || 0).toLocaleString() + ' JOD'">
|
||||
</div>
|
||||
<input x-show="isEditingInvoice" type="number" step="0.01" x-model="currentInvoice.tax_amount" class="form-input" style="padding:4px; font-size:13px; color:var(--amber-mid);" placeholder="الضريبة">
|
||||
</div>
|
||||
|
||||
<!-- Items Table -->
|
||||
@@ -2562,10 +2579,18 @@
|
||||
<span x-show="isBusy">⏳ جاري التدقيق...</span>
|
||||
</button>
|
||||
|
||||
<button x-show="currentInvoice?.status === 'extracted'"
|
||||
style="width:100%; background:var(--red-subtle); color:var(--red-mid); border:none; padding:10px; border-radius:11px; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:all 0.18s;"
|
||||
onmouseover="this.style.background='#FECACA'"
|
||||
onmouseout="this.style.background='var(--red-subtle)'">❌ رفض الفاتورة</button>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<button x-show="currentInvoice?.status === 'extracted' || currentInvoice?.status === 'rejected'"
|
||||
@click="rejectInvoice(currentInvoice.id)"
|
||||
style="flex:1; background:var(--amber-subtle); color:var(--amber-mid); border:none; padding:10px; border-radius:11px; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:all 0.18s;"
|
||||
onmouseover="this.style.background='#FDE68A'"
|
||||
onmouseout="this.style.background='var(--amber-subtle)'">❌ رفض</button>
|
||||
|
||||
<button @click="deleteInvoice(currentInvoice.id)"
|
||||
style="flex:1; background:var(--red-subtle); color:var(--red-mid); border:none; padding:10px; border-radius:11px; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:all 0.18s;"
|
||||
onmouseover="this.style.background='#FECACA'"
|
||||
onmouseout="this.style.background='var(--red-subtle)'">🗑️ حذف</button>
|
||||
</div>
|
||||
|
||||
<div x-show="currentInvoice?.status === 'approved'"
|
||||
style="width:100%; background:var(--green-subtle); color:var(--green-mid); border:1px solid rgba(5,150,105,0.2); padding:10px; border-radius:11px; font-size:14px; font-weight:700; text-align:center;">
|
||||
@@ -2829,7 +2854,7 @@
|
||||
showExcelModal: false, showBatchUploadModal: false,
|
||||
isUploadingBatch: false, batchProgress: { total: 0, current: 0 },
|
||||
showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false,
|
||||
acknowledgedWarnings: false,
|
||||
acknowledgedWarnings: false, isEditingInvoice: false,
|
||||
isBusy: false, globalError: '',
|
||||
|
||||
newUser: { name: '', email: '', password: '', role: 'accountant', tenant_id: '' },
|
||||
@@ -2849,14 +2874,8 @@
|
||||
subtitle() { return { dashboard: 'نظرة شاملة على نشاط النظام', users: 'إدارة المستخدمين والصلاحيات', companies: 'إدارة الشركات والربط بالفوترة الحكومية', invoices: 'رفع ومعالجة الفواتير الضريبية', tenants: 'إدارة المكاتب المحاسبية المشتركة', subscription: 'تفاصيل باقتك الحالية واستهلاك الموارد' }[this.page] || ''; },
|
||||
|
||||
get filteredInvoices() {
|
||||
const now = new Date();
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
return this.invoices.filter(inv => {
|
||||
// 1. Time Filter (Current Month)
|
||||
const invDate = new Date(inv.invoice_date || inv.created_at);
|
||||
if (invDate < startOfMonth) return false;
|
||||
|
||||
// Removed time filter to prevent invoices from disappearing after a month
|
||||
// 2. Company Filter
|
||||
if (this.invoiceCompanyFilter && inv.company_id != this.invoiceCompanyFilter) return false;
|
||||
|
||||
@@ -3155,6 +3174,83 @@
|
||||
}
|
||||
},
|
||||
|
||||
async rejectInvoice(id) {
|
||||
if (this.isBusy) return;
|
||||
if (!confirm('هل أنت متأكد من رفض هذه الفاتورة؟')) return;
|
||||
this.isBusy = true;
|
||||
try {
|
||||
const res = await fetch('/index.php?route=v1/invoices/reject', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: id })
|
||||
});
|
||||
const json = await res.json();
|
||||
this.isBusy = false;
|
||||
|
||||
if (json.success) {
|
||||
alert('تم رفض الفاتورة بنجاح!');
|
||||
this.showViewModal = false;
|
||||
this.loadAll();
|
||||
} else {
|
||||
this.showError(json.message);
|
||||
}
|
||||
} catch (e) {
|
||||
this.isBusy = false;
|
||||
this.showError('حدث خطأ أثناء رفض الفاتورة');
|
||||
}
|
||||
},
|
||||
|
||||
async deleteInvoice(id) {
|
||||
if (this.isBusy) return;
|
||||
if (!confirm('هل أنت متأكد من حذف هذه الفاتورة بشكل نهائي؟ لا يمكن التراجع عن هذا الإجراء.')) return;
|
||||
this.isBusy = true;
|
||||
try {
|
||||
const res = await fetch('/index.php?route=v1/invoices/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: id })
|
||||
});
|
||||
const json = await res.json();
|
||||
this.isBusy = false;
|
||||
|
||||
if (json.success) {
|
||||
alert('تم حذف الفاتورة بنجاح!');
|
||||
this.showViewModal = false;
|
||||
this.loadAll();
|
||||
} else {
|
||||
this.showError(json.message);
|
||||
}
|
||||
} catch (e) {
|
||||
this.isBusy = false;
|
||||
this.showError('حدث خطأ أثناء حذف الفاتورة');
|
||||
}
|
||||
},
|
||||
|
||||
async updateInvoice() {
|
||||
if (!this.currentInvoice || this.isBusy) return;
|
||||
this.isBusy = true;
|
||||
try {
|
||||
const res = await fetch('/index.php?route=v1/invoices/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.currentInvoice)
|
||||
});
|
||||
const json = await res.json();
|
||||
this.isBusy = false;
|
||||
|
||||
if (json.success) {
|
||||
alert('تم تحديث بيانات الفاتورة بنجاح!');
|
||||
this.isEditingInvoice = false;
|
||||
this.loadAll();
|
||||
} else {
|
||||
this.showError(json.message);
|
||||
}
|
||||
} catch (e) {
|
||||
this.isBusy = false;
|
||||
this.showError('حدث خطأ أثناء تحديث الفاتورة');
|
||||
}
|
||||
},
|
||||
|
||||
async uploadExcel() {
|
||||
const fileInput = document.getElementById('excelFileInput');
|
||||
if (!fileInput.files[0]) return alert('الرجاء اختيار ملف اكسل');
|
||||
|
||||
Reference in New Issue
Block a user