= ?'; $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; }; // ══════════════════════════════════════════ // 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 // Process each invoice $sheetIndex = 0; 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(); // Create sheet (reuse first sheet for first invoice) if ($sheetIndex === 0) { $sheet = $spreadsheet->getActiveSheet(); } else { $sheet = $spreadsheet->createSheet(); } $invoiceNum = $inv['invoice_number'] ?? ('INV-' . ($sheetIndex + 1)); $sheetTitle = mb_substr(preg_replace('/[^a-zA-Z0-9\x{0600}-\x{06FF}\s\-]/u', '', $invoiceNum), 0, 31) ?: ('فاتورة ' . ($sheetIndex + 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) $row = 1; // ── INVOICE HEADER ────────────────────────── $sheet->mergeCells("A{$row}:I{$row}"); $sheet->setCellValue("A{$row}", 'مُـصَـادَق — تقرير فاتورة مشتريات'); $sheet->getStyle("A{$row}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 16, 'color' => new Color($headerFont)], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => new Color($headerBg)], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getRowDimension($row)->setRowHeight(40); $row++; // Invoice meta data (2 columns layout) $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{$row}", $meta[0]); $sheet->mergeCells("B{$row}:C{$row}"); $sheet->setCellValue("B{$row}", $meta[1]); $sheet->setCellValue("E{$row}", $meta[2]); $sheet->mergeCells("F{$row}:I{$row}"); $sheet->setCellValue("F{$row}", $meta[3]); // Style labels $sheet->getStyle("A{$row}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 11, 'color' => new Color($subHeaderFont)], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => new Color($subHeaderBg)], ]); $sheet->getStyle("E{$row}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 11, 'color' => new Color($subHeaderFont)], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => new Color($subHeaderBg)], ]); $sheet->getRowDimension($row)->setRowHeight(24); $row++; } $row++; // Empty spacer row // ── LINE ITEMS TABLE HEADER ───────────────── $headers = ['#', 'وصف البند', 'الكمية', 'سعر الوحدة', 'المجموع الجزئي', 'نسبة الضريبة', 'قيمة الضريبة', 'قيمة الخصم', 'الصافي']; $cols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']; $headerRow = $row; foreach ($headers as $i => $header) { $sheet->setCellValue($cols[$i] . $row, $header); } $sheet->getStyle("A{$row}:I{$row}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 12, 'color' => new Color($headerFont)], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => new Color($headerBg)], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], 'borders' => [ 'allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => new Color($headerBg)], ], ]); $sheet->getRowDimension($row)->setRowHeight(32); $row++; // ── LINE ITEMS WITH FORMULAS ──────────────── $firstDataRow = $row; if (!empty($lines)) { foreach ($lines as $lineIdx => $line) { $lineNum = $lineIdx + 1; $quantity = is_numeric($line['quantity'] ?? null) ? (float)$line['quantity'] : 1; $unitPrice = is_numeric($line['unit_price'] ?? null) ? (float)$line['unit_price'] : 0; $taxRate = is_numeric($line['tax_rate'] ?? null) ? (float)$line['tax_rate'] : 0.16; $discount = is_numeric($line['discount_amount'] ?? null) ? (float)$line['discount_amount'] : 0; // A: Line number $sheet->setCellValue("A{$row}", $lineNum); // B: Description $sheet->setCellValue("B{$row}", $line['description'] ?? 'بدون وصف'); // C: Quantity (value) $sheet->setCellValue("C{$row}", $quantity); // D: Unit Price (value) $sheet->setCellValue("D{$row}", $unitPrice); // E: Subtotal = Quantity × Unit Price (FORMULA) $sheet->setCellValue("E{$row}", "=C{$row}*D{$row}"); // F: Tax Rate (as percentage) $sheet->setCellValue("F{$row}", $taxRate); $sheet->getStyle("F{$row}")->getNumberFormat()->setFormatCode('0%'); // G: Tax Amount = Subtotal × Tax Rate (FORMULA) $sheet->setCellValue("G{$row}", "=E{$row}*F{$row}"); // H: Discount amount (value) $sheet->setCellValue("H{$row}", $discount); // I: Net Total = Subtotal + Tax - Discount (FORMULA) $sheet->setCellValue("I{$row}", "=E{$row}+G{$row}-H{$row}"); // Alternating row colors if ($lineIdx % 2 === 1) { $sheet->getStyle("A{$row}:I{$row}")->applyFromArray([ 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => new Color($altRowBg)], ]); } // Number formatting for currency columns foreach (['D', 'E', 'G', 'H', 'I'] as $col) { $sheet->getStyle("{$col}{$row}")->getNumberFormat()->setFormatCode('#,##0.000'); } $sheet->getStyle("C{$row}")->getNumberFormat()->setFormatCode('#,##0'); // Center align numbers $sheet->getStyle("A{$row}:I{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getStyle("B{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT); $sheet->getRowDimension($row)->setRowHeight(26); $row++; } } else { // If no line items, create a single row from invoice totals $sheet->setCellValue("A{$row}", 1); $sheet->setCellValue("B{$row}", 'إجمالي الفاتورة (بدون تفاصيل بنود)'); $sheet->setCellValue("C{$row}", 1); $sheet->setCellValue("D{$row}", (float)($inv['subtotal'] ?? 0)); $sheet->setCellValue("E{$row}", "=C{$row}*D{$row}"); $sheet->setCellValue("F{$row}", 0.16); $sheet->getStyle("F{$row}")->getNumberFormat()->setFormatCode('0%'); $sheet->setCellValue("G{$row}", "=E{$row}*F{$row}"); $sheet->setCellValue("H{$row}", (float)($inv['discount_total'] ?? 0)); $sheet->setCellValue("I{$row}", "=E{$row}+G{$row}-H{$row}"); foreach (['D', 'E', 'G', 'H', 'I'] as $col) { $sheet->getStyle("{$col}{$row}")->getNumberFormat()->setFormatCode('#,##0.000'); } $sheet->getStyle("A{$row}:I{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $sheet->getRowDimension($row)->setRowHeight(26); $row++; } $lastDataRow = $row - 1; // ── DATA AREA BORDERS ── $sheet->getStyle("A{$headerRow}:I{$lastDataRow}")->applyFromArray([ 'borders' => [ 'allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => new Color($borderColor)], ], ]); // ── TOTALS ROW WITH SUM FORMULAS ──────────── $sheet->mergeCells("A{$row}:B{$row}"); $sheet->setCellValue("A{$row}", 'المجموع الكلي'); // C: Sum of quantities $sheet->setCellValue("C{$row}", "=SUM(C{$firstDataRow}:C{$lastDataRow})"); // D: (empty — unit price sum doesn't make sense) $sheet->setCellValue("D{$row}", ''); // E: Sum of subtotals $sheet->setCellValue("E{$row}", "=SUM(E{$firstDataRow}:E{$lastDataRow})"); // F: (empty — average tax rate doesn't belong here) $sheet->setCellValue("F{$row}", ''); // G: Sum of tax amounts $sheet->setCellValue("G{$row}", "=SUM(G{$firstDataRow}:G{$lastDataRow})"); // H: Sum of discounts $sheet->setCellValue("H{$row}", "=SUM(H{$firstDataRow}:H{$lastDataRow})"); // I: Sum of net totals $sheet->setCellValue("I{$row}", "=SUM(I{$firstDataRow}:I{$lastDataRow})"); // Style totals row $sheet->getStyle("A{$row}:I{$row}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 13, 'color' => new Color($totalFont)], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => new Color($totalBg)], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], 'borders' => [ 'allBorders' => ['borderStyle' => Border::BORDER_MEDIUM, 'color' => new Color('059669')], ], ]); $sheet->getStyle("A{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); foreach (['C', 'E', 'G', 'H', 'I'] as $col) { $sheet->getStyle("{$col}{$row}")->getNumberFormat()->setFormatCode('#,##0.000'); } $sheet->getRowDimension($row)->setRowHeight(36); $row += 2; // ── FOOTER ── $sheet->mergeCells("A{$row}:I{$row}"); $sheet->setCellValue("A{$row}", 'تم إنشاء هذا التقرير تلقائياً من منصة مُصادَق — ' . date('Y-m-d H:i')); $sheet->getStyle("A{$row}")->applyFromArray([ 'font' => ['italic' => true, 'size' => 9, 'color' => new Color('8B82B0')], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER], ]); // ── Print settings ── $sheet->getPageSetup()->setOrientation(\PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE); $sheet->getPageSetup()->setFitToWidth(1); $sheetIndex++; } // 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()); }