Update: 2026-05-09 13:10:07

This commit is contained in:
Hamza-Ayed
2026-05-09 13:10:07 +03:00
parent 9159e2d274
commit c0896468a7
2 changed files with 136 additions and 31 deletions

View File

@@ -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)

View File

@@ -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;