Update: 2026-05-09 13:10:07
This commit is contained in:
@@ -47,10 +47,10 @@ if ($batch['status'] !== 'uploading') {
|
||||
}
|
||||
|
||||
// 3. Validate file type
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif'];
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif', 'application/pdf'];
|
||||
$mimeType = $_FILES['image']['type'];
|
||||
if (!in_array($mimeType, $allowedTypes)) {
|
||||
json_error('نوع الملف غير مدعوم. المسموح: JPEG, PNG, WebP, HEIC', 422);
|
||||
json_error('نوع الملف غير مدعوم. المسموح: صور و PDF', 422);
|
||||
}
|
||||
|
||||
// 4. Validate file size (max 10MB)
|
||||
|
||||
163
public/shell.php
163
public/shell.php
@@ -1137,6 +1137,9 @@
|
||||
<button x-show="page==='invoices'" @click="showUploadModal = true" class="btn btn-teal">
|
||||
<span>📤</span> رفع فاتورة
|
||||
</button>
|
||||
<button x-show="page==='invoices'" @click="showBatchUploadModal = true" class="btn btn-navy">
|
||||
<span>📁</span> استيراد مجمع (Batch)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1925,6 +1928,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── BATCH UPLOAD MODAL ───────────────────────────── -->
|
||||
<div x-show="showBatchUploadModal" x-cloak class="modal-backdrop">
|
||||
<div class="modal-box">
|
||||
<div class="modal-head">
|
||||
<div class="modal-head-icon navy">📁</div>
|
||||
<div style="flex:1;">
|
||||
<div class="modal-title">رفع مجمع للفواتير (Batch Processing)</div>
|
||||
<div class="modal-subtitle">اختر عدة فواتير لمعالجتها في الخلفية</div>
|
||||
</div>
|
||||
<button @click="showBatchUploadModal = false" class="modal-close-btn">✕</button>
|
||||
</div>
|
||||
<div class="modal-divider"></div>
|
||||
<form @submit.prevent="uploadBatchInvoices">
|
||||
<div class="modal-body" style="display:flex; flex-direction:column; gap:14px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">اختر الشركة</label>
|
||||
<select x-model="uploadData.company_id" class="form-input" required :disabled="isUploadingBatch">
|
||||
<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="form-group">
|
||||
<label class="form-label">ملفات الفواتير (متعدد)</label>
|
||||
<div style="font-size:12px; color:var(--text-3); margin-bottom:6px;">مدعوم: صور أو PDF (يمكنك تحديد أكثر من ملف)</div>
|
||||
<input type="file" id="batchFileInput" multiple accept="image/*,application/pdf" class="form-input"
|
||||
style="padding:8px;" required :disabled="isUploadingBatch">
|
||||
</div>
|
||||
<div x-show="isUploadingBatch" style="margin-top:10px;">
|
||||
<div style="font-size:12px; font-weight:bold; color:var(--navy); margin-bottom:4px; display:flex; justify-content:space-between;">
|
||||
<span>جاري رفع الدفعة...</span>
|
||||
<span x-text="batchProgress.current + ' / ' + batchProgress.total"></span>
|
||||
</div>
|
||||
<div style="height:6px; background:#e2e8f0; border-radius:3px; overflow:hidden;">
|
||||
<div style="height:100%; background:var(--navy); transition:width 0.3s;" :style="'width:' + (batchProgress.total ? (batchProgress.current/batchProgress.total*100) : 0) + '%'"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-divider"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-navy" :disabled="isUploadingBatch" style="flex:1;">
|
||||
<span x-show="!isUploadingBatch">📁 بدء الرفع المجمع</span>
|
||||
<span x-show="isUploadingBatch">⏳ يرجى الانتظار...</span>
|
||||
</button>
|
||||
<button type="button" @click="showBatchUploadModal = false" class="btn btn-ghost" :disabled="isUploadingBatch">إلغاء</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── VIEW INVOICE MODAL ───────────────────────────── -->
|
||||
<div x-show="showViewModal" x-cloak class="modal-backdrop" @click.self="showViewModal = false">
|
||||
<div
|
||||
@@ -2010,37 +2064,30 @@
|
||||
|
||||
<!-- Items Table -->
|
||||
<div x-show="currentInvoice?.items?.length > 0"
|
||||
style="border:1px solid var(--border); border-radius:10px; overflow:hidden;">
|
||||
style="border:1px solid var(--border); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; max-height:300px;">
|
||||
<div
|
||||
style="padding:8px 12px; background:#f8fafc; font-size:11px; font-weight:700; color:var(--text-3); text-transform:uppercase; letter-spacing:0.06em;">
|
||||
style="padding:8px 12px; background:#f8fafc; font-size:11px; font-weight:700; color:var(--text-3); text-transform:uppercase; letter-spacing:0.06em; flex-shrink:0; position:sticky; top:0; z-index:2;">
|
||||
بنود الفاتورة</div>
|
||||
<table style="width:100%; border-collapse:collapse; font-size:12px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
style="padding:8px 12px; text-align:right; color:var(--text-3); font-size:11px; border-bottom:1px solid var(--border);">
|
||||
البند</th>
|
||||
<th
|
||||
style="padding:8px 12px; text-align:center; color:var(--text-3); font-size:11px; border-bottom:1px solid var(--border);">
|
||||
الكمية</th>
|
||||
<th
|
||||
style="padding:8px 12px; text-align:left; color:var(--text-3); font-size:11px; border-bottom:1px solid var(--border);">
|
||||
السعر</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="item in currentInvoice?.items" :key="item.id">
|
||||
<tr style="border-bottom:1px solid #f0f4f8;">
|
||||
<td style="padding:8px 12px; color:var(--text-2);" x-text="item.description">
|
||||
</td>
|
||||
<td style="padding:8px 12px; text-align:center; color:var(--text-3); font-family:'IBM Plex Mono',monospace;"
|
||||
x-text="item.quantity"></td>
|
||||
<td style="padding:8px 12px; text-align:left; color:var(--teal); font-family:'IBM Plex Mono',monospace;"
|
||||
x-text="item.unit_price"></td>
|
||||
<div style="overflow-y:auto; flex:1;">
|
||||
<table style="width:100%; border-collapse:collapse; font-size:12px;">
|
||||
<thead style="position:sticky; top:0; background:white; z-index:1; box-shadow:0 1px 2px rgba(0,0,0,0.05);">
|
||||
<tr>
|
||||
<th style="padding:8px 12px; text-align:right; color:var(--text-3); font-size:11px; border-bottom:1px solid var(--border);">البند</th>
|
||||
<th style="padding:8px 12px; text-align:center; color:var(--text-3); font-size:11px; border-bottom:1px solid var(--border);">الكمية</th>
|
||||
<th style="padding:8px 12px; text-align:left; color:var(--text-3); font-size:11px; border-bottom:1px solid var(--border);">السعر</th>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="item in currentInvoice?.items" :key="item.id">
|
||||
<tr style="border-bottom:1px solid #f0f4f8;">
|
||||
<td style="padding:8px 12px; color:var(--text-2);" x-text="item.description"></td>
|
||||
<td style="padding:8px 12px; text-align:center; color:var(--text-3); font-family:'IBM Plex Mono',monospace;" x-text="item.quantity"></td>
|
||||
<td style="padding:8px 12px; text-align:left; color:var(--teal); font-family:'IBM Plex Mono',monospace;" x-text="item.unit_price"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code -->
|
||||
@@ -2298,7 +2345,8 @@
|
||||
|
||||
showAddUserModal: false, showAddCompanyModal: false, showConnectModal: false,
|
||||
showUploadModal: false, showViewModal: false, showCompanyStatsModal: false,
|
||||
showExcelModal: false,
|
||||
showExcelModal: false, showBatchUploadModal: false,
|
||||
isUploadingBatch: false, batchProgress: { total: 0, current: 0 },
|
||||
showAddTenantModal: false, showEditTenantModal: false, showTenantStatsModal: false,
|
||||
acknowledgedWarnings: false,
|
||||
isBusy: false, globalError: '',
|
||||
@@ -2509,6 +2557,63 @@
|
||||
}
|
||||
},
|
||||
|
||||
async uploadBatchInvoices() {
|
||||
const fileInput = document.getElementById('batchFileInput');
|
||||
if (!fileInput.files.length) return alert('الرجاء اختيار ملفات');
|
||||
if (!this.uploadData.company_id) return alert('الرجاء اختيار الشركة');
|
||||
|
||||
this.isUploadingBatch = true;
|
||||
this.batchProgress = { total: fileInput.files.length, current: 0 };
|
||||
|
||||
try {
|
||||
// 1. Create batch
|
||||
const batchRes = await fetch('/index.php?route=v1/batches/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ company_id: this.uploadData.company_id, expected_images: fileInput.files.length, source: 'web_batch' })
|
||||
}).then(r => r.json());
|
||||
|
||||
if (!batchRes.success) {
|
||||
this.isUploadingBatch = false;
|
||||
return this.showError(batchRes.message);
|
||||
}
|
||||
|
||||
const batchId = batchRes.data.batch_id;
|
||||
|
||||
// 2. Upload files sequentially
|
||||
for (let i = 0; i < fileInput.files.length; i++) {
|
||||
const file = fileInput.files[i];
|
||||
const formData = new FormData();
|
||||
formData.append('batch_id', batchId);
|
||||
formData.append('image_order', i+1);
|
||||
formData.append('image', file);
|
||||
|
||||
await fetch('/index.php?route=v1/batches/upload-image', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + this.token() },
|
||||
body: formData
|
||||
});
|
||||
this.batchProgress.current = i + 1;
|
||||
}
|
||||
|
||||
// 3. Finalize
|
||||
await fetch('/index.php?route=v1/batches/finalize', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + this.token(), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ batch_id: batchId })
|
||||
});
|
||||
|
||||
this.isUploadingBatch = false;
|
||||
this.showBatchUploadModal = false;
|
||||
alert('تم رفع الدفعة بنجاح! جاري معالجتها في الخلفية.');
|
||||
this.loadAll();
|
||||
fileInput.value = '';
|
||||
} catch (e) {
|
||||
this.isUploadingBatch = false;
|
||||
this.showError('فشل الاتصال بالخادم أثناء الرفع المجمع');
|
||||
}
|
||||
},
|
||||
|
||||
async approveInvoice() {
|
||||
if (!this.currentInvoice || this.isBusy) return;
|
||||
this.isBusy = true;
|
||||
|
||||
Reference in New Issue
Block a user