= ?'; $params[] = $dateFrom; } if ($dateTo) { $where[] = 'i.invoice_date <= ?'; $params[] = $dateTo; } if ($status) { $where[] = 'i.status = ?'; $params[] = $status; } $whereClause = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $stmt = $db->prepare(" SELECT i.*, c.name as company_name_raw FROM invoices i JOIN companies c ON i.company_id = c.id $whereClause ORDER BY i.invoice_date DESC LIMIT 5000 "); $stmt->execute($params); $invoices = $stmt->fetchAll(); if (empty($invoices)) { json_error('لا توجد فواتير لتصديرها', 404); } // Decrypt helper $dec = function($val) { if (empty($val)) return ''; $result = Encryption::decrypt((string)$val); return ($result !== false && $result !== null) ? $result : (string)$val; }; // Robust download helper for QR codes $downloadUrl = function($url) { $data = @file_get_contents($url); if ($data === false && function_exists('curl_init')) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $data = curl_exec($ch); curl_close($ch); } return $data; }; // ══════════════════════════════════════════ // BUILD SPREADSHEET // ══════════════════════════════════════════ $spreadsheet = new Spreadsheet(); $spreadsheet->getProperties() ->setCreator('مُصادَق - Musadaq') ->setTitle('تقرير الفواتير') ->setDescription('تقرير فواتير المشتريات - تم إنشاؤه تلقائياً من منصة مُصادَق'); // === COLORS === $headerBg = '1C1550'; // Deep violet $headerFont = 'FFFFFF'; // White $subHeaderBg = 'EDE9FE'; // Light violet $subHeaderFont = '5B21B6'; // Violet $totalBg = 'D1FAE5'; // Light green $totalFont = '065F46'; // Dark green $borderColor = 'E2E1F0'; // Light border $altRowBg = 'F8F7FD'; // Alternating row $logoPath = ROOT_PATH . '/public/assets/img/logo.jpg'; if (!file_exists($logoPath)) { error_log("Excel Export Error: Logo not found at {$logoPath}"); } // ══════════════════════════════════════════ // 1. SUMMARY SHEET (First Sheet) // ══════════════════════════════════════════ $summarySheet = $spreadsheet->getActiveSheet(); $summarySheet->setTitle('الملخص الإجمالي'); $summarySheet->setRightToLeft(true); // --- SUMMARY HEADER --- // We use A1 for Logo, B1:I1 for Title, J1 for Link/QR to avoid merge issues in some viewers $summarySheet->setCellValue("B1", 'مُـصَـادَق — ملخص الفواتير الإجمالي'); $summarySheet->mergeCells("B1:I1"); $summarySheet->getStyle("B1:I1")->applyFromArray([ 'font' => ['bold' => true, 'size' => 16, 'color' => ['argb' => 'FF' . $headerFont]], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $headerBg]], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], ]); $summarySheet->getRowDimension(1)->setRowHeight(45); // Style A1 and J1 background to match the header $summarySheet->getStyle("A1")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FF' . $headerBg); $summarySheet->getStyle("J1")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FF' . $headerBg); // --- Add Logo --- try { if (file_exists($logoPath)) { $logoSummary = new Drawing(); $logoSummary->setName('Musadaq Logo'); $logoSummary->setPath($logoPath); $logoSummary->setHeight(38); $logoSummary->setCoordinates('A1'); $logoSummary->setOffsetX(5); $logoSummary->setOffsetY(5); $logoSummary->setWorksheet($summarySheet); } } catch(\Exception $e) { error_log('Logo Summary Error: ' . $e->getMessage()); } // --- Add Clickable Website Link --- $summarySheet->setCellValue('J1', 'musadaq.intaleqapp.com/verify_qr'); $summarySheet->getCell('J1')->getHyperlink()->setUrl('https://musadaq.intaleqapp.com/index.php?route=verify_qr'); $summarySheet->getStyle("J1")->applyFromArray([ 'font' => ['color' => ['argb' => 'FFFFFFFF'], 'underline' => true, 'size' => 9], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT, 'vertical' => Alignment::VERTICAL_CENTER], ]); // --- Add QR Code to Summary Header --- try { $summaryUrl = "https://musadaq.intaleqapp.com/index.php?route=verify_qr"; $qrApiUrl = "https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=" . urlencode($summaryUrl); $qrData = $downloadUrl($qrApiUrl); if ($qrData) { $tmpQr = tempnam(sys_get_temp_dir(), 'qr_sum_'); file_put_contents($tmpQr, $qrData); $drawingQr = new Drawing(); $drawingQr->setName('Musadaq QR'); $drawingQr->setPath($tmpQr); $drawingQr->setHeight(38); $drawingQr->setCoordinates('J1'); $drawingQr->setOffsetX(5); $drawingQr->setOffsetY(5); $drawingQr->setWorksheet($summarySheet); } } catch(\Exception $e) {} // Summary Meta Info $companyNameFilter = 'جميع الشركات'; if ($companyId) { $cStmt = $db->prepare("SELECT name FROM companies WHERE id = ?"); $cStmt->execute([$companyId]); $cName = $cStmt->fetchColumn(); if ($cName) $companyNameFilter = $dec($cName); } $summarySheet->setCellValue("A3", 'الشركة:'); $summarySheet->setCellValue("B3", $companyNameFilter); $summarySheet->setCellValue("D3", 'الفترة:'); $summarySheet->setCellValue("E3", ($dateFrom ?? '—') . ' إلى ' . ($dateTo ?? '—')); $summarySheet->setCellValue("G3", 'عدد الفواتير:'); $summarySheet->setCellValue("H3", count($invoices)); $summarySheet->getStyle("A3:H3")->getFont()->setBold(true); // --- SUMMARY TABLE HEADERS --- $row = 5; $summaryHeaders = ['#', 'رقم الفاتورة', 'المورّد', 'وصف البند', 'الكمية', 'سعر الوحدة', 'المجموع الجزئي', 'نسبة الضريبة', 'قيمة الضريبة', 'الصافي']; $sumCols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']; foreach ($summaryHeaders as $i => $h) { $summarySheet->setCellValue($sumCols[$i] . $row, $h); } $summarySheet->getStyle("A{$row}:J{$row}")->applyFromArray([ 'font' => ['bold' => true, 'color' => ['argb' => 'FF' . $headerFont]], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $headerBg]], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER], ]); // Summary column widths $summarySheet->getColumnDimension('B')->setWidth(18); $summarySheet->getColumnDimension('C')->setWidth(25); $summarySheet->getColumnDimension('D')->setWidth(35); $summarySheet->getColumnDimension('G')->setWidth(14); $summarySheet->getColumnDimension('I')->setWidth(14); $summarySheet->getColumnDimension('J')->setWidth(16); $row++; $summaryStartRow = $row; $globalLineCount = 0; // ══════════════════════════════════════════ // 2. INDIVIDUAL INVOICE SHEETS + POPULATE SUMMARY // ══════════════════════════════════════════ foreach ($invoices as $invIdx => $inv) { // Fetch line items for this invoice $stmtLines = $db->prepare("SELECT * FROM invoice_lines WHERE invoice_id = ? ORDER BY line_number ASC"); $stmtLines->execute([$inv['id']]); $lines = $stmtLines->fetchAll(); // --- Add to Summary Sheet --- if (!empty($lines)) { foreach ($lines as $line) { $globalLineCount++; $summarySheet->setCellValue("A{$row}", $globalLineCount); $summarySheet->setCellValue("B{$row}", $inv['invoice_number'] ?? '-'); $summarySheet->setCellValue("C{$row}", $dec($inv['supplier_name'])); $summarySheet->setCellValue("D{$row}", $line['description'] ?? 'بدون وصف'); $summarySheet->setCellValue("E{$row}", (float)$line['quantity']); $summarySheet->setCellValue("F{$row}", (float)$line['unit_price']); $summarySheet->setCellValue("G{$row}", "=E{$row}*F{$row}"); $summarySheet->setCellValue("H{$row}", (float)$line['tax_rate']); $summarySheet->setCellValue("I{$row}", "=G{$row}*H{$row}"); $summarySheet->setCellValue("J{$row}", "=G{$row}+I{$row}"); if ($globalLineCount % 2 === 0) { $summarySheet->getStyle("A{$row}:J{$row}")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FFF8F7FD'); } $row++; } } else { // Fallback if no line items $globalLineCount++; $summarySheet->setCellValue("A{$row}", $globalLineCount); $summarySheet->setCellValue("B{$row}", $inv['invoice_number'] ?? '-'); $summarySheet->setCellValue("C{$row}", $dec($inv['supplier_name'])); $summarySheet->setCellValue("D{$row}", 'إجمالي الفاتورة'); $summarySheet->setCellValue("E{$row}", 1); $summarySheet->setCellValue("F{$row}", (float)$inv['subtotal']); $summarySheet->setCellValue("G{$row}", "=E{$row}*F{$row}"); $summarySheet->setCellValue("H{$row}", 0.16); $summarySheet->setCellValue("I{$row}", "=G{$row}*H{$row}"); $summarySheet->setCellValue("J{$row}", "=G{$row}+I{$row}"); $row++; } // --- Create Individual Sheet --- $sheet = $spreadsheet->createSheet(); $invoiceNum = $inv['invoice_number'] ?? ('INV-' . ($invIdx + 1)); $sheetTitle = mb_substr(preg_replace('/[^a-zA-Z0-9\x{0600}-\x{06FF}\s\-]/u', '', $invoiceNum), 0, 31) ?: ('فاتورة ' . ($invIdx + 1)); $sheet->setTitle($sheetTitle); $sheet->setRightToLeft(true); // ── Column widths ── $sheet->getColumnDimension('A')->setWidth(6); // # $sheet->getColumnDimension('B')->setWidth(38); // Description $sheet->getColumnDimension('C')->setWidth(12); // Quantity $sheet->getColumnDimension('D')->setWidth(14); // Unit Price $sheet->getColumnDimension('E')->setWidth(16); // Subtotal (formula) $sheet->getColumnDimension('F')->setWidth(14); // Tax Rate $sheet->getColumnDimension('G')->setWidth(16); // Tax Amount (formula) $sheet->getColumnDimension('H')->setWidth(14); // Discount $sheet->getColumnDimension('I')->setWidth(18); // Net Total (formula) $invRow = 1; // ── INVOICE HEADER ────────────────────────── // We use A for Logo, B:H for Title, I for QR to avoid merge issues $sheet->setCellValue("B{$invRow}", 'مُـصَـادَق — تقرير فاتورة مشتريات'); $sheet->mergeCells("B{$invRow}:H{$invRow}"); $sheet->getStyle("B{$invRow}:H{$invRow}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 16, 'color' => ['argb' => 'FF' . $headerFont]], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $headerBg]], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getRowDimension($invRow)->setRowHeight(45); // Background color for side cells $sheet->getStyle("A{$invRow}")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FF' . $headerBg); $sheet->getStyle("I{$invRow}")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FF' . $headerBg); // --- Add Logo --- try { if (file_exists($logoPath)) { $logoInv = new Drawing(); $logoInv->setName('Musadaq Logo'); $logoInv->setPath($logoPath); $logoInv->setHeight(38); $logoInv->setCoordinates('A' . $invRow); $logoInv->setOffsetX(5); $logoInv->setOffsetY(5); $logoInv->setWorksheet($sheet); } } catch(\Exception $e) { error_log('Logo Invoice Error: ' . $e->getMessage()); } // --- Add Clickable Website Link --- // We'll move the link slightly down or put it in I1 with the QR $sheet->setCellValue("I" . $invRow, 'musadaq.intaleqapp.com/verify_qr'); $verifyUrl = "https://musadaq.intaleqapp.com/index.php?route=verify_qr&id=" . $inv['id']; $sheet->getCell("I" . $invRow)->getHyperlink()->setUrl($verifyUrl); $sheet->getStyle("I" . $invRow)->applyFromArray([ 'font' => ['color' => ['argb' => 'FFFFFFFF'], 'underline' => true, 'size' => 8], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_LEFT, 'vertical' => Alignment::VERTICAL_TOP], ]); // --- Add Verification QR Code --- try { $verifyUrl = "https://musadaq.intaleqapp.com/index.php?route=verify_qr&id=" . $inv['id']; $qrApiUrl = "https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=" . urlencode($verifyUrl); $qrData = $downloadUrl($qrApiUrl); if ($qrData) { $tmpQr = tempnam(sys_get_temp_dir(), 'qr_inv_'); file_put_contents($tmpQr, $qrData); $drawingQr = new Drawing(); $drawingQr->setName('Verification QR'); $drawingQr->setPath($tmpQr); $drawingQr->setHeight(38); $drawingQr->setCoordinates('I' . $invRow); $drawingQr->setOffsetX(5); $drawingQr->setOffsetY(5); $drawingQr->setWorksheet($sheet); } } catch(\Exception $e) {} $invRow++; // Invoice meta data $metaData = [ ['رقم الفاتورة', $inv['invoice_number'] ?? '-', 'اسم المورّد', $dec($inv['supplier_name'])], ['تاريخ الفاتورة', $inv['invoice_date'] ?? '-', 'الرقم الضريبي للمورّد', $dec($inv['supplier_tin'])], ['الشركة', $dec($inv['company_name_raw'] ?? ''), 'العملة', $inv['currency_code'] ?? 'JOD'], ['نوع الفاتورة', ($inv['invoice_type'] === 'cash' ? 'نقدي' : 'آجل'), 'الحالة', match($inv['status']) { 'extracted' => 'مستخرجة', 'approved' => 'معتمدة', 'submitted' => 'مقدمة لجوفتورة', 'rejected' => 'مرفوضة', default => $inv['status'] }], ]; foreach ($metaData as $meta) { $sheet->setCellValue("A{$invRow}", $meta[0]); $sheet->mergeCells("B{$invRow}:C{$invRow}"); $sheet->setCellValue("B{$invRow}", $meta[1]); $sheet->setCellValue("E{$invRow}", $meta[2]); $sheet->mergeCells("F{$invRow}:I{$invRow}"); $sheet->setCellValue("F{$invRow}", $meta[3]); $sheet->getStyle("A{$invRow}:C{$invRow}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 11, 'color' => ['argb' => 'FF' . $subHeaderFont]], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $subHeaderBg]], ]); $sheet->getStyle("E{$invRow}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 11, 'color' => ['argb' => 'FF' . $subHeaderFont]], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $subHeaderBg]], ]); $sheet->getRowDimension($invRow)->setRowHeight(24); $invRow++; } $invRow++; // Items Header $headers = ['#', 'وصف البند', 'الكمية', 'سعر الوحدة', 'المجموع الجزئي', 'نسبة الضريبة', 'قيمة الضريبة', 'الخصم', 'الصافي']; $cols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']; foreach ($headers as $i => $h) $sheet->setCellValue($cols[$i] . $invRow, $h); $sheet->getStyle("A{$invRow}:I{$invRow}")->applyFromArray([ 'font' => ['bold' => true, 'color' => ['argb' => 'FF' . $headerFont]], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $headerBg]], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER], ]); $sheet->getRowDimension($invRow)->setRowHeight(32); $invRow++; $itemsStart = $invRow; if (!empty($lines)) { foreach ($lines as $lIdx => $line) { $sheet->setCellValue("A{$invRow}", $lIdx + 1); $sheet->setCellValue("B{$invRow}", $line['description'] ?? 'بدون وصف'); $sheet->setCellValue("C{$invRow}", (float)$line['quantity']); $sheet->setCellValue("D{$invRow}", (float)$line['unit_price']); $sheet->setCellValue("E{$invRow}", "=C{$invRow}*D{$invRow}"); $sheet->setCellValue("F{$invRow}", (float)$line['tax_rate']); $sheet->getStyle("F{$invRow}")->getNumberFormat()->setFormatCode('0%'); $sheet->setCellValue("G{$invRow}", "=E{$invRow}*F{$invRow}"); $sheet->setCellValue("H{$invRow}", (float)$line['discount_amount']); $sheet->setCellValue("I{$invRow}", "=E{$invRow}+G{$invRow}-H{$invRow}"); if ($lIdx % 2 === 1) $sheet->getStyle("A{$invRow}:I{$invRow}")->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setARGB('FFF8F7FD'); foreach (['D','E','G','H','I'] as $c) $sheet->getStyle("{$c}{$invRow}")->getNumberFormat()->setFormatCode('#,##0.000'); $invRow++; } } else { $sheet->setCellValue("A{$invRow}", 1); $sheet->setCellValue("B{$invRow}", 'إجمالي الفاتورة'); $sheet->setCellValue("C{$invRow}", 1); $sheet->setCellValue("D{$invRow}", (float)$inv['subtotal']); $sheet->setCellValue("E{$invRow}", "=C{$invRow}*D{$invRow}"); $sheet->setCellValue("F{$invRow}", 0.16); $sheet->getStyle("F{$invRow}")->getNumberFormat()->setFormatCode('0%'); $sheet->setCellValue("G{$invRow}", "=E{$invRow}*F{$invRow}"); $sheet->setCellValue("H{$invRow}", (float)$inv['discount_total']); $sheet->setCellValue("I{$invRow}", "=E{$invRow}+G{$invRow}-H{$invRow}"); foreach (['D','E','G','H','I'] as $c) $sheet->getStyle("{$c}{$invRow}")->getNumberFormat()->setFormatCode('#,##0.000'); $invRow++; } // Totals row for individual sheet $lastItemRow = $invRow - 1; $sheet->mergeCells("A{$invRow}:B{$invRow}"); $sheet->setCellValue("A{$invRow}", 'المجموع الكلي'); $sheet->setCellValue("C{$invRow}", "=SUM(C{$itemsStart}:C{$lastItemRow})"); $sheet->setCellValue("E{$invRow}", "=SUM(E{$itemsStart}:E{$lastItemRow})"); $sheet->setCellValue("G{$invRow}", "=SUM(G{$itemsStart}:G{$lastItemRow})"); $sheet->setCellValue("H{$invRow}", "=SUM(H{$itemsStart}:H{$lastItemRow})"); $sheet->setCellValue("I{$invRow}", "=SUM(I{$itemsStart}:I{$lastItemRow})"); $sheet->getStyle("G{$invRow}:I{$invRow}")->applyFromArray([ 'font' => ['bold' => true, 'color' => ['argb' => 'FF' . $totalFont]], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $totalBg]], ]); foreach (['C','E','G','H','I'] as $c) $sheet->getStyle("{$c}{$invRow}")->getNumberFormat()->setFormatCode('#,##0.000'); $invRow += 2; $sheet->setCellValue("A{$invRow}", 'تم إنشاء هذا التقرير تلقائياً من منصة مُصادَق — ' . date('Y-m-d H:i')); } // Final Summary Row with totals $lastSummaryRow = $row - 1; $summarySheet->mergeCells("A{$row}:D{$row}"); $summarySheet->setCellValue("A{$row}", 'المجموع الكلي النهائي'); $summarySheet->setCellValue("G{$row}", "=SUM(G{$summaryStartRow}:G{$lastSummaryRow})"); $summarySheet->setCellValue("I{$row}", "=SUM(I{$summaryStartRow}:I{$lastSummaryRow})"); $summarySheet->setCellValue("J{$row}", "=SUM(J{$summaryStartRow}:J{$lastSummaryRow})"); $summarySheet->getStyle("A{$row}:J{$row}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 13, 'color' => ['argb' => 'FF' . $totalFont]], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['argb' => 'FF' . $totalBg]], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER], ]); foreach (['F', 'G', 'I', 'J'] as $c) { $summarySheet->getStyle("{$c}{$summaryStartRow}:{$c}{$row}")->getNumberFormat()->setFormatCode('#,##0.000'); } $summarySheet->getStyle("H{$summaryStartRow}:H{$row}")->getNumberFormat()->setFormatCode('0%'); // Set first sheet as active $spreadsheet->setActiveSheetIndex(0); // ══════════════════════════════════════════ // SEND FILE // ══════════════════════════════════════════ $filename = 'musadaq_invoices_' . date('Y-m-d_His') . '.xlsx'; header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Cache-Control: max-age=0'); header('Pragma: public'); $writer = new Xlsx($spreadsheet); $writer->save('php://output'); $spreadsheet->disconnectWorksheets(); unset($spreadsheet); exit; } catch (\Exception $e) { if (ob_get_length()) ob_end_clean(); header('Content-Type: text/plain; charset=utf-8'); file_put_contents(STORAGE_PATH . '/logs/export_errors.log', "[" . date('Y-m-d H:i:s') . "] " . $e->getMessage() . "\n" . $e->getTraceAsString(), FILE_APPEND); die("خطأ في التصدير: " . $e->getMessage()); }