diff --git a/create_tenant_views.php b/create_tenant_views.php deleted file mode 100644 index ba249af..0000000 --- a/create_tenant_views.php +++ /dev/null @@ -1,54 +0,0 @@ -exec("DROP FUNCTION IF EXISTS current_outlet_id"); - $db->exec("CREATE FUNCTION current_outlet_id() RETURNS INT DETERMINISTIC RETURN @session_outlet_id"); - - foreach ($tables as $t) { - // Ensure table exists and isn't already a view - $stmt = $db->query("SHOW FULL TABLES LIKE '$t'"); - $row = $stmt->fetch(PDO::FETCH_NUM); - if (!$row) continue; - if ($row[1] === 'VIEW') continue; // Already processed - - // Make sure it has outlet_id - $hasCol = false; - $cols = $db->query("SHOW COLUMNS FROM `$t`")->fetchAll(PDO::FETCH_ASSOC); - foreach ($cols as $c) { - if ($c['Field'] === 'outlet_id') $hasCol = true; - } - if (!$hasCol) { - $db->exec("ALTER TABLE `$t` ADD COLUMN IF NOT EXISTS `outlet_id` int(11) DEFAULT 1 AFTER `id`"); - } - - // Alter default to 1 if it's NULL - $db->exec("ALTER TABLE `$t` MODIFY `outlet_id` int(11) DEFAULT 1"); - - // Rename table to _table - $db->exec("RENAME TABLE `$t` TO `_$t`"); - - // Create view - $db->exec("CREATE VIEW `$t` AS SELECT * FROM `_$t` WHERE outlet_id = current_outlet_id() OR current_outlet_id() IS NULL OR current_outlet_id() = 0"); - - // Create trigger - $db->exec("CREATE TRIGGER `trg_ins_$t` BEFORE INSERT ON `_$t` FOR EACH ROW BEGIN - IF current_outlet_id() IS NOT NULL AND current_outlet_id() != 0 THEN - SET NEW.outlet_id = current_outlet_id(); - END IF; - END"); - - echo "Processed $t\n"; - } -} catch (Exception $e) { - echo "Error: " . $e->getMessage() . "\n"; -} diff --git a/data_fetch.txt b/data_fetch.txt deleted file mode 100644 index 1e04ab7..0000000 --- a/data_fetch.txt +++ /dev/null @@ -1,1001 +0,0 @@ - } else { - fputcsv($output, $headers); - foreach ($rows as $row) fputcsv($output, $row); - fclose($output); - } - exit; -} - -// Global data for modals -$data['categories'] = db()->query("SELECT * FROM stock_categories ORDER BY name_en ASC")->fetchAll(); -$data['units'] = db()->query("SELECT * FROM stock_units ORDER BY name_en ASC")->fetchAll(); -$data['suppliers'] = db()->query("SELECT * FROM suppliers ORDER BY name ASC")->fetchAll(); -$data['accounts'] = db()->query("SELECT * FROM acc_accounts ORDER BY code ASC")->fetchAll(); -$data['customers_list'] = db()->query("SELECT * FROM customers ORDER BY name ASC")->fetchAll(); -$customers = $data['customers_list']; // For backward compatibility in some modals - -$settings_raw = db()->query("SELECT * FROM settings")->fetchAll(); -$data['settings'] = []; -foreach ($settings_raw as $s) { - $data['settings'][$s['key']] = $s['value']; -} - -$limit = isset($_GET["limit"]) ? max(5, (int)$_GET["limit"]) : 20; -$page_num = isset($_GET["p"]) ? (int)$_GET["p"] : 1; -if ($page_num < 1) $page_num = 1; -$offset = ($page_num - 1) * $limit; -switch ($page) { - case 'suppliers': - $where = ["1=1"]; - $params = []; - if (!empty($_GET['search'])) { - $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ? OR tax_id LIKE ?)"; - $params[] = "%{$_GET['search']}%"; - $params[] = "%{$_GET['search']}%"; - $params[] = "%{$_GET['search']}%"; - $params[] = "%{$_GET['search']}%"; - } - if (!empty($_GET['start_date'])) { - $where[] = "DATE(created_at) >= ?"; - $params[] = $_GET['start_date']; - } - if (!empty($_GET['end_date'])) { - $where[] = "DATE(created_at) <= ?"; - $params[] = $_GET['end_date']; - } - $whereSql = implode(" AND ", $where); - - $countStmt = db()->prepare("SELECT COUNT(*) FROM suppliers WHERE $whereSql"); - $countStmt->execute($params); - $total_records = (int)$countStmt->fetchColumn(); - $data['total_pages'] = ceil($total_records / $limit); - $data['current_page'] = $page_num; - - $stmt = db()->prepare("SELECT * FROM suppliers WHERE $whereSql ORDER BY id DESC LIMIT $limit OFFSET $offset"); - $stmt->execute($params); - $data['customers'] = $stmt->fetchAll(); // Keep 'customers' key for template compatibility if needed, or update template - break; - case 'customers': - $where = ["1=1"]; - $params = []; - if (!empty($_GET['search'])) { - $where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ? OR tax_id LIKE ?)"; - $params[] = "%{$_GET['search']}%"; - $params[] = "%{$_GET['search']}%"; - $params[] = "%{$_GET['search']}%"; - $params[] = "%{$_GET['search']}%"; - } - if (!empty($_GET['start_date'])) { - $where[] = "DATE(created_at) >= ?"; - $params[] = $_GET['start_date']; - } - if (!empty($_GET['end_date'])) { - $where[] = "DATE(created_at) <= ?"; - $params[] = $_GET['end_date']; - } - $whereSql = implode(" AND ", $where); - - $countStmt = db()->prepare("SELECT COUNT(*) FROM customers WHERE $whereSql"); - $countStmt->execute($params); - $total_records = (int)$countStmt->fetchColumn(); - $data['total_pages'] = ceil($total_records / $limit); - $data['current_page'] = $page_num; - - $stmt = db()->prepare("SELECT * FROM customers WHERE $whereSql ORDER BY id DESC LIMIT $limit OFFSET $offset"); - $stmt->execute($params); - $data['customers'] = $stmt->fetchAll(); - break; - case 'categories': - // Already fetched globally - break; - case 'units': - // Already fetched globally - break; - case 'items': - file_put_contents('debug.log', date('Y-m-d H:i:s') . " - Items case hit\n", FILE_APPEND); - $where = ["1=1"]; - $params = []; - if (!empty($_GET['search'])) { - $where[] = "(i.name_en LIKE ? OR i.name_ar LIKE ? OR i.sku LIKE ?)"; - $params[] = "%{$_GET['search']}%"; - $params[] = "%{$_GET['search']}%"; - $params[] = "%{$_GET['search']}%"; - } - $whereSql = implode(" AND ", $where); - - $countStmt = db()->prepare("SELECT COUNT(*) FROM stock_items i - LEFT JOIN stock_categories c ON i.category_id = c.id - LEFT JOIN stock_units u ON i.unit_id = u.id - LEFT JOIN suppliers s ON i.supplier_id = s.id WHERE $whereSql"); - $countStmt->execute($params); - $total_records = (int)$countStmt->fetchColumn(); - $data['total_pages'] = ceil($total_records / $limit); - $data['current_page'] = $page_num; - - $stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar, u.short_name_en as unit_en, u.short_name_ar as unit_ar, s.name as supplier_name - FROM stock_items i - LEFT JOIN stock_categories c ON i.category_id = c.id - LEFT JOIN stock_units u ON i.unit_id = u.id - LEFT JOIN suppliers s ON i.supplier_id = s.id - WHERE $whereSql - ORDER BY i.id DESC LIMIT $limit OFFSET $offset"); - $stmt->execute($params); - $data['items'] = $stmt->fetchAll(); - break; - case 'quotations': - $where = ["1=1"]; - $params = []; - if (!empty($_GET['search'])) { - $s = $_GET['search']; - $clean_id = preg_replace('/[^0-9]/', '', $s); - if ($clean_id !== '') { - $where[] = "(q.id LIKE ? OR c.name LIKE ? OR q.id = ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - $params[] = $clean_id; - } else { - $where[] = "(q.id LIKE ? OR c.name LIKE ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - } - } - if (!empty($_GET['customer_id'])) { - $where[] = "q.customer_id = ?"; - $params[] = $_GET['customer_id']; - } - if (!empty($_GET['start_date'])) { - $where[] = "q.quotation_date >= ?"; - $params[] = $_GET['start_date']; - } - if (!empty($_GET['end_date'])) { - $where[] = "q.quotation_date <= ?"; - $params[] = $_GET['end_date']; - } - $whereSql = implode(" AND ", $where); - - - - $countStmt = db()->prepare("SELECT COUNT(*) FROM quotations q JOIN customers c ON q.customer_id = c.id WHERE $whereSql"); - $countStmt->execute($params); - $total_records = (int)$countStmt->fetchColumn(); - $data['total_pages'] = ceil($total_records / $limit); - $data['current_page'] = $page_num; - - $stmt = db()->prepare("SELECT q.*, c.name as customer_name - FROM quotations q - JOIN customers c ON q.customer_id = c.id - WHERE $whereSql - ORDER BY q.id DESC - LIMIT $limit OFFSET $offset"); - $stmt->execute($params); - $data['quotations'] = $stmt->fetchAll(); - break; - case 'lpos': - $where = ["1=1"]; - $params = []; - if (!empty($_GET['search'])) { - $s = $_GET['search']; - $clean_id = preg_replace('/[^0-9]/', '', $s); - if ($clean_id !== '') { - $where[] = "(q.id LIKE ? OR s.name LIKE ? OR q.id = ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - $params[] = $clean_id; - } else { - $where[] = "(q.id LIKE ? OR s.name LIKE ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - } - } - if (!empty($_GET['supplier_id'])) { - $where[] = "q.supplier_id = ?"; - $params[] = $_GET['supplier_id']; - } - if (!empty($_GET['start_date'])) { - $where[] = "q.lpo_date >= ?"; - $params[] = $_GET['start_date']; - } - if (!empty($_GET['end_date'])) { - $where[] = "q.lpo_date <= ?"; - $params[] = $_GET['end_date']; - } - $whereSql = implode(" AND ", $where); - - - - $countStmt = db()->prepare("SELECT COUNT(*) FROM lpos q JOIN suppliers s ON q.supplier_id = s.id WHERE $whereSql"); - $countStmt->execute($params); - $total_records = (int)$countStmt->fetchColumn(); - $data['total_pages'] = ceil($total_records / $limit); - $data['current_page'] = $page_num; - - $stmt = db()->prepare("SELECT q.*, s.name as supplier_name - FROM lpos q - JOIN suppliers s ON q.supplier_id = s.id - WHERE $whereSql - ORDER BY q.id DESC - LIMIT $limit OFFSET $offset"); - $stmt->execute($params); - $data['lpos'] = $stmt->fetchAll(); - break; - case 'payment_methods': - $data['payment_methods'] = db()->query("SELECT * FROM payment_methods ORDER BY id DESC")->fetchAll(); - break; - case 'settings': - // Already fetched globally - break; - case 'my_profile': - $stmt = db()->prepare("SELECT * FROM users WHERE id = ?"); - $stmt->execute([$_SESSION['user_id']]); - $data['user'] = $stmt->fetch(); - break; - case 'sales': - case 'purchases': - $type = ($page === 'sales') ? 'sale' : 'purchase'; - $table = ($type === 'purchase') ? 'purchases' : 'invoices'; - $cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id'; - $cust_supplier_table = ($type === 'purchase') ? 'suppliers' : 'customers'; - - $where = ["1=1"]; - $params = []; - - if (!empty($_GET['search'])) { - $s = $_GET['search']; - $clean_id = preg_replace('/[^0-9]/', '', $s); - if ($clean_id !== '') { - $where[] = "(v.id LIKE ? OR c.name LIKE ? OR v.id = ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - $params[] = $clean_id; - } else { - $where[] = "(v.id LIKE ? OR c.name LIKE ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - } - } - - if (!empty($_GET['customer_id'])) { - $where[] = "v.$cust_supplier_col = ?"; - $params[] = $_GET['customer_id']; - } - - if (!empty($_GET['start_date'])) { - $where[] = "v.invoice_date >= ?"; - $params[] = $_GET['start_date']; - } - - if (!empty($_GET['end_date'])) { - $where[] = "v.invoice_date <= ?"; - $params[] = $_GET['end_date']; - } - - $whereSql = implode(" AND ", $where); - - $countStmt = db()->prepare("SELECT COUNT(*) FROM $table v LEFT JOIN $cust_supplier_table c ON v.$cust_supplier_col = c.id WHERE $whereSql"); - $countStmt->execute($params); - $total_records = (int)$countStmt->fetchColumn(); - $data['total_pages'] = ceil($total_records / $limit); - $data['current_page'] = $page_num; - - $stmt = db()->prepare("SELECT v.*, c.name as customer_name, c.tax_id as customer_tax_id, c.phone as customer_phone - FROM $table v - LEFT JOIN $cust_supplier_table c ON v.$cust_supplier_col = c.id - WHERE $whereSql - ORDER BY v.id DESC LIMIT $limit OFFSET $offset"); - $stmt->execute($params); - $data['invoices'] = $stmt->fetchAll(); - foreach ($data['invoices'] as &$inv) { - $inv['total_in_words'] = numberToWordsOMR($inv['total_with_vat']); - if ($type === 'sale') { - $item_stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.vat_rate FROM invoice_items ii LEFT JOIN stock_items i ON ii.item_id = i.id WHERE ii.invoice_id = ?"); - $item_stmt->execute([$inv['id']]); - $inv['items'] = $item_stmt->fetchAll(PDO::FETCH_ASSOC); - } else { - $item_stmt = db()->prepare("SELECT pi.*, i.name_en, i.name_ar, i.vat_rate FROM purchase_items pi LEFT JOIN stock_items i ON pi.item_id = i.id WHERE pi.purchase_id = ?"); - $item_stmt->execute([$inv['id']]); - $inv['items'] = $item_stmt->fetchAll(PDO::FETCH_ASSOC); - } - } - unset($inv); - - $items_list_raw = db()->query("SELECT id, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate, is_promotion, promotion_start, promotion_end, promotion_percent FROM stock_items ORDER BY name_en ASC")->fetchAll(PDO::FETCH_ASSOC); - foreach ($items_list_raw as &$item) { - $item['sale_price'] = getPromotionalPrice($item); - } - $data['items_list'] = $items_list_raw; - $data['customers_list'] = db()->query("SELECT id, name FROM $cust_supplier_table ORDER BY name ASC")->fetchAll(); - - if ($type === 'sale') { - $data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices ORDER BY id DESC")->fetchAll(); - } else { - $data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM purchases ORDER BY id DESC")->fetchAll(); - } - break; - - case 'sales_returns': - $where = ["1=1"]; - $params = []; - if (!empty($_GET['search'])) { - $s = $_GET['search']; - $clean_id = preg_replace('/[^0-9]/', '', $s); - if ($clean_id !== '') { - $where[] = "(sr.id LIKE ? OR c.name LIKE ? OR sr.invoice_id LIKE ? OR sr.id = ? OR sr.invoice_id = ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - $params[] = "%$s%"; - $params[] = $clean_id; - $params[] = $clean_id; - } else { - $where[] = "(sr.id LIKE ? OR c.name LIKE ? OR sr.invoice_id LIKE ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - $params[] = "%$s%"; - } - } - $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT sr.*, c.name as customer_name, i.total_with_vat as invoice_total - FROM sales_returns sr - LEFT JOIN customers c ON sr.customer_id = c.id - LEFT JOIN invoices i ON sr.invoice_id = i.id - WHERE $whereSql - ORDER BY sr.id DESC"); - $stmt->execute($params); - $data['returns'] = $stmt->fetchAll(); - $data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices ORDER BY id DESC")->fetchAll(); - break; - - case 'purchase_returns': - $where = ["1=1"]; - $params = []; - if (!empty($_GET['search'])) { - $s = $_GET['search']; - $clean_id = preg_replace('/[^0-9]/', '', $s); - if ($clean_id !== '') { - $where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.purchase_id LIKE ? OR pr.id = ? OR pr.purchase_id = ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - $params[] = "%$s%"; - $params[] = $clean_id; - $params[] = $clean_id; - } else { - $where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.purchase_id LIKE ?)"; - $params[] = "%$s%"; - $params[] = "%$s%"; - $params[] = "%$s%"; - } - } - $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT pr.*, c.name as supplier_name, i.total_with_vat as invoice_total - FROM purchase_returns pr - LEFT JOIN suppliers c ON pr.supplier_id = c.id - LEFT JOIN purchases i ON pr.purchase_id = i.id - WHERE $whereSql - ORDER BY pr.id DESC"); - $stmt->execute($params); - $data['returns'] = $stmt->fetchAll(); - $data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM purchases ORDER BY id DESC")->fetchAll(); - break; - - case 'customer_statement': - case 'supplier_statement': - $isCustomer = ($page === 'customer_statement'); - $entityTable = $isCustomer ? 'customers' : 'suppliers'; - $invoiceTable = $isCustomer ? 'invoices' : 'purchases'; - $paymentTable = $isCustomer ? 'payments' : 'purchase_payments'; - $fkColumn = $isCustomer ? 'customer_id' : 'supplier_id'; - $invFkColumn = $isCustomer ? 'invoice_id' : 'purchase_id'; - - $data['entities'] = db()->query("SELECT id, name, balance FROM $entityTable ORDER BY name ASC")->fetchAll(); - - $entity_id = (int)($_GET['entity_id'] ?? 0); - if ($entity_id) { - $data['selected_entity'] = db()->query("SELECT * FROM $entityTable WHERE id = $entity_id")->fetch(); - $start_date = $_GET['start_date'] ?? date('Y-m-01'); - $end_date = $_GET['end_date'] ?? date('Y-m-d'); - - $stmt = db()->prepare("SELECT 'invoice' as trans_type, id, invoice_date as trans_date, total_with_vat as amount, status, id as ref_no - FROM $invoiceTable - WHERE $fkColumn = ? AND invoice_date BETWEEN ? AND ?"); - $stmt->execute([$entity_id, $start_date, $end_date]); - $invoices = $stmt->fetchAll(PDO::FETCH_ASSOC); - - $stmt = db()->prepare("SELECT 'payment' as trans_type, p.id, p.payment_date as trans_date, p.amount, p.payment_method, p.$invFkColumn as ref_no - FROM $paymentTable p - JOIN $invoiceTable i ON p.$invFkColumn = i.id - WHERE i.$fkColumn = ? AND p.payment_date BETWEEN ? AND ?"); - $stmt->execute([$entity_id, $start_date, $end_date]); - $payments = $stmt->fetchAll(PDO::FETCH_ASSOC); - - $transactions = array_merge($invoices, $payments); - usort($transactions, function($a, $b) { - return strtotime($a['trans_date']) <=> strtotime($b['trans_date']); - }); - - $data['transactions'] = $transactions; - } - break; - case 'expense_categories': - $data['expense_categories'] = db()->query("SELECT * FROM expense_categories ORDER BY name_en ASC")->fetchAll(); - break; - case 'expenses': - $where = ["1=1"]; - $params = []; - if (!empty($_GET['category_id'])) { - $where[] = "e.category_id = ?"; - $params[] = $_GET['category_id']; - } - if (!empty($_GET['start_date'])) { - $where[] = "e.expense_date >= ?"; - $params[] = $_GET['start_date']; - } - if (!empty($_GET['end_date'])) { - $where[] = "e.expense_date <= ?"; - $params[] = $_GET['end_date']; - } - $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT e.*, c.name_en as cat_en, c.name_ar as cat_ar - FROM expenses e - LEFT JOIN expense_categories c ON e.category_id = c.id - WHERE $whereSql - ORDER BY e.expense_date DESC, e.id DESC"); - $stmt->execute($params); - $data['expenses'] = $stmt->fetchAll(); - break; - case 'role_groups': - $data['role_groups'] = db()->query("SELECT * FROM role_groups ORDER BY name ASC")->fetchAll(); - break; - case 'users': - $data['users'] = db()->query("SELECT u.*, g.name as group_name FROM users u LEFT JOIN role_groups g ON u.group_id = g.id ORDER BY u.username ASC")->fetchAll(); - $data['role_groups'] = db()->query("SELECT id, name FROM role_groups ORDER BY name ASC")->fetchAll(); - break; - case 'backups': - $data['backups'] = BackupService::getBackups(); - $stmt = db()->prepare("SELECT * FROM settings WHERE `key` IN ('backup_limit', 'backup_auto_enabled', 'backup_time')"); - $stmt->execute(); - $data['backup_settings'] = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); - break; - case 'accounting': - $data['journal_entries'] = db()->query("SELECT je.*, - (SELECT SUM(debit) FROM acc_ledger WHERE journal_entry_id = je.id) as total_debit - FROM acc_journal_entries je - ORDER BY je.entry_date DESC, je.id DESC LIMIT 100")->fetchAll(); - $data['accounts'] = db()->query("SELECT * FROM acc_accounts ORDER BY code ASC")->fetchAll(); - - if (isset($_GET['action']) && $_GET['action'] === 'get_entry_details') { - header('Content-Type: application/json'); - $id = (int)$_GET['id']; - $stmt = db()->prepare("SELECT l.*, a.name_en, a.code FROM acc_ledger l JOIN acc_accounts a ON l.account_id = a.id WHERE l.journal_entry_id = ?"); - $stmt->execute([$id]); - echo json_encode($stmt->fetchAll()); - exit; - } - - if (isset($_GET['view']) && $_GET['view'] === 'trial_balance') { - $data['trial_balance'] = db()->query("SELECT a.code, a.name_en, SUM(l.debit) as total_debit, SUM(l.credit) as total_credit - FROM acc_accounts a - LEFT JOIN acc_ledger l ON a.id = l.account_id - GROUP BY a.id - HAVING total_debit > 0 OR total_credit > 0 - ORDER BY a.code ASC")->fetchAll(); - } - - if (isset($_GET['view']) && $_GET['view'] === 'profit_loss') { - $data['revenue_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'revenue' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll(); - $data['expense_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'expense' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll(); - } - - if (isset($_GET['view']) && $_GET['view'] === 'balance_sheet') { - $data['asset_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'asset' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll(); - $data['liability_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'liability' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll(); - $data['equity_accounts'] = db()->query("SELECT code, name_en, name_ar FROM acc_accounts WHERE type = 'equity' AND parent_id IS NOT NULL ORDER BY code ASC")->fetchAll(); - } - - if (isset($_GET['view']) && $_GET['view'] === 'vat_report') { - $start = $_GET['start_date'] ?? date('Y-m-01'); - $end = $_GET['end_date'] ?? date('Y-m-d'); - $data['vat_report'] = getVatReport($start, $end); - $data['start_date'] = $start; - $data['end_date'] = $end; - } - - if (isset($_GET['view']) && $_GET['view'] === 'coa') { - $data['coa'] = db()->query("SELECT a.*, p.name_en as parent_name - FROM acc_accounts a - LEFT JOIN acc_accounts p ON a.parent_id = p.id - ORDER BY a.code ASC")->fetchAll(); - } - break; - case 'expense_report': - $start_date = $_GET['start_date'] ?? date('Y-m-01'); - $end_date = $_GET['end_date'] ?? date('Y-m-d'); - $category_id = $_GET['category_id'] ?? ''; - - $where = "WHERE e.expense_date BETWEEN ? AND ?"; - $params = [$start_date, $end_date]; - - if ($category_id !== '') { - $where .= " AND e.category_id = ?"; - $params[] = $category_id; - } - - $stmt = db()->prepare("SELECT c.name_en, c.name_ar, SUM(e.amount) as total - FROM expenses e - JOIN expense_categories c ON e.category_id = c.id - $where - GROUP BY c.id - ORDER BY total DESC"); - $stmt->execute($params); - $data['report_by_category'] = $stmt->fetchAll(); - - $stmt = db()->prepare("SELECT SUM(amount) FROM expenses e $where"); - $stmt->execute($params); - $data['total_expenses'] = $stmt->fetchColumn() ?: 0; - - $data['expense_categories'] = db()->query("SELECT * FROM expense_categories ORDER BY name_en ASC")->fetchAll(); - break; - case 'expiry_report': - $where = ["expiry_date IS NOT NULL"]; - $params = []; - $filter = $_GET['filter'] ?? 'all'; - if ($filter === 'expired') { - $where[] = "expiry_date <= CURDATE()"; - } elseif ($filter === 'near_expiry') { - $where[] = "expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)"; - } - - $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar - FROM stock_items i - LEFT JOIN stock_categories c ON i.category_id = c.id - WHERE $whereSql - ORDER BY i.expiry_date ASC"); - $stmt->execute($params); - $data['expiry_items'] = $stmt->fetchAll(); - break; - case 'low_stock_report': - $stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar, s.name as supplier_name - FROM stock_items i - LEFT JOIN stock_categories c ON i.category_id = c.id - LEFT JOIN suppliers s ON i.supplier_id = s.id - WHERE i.stock_quantity <= i.min_stock_level - ORDER BY (i.min_stock_level - i.stock_quantity) DESC"); - $stmt->execute(); - $data['low_stock_items'] = $stmt->fetchAll(); - break; - case 'cashflow_report': - $start_date = $_GET['start_date'] ?? date('Y-m-01'); - $end_date = $_GET['end_date'] ?? date('Y-m-d'); - - // Fetch Cash & Bank Account IDs - $cash_accounts = db()->query("SELECT id FROM acc_accounts WHERE code IN (1100, 1200)")->fetchAll(PDO::FETCH_COLUMN); - $cash_ids_str = implode(',', $cash_accounts); - - if (!empty($cash_ids_str)) { - // Opening Balance - $stmt = db()->prepare("SELECT SUM(debit - credit) FROM acc_ledger l JOIN acc_journal_entries je ON l.journal_entry_id = je.id WHERE l.account_id IN ($cash_ids_str) AND je.entry_date < ?"); - $stmt->execute([$start_date]); - $data['opening_balance'] = $stmt->fetchColumn() ?: 0; - - // Transactions in range - $stmt = db()->prepare("SELECT - je.entry_date, - je.description, - l.debit as inflow, - l.credit as outflow, - a.name_en as other_account, - a.type as other_type - FROM acc_ledger l - JOIN acc_journal_entries je ON l.journal_entry_id = je.id - LEFT JOIN acc_ledger l2 ON l2.journal_entry_id = je.id AND l2.id != l.id - LEFT JOIN acc_accounts a ON l2.account_id = a.id - WHERE l.account_id IN ($cash_ids_str) - AND je.entry_date BETWEEN ? AND ? - ORDER BY je.entry_date ASC, je.id ASC"); - $stmt->execute([$start_date, $end_date]); - $data['cash_transactions'] = $stmt->fetchAll(PDO::FETCH_ASSOC); - } else { - $data['opening_balance'] = 0; - $data['cash_transactions'] = []; - } - break; - case 'hr_departments': - $data['departments'] = db()->query("SELECT * FROM hr_departments ORDER BY id DESC")->fetchAll(); - break; - case 'hr_employees': - $data['employees'] = db()->query("SELECT e.*, d.name as dept_name FROM hr_employees e LEFT JOIN hr_departments d ON e.department_id = d.id ORDER BY e.id DESC")->fetchAll(); - $data['departments'] = db()->query("SELECT * FROM hr_departments ORDER BY name ASC")->fetchAll(); - break; - case 'hr_attendance': - $date = $_GET['date'] ?? date('Y-m-d'); - $data['attendance_date'] = $date; - $data['employees'] = db()->query("SELECT e.id, e.name, d.name as dept_name, a.status, a.clock_in, a.clock_out - FROM hr_employees e - LEFT JOIN hr_departments d ON e.department_id = d.id - LEFT JOIN hr_attendance a ON e.id = a.employee_id AND a.attendance_date = '$date' - WHERE e.status = 'active' ORDER BY e.name ASC")->fetchAll(); - break; - case 'hr_payroll': - $month = (int)($_GET['month'] ?? date('m')); - $year = (int)($_GET['year'] ?? date('Y')); - $data['month'] = $month; - $data['year'] = $year; - $data['payroll'] = db()->query("SELECT p.*, e.name as emp_name FROM hr_payroll p JOIN hr_employees e ON p.employee_id = e.id WHERE p.payroll_month = $month AND p.payroll_year = $year ORDER BY p.id DESC")->fetchAll(); - $data['employees'] = db()->query("SELECT id, name, salary FROM hr_employees WHERE status = 'active' ORDER BY name ASC")->fetchAll(); - break; - case 'loyalty_history': - $where = ["1=1"]; - $params = []; - if (!empty($_GET['customer_id'])) { - $where[] = "lt.customer_id = ?"; - $params[] = (int)$_GET['customer_id']; - } - if (!empty($_GET['type'])) { - $where[] = "lt.transaction_type = ?"; - $params[] = $_GET['type']; - } - $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT lt.*, c.name as customer_name, c.loyalty_tier, c.loyalty_points - FROM loyalty_transactions lt - JOIN customers c ON lt.customer_id = c.id - WHERE $whereSql - ORDER BY lt.created_at DESC"); - $stmt->execute($params); - $data['loyalty_transactions'] = $stmt->fetchAll(); - break; - case 'devices': - $data['devices'] = db()->query("SELECT * FROM hr_biometric_devices ORDER BY id DESC")->fetchAll(); - break; - case 'scale_devices': - $data['scale_devices'] = db()->query("SELECT * FROM pos_devices ORDER BY id DESC")->fetchAll(); - break; - case 'cash_registers': - $data['cash_registers'] = db()->query("SELECT * FROM cash_registers ORDER BY id DESC")->fetchAll(); - break; - case 'register_sessions': - $where = ["1=1"]; - $params = []; - - // Filter by user if provided and user has permission - if (isset($_GET['user_id']) && !empty($_GET['user_id'])) { - if (can('users_view')) { - $where[] = "s.user_id = ?"; - $params[] = $_GET['user_id']; - } - } - - if (!can('users_view')) { - $where[] = "s.user_id = ?"; - $params[] = $_SESSION['user_id']; - } - - // Filter by date range - if (isset($_GET['date_from']) && !empty($_GET['date_from'])) { - $where[] = "s.opened_at >= ?"; - $params[] = $_GET['date_from'] . ' 00:00:00'; - } - if (isset($_GET['date_to']) && !empty($_GET['date_to'])) { - $where[] = "s.opened_at <= ?"; - $params[] = $_GET['date_to'] . ' 23:59:59'; - } - - $whereSql = implode(" AND ", $where); - $stmt = db()->prepare("SELECT s.*, r.name as register_name, u.username - FROM register_sessions s - LEFT JOIN cash_registers r ON s.register_id = r.id - LEFT JOIN users u ON s.user_id = u.id - WHERE $whereSql - ORDER BY s.id DESC"); - $stmt->execute($params); - $data['sessions'] = $stmt->fetchAll(); - $data['cash_registers'] = db()->query("SELECT * FROM cash_registers WHERE status = 'active'")->fetchAll(); - $data['users'] = db()->query("SELECT id, username FROM users ORDER BY username ASC")->fetchAll(); - break; - default: - if (can('dashboard_view')) { - $data['customers'] = db()->query("SELECT * FROM customers ORDER BY id DESC LIMIT 5")->fetchAll(); - $data['stats'] = [ - 'total_customers' => db()->query("SELECT COUNT(*) FROM customers")->fetchColumn(), - 'total_items' => db()->query("SELECT COUNT(*) FROM stock_items")->fetchColumn(), - 'total_sales' => (db()->query("SELECT SUM(total_with_vat) FROM invoices")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(net_amount) FROM pos_transactions WHERE status = 'completed'")->fetchColumn() ?: 0), - 'total_received' => (db()->query("SELECT SUM(amount) FROM payments")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(amount) FROM pos_payments")->fetchColumn() ?: 0), - 'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM purchases")->fetchColumn() ?: 0, - 'total_paid' => db()->query("SELECT SUM(amount) FROM purchase_payments")->fetchColumn() ?: 0, - 'expired_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE()")->fetchColumn(), - 'near_expiry_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)")->fetchColumn(), - 'low_stock_items_count' => db()->query("SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level")->fetchColumn(), - ]; - $data['stats']['total_receivable'] = $data['stats']['total_sales'] - $data['stats']['total_received']; - $data['stats']['total_payable'] = $data['stats']['total_purchases'] - $data['stats']['total_paid']; - - // Sales Chart Data - $data['monthly_sales'] = db()->query("SELECT DATE_FORMAT(invoice_date, '%M %Y') as label, SUM(total_with_vat) as total FROM invoices GROUP BY DATE_FORMAT(invoice_date, '%Y-%m') ORDER BY invoice_date ASC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC); - $data['yearly_sales'] = db()->query("SELECT YEAR(invoice_date) as label, SUM(total_with_vat) as total FROM invoices GROUP BY label ORDER BY label ASC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC); - } - break; -} - -$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; -?> - - - - - - <?= __('accounting') ?> - Admin Panel - - - - - - - - - - - - - - - - - - - - - - - 0): ?> -
- - . - -
- - - - - -+ query("SELECT * FROM outlets WHERE status = 'active'")->fetchAll(PDO::FETCH_ASSOC); -+ $cur_out = $_SESSION['outlet_id'] ?? 0; -+ $cur_name = 'All Outlets'; -+ foreach ($outlets as $o) { if ($o['id'] == $cur_out) $cur_name = $o['name']; } -+ ?> -+ -+ - - -+ -+
-+
-+
Manage Outlets
-+ -+
-+
-+
-+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+
IDNameAddressPhoneStatusCreated AtActions
# -+ -+ -+
-+ -+ -+
-+ -+
-+
-+
-+
-+ -+ -+ -+ -+ - -
-
diff --git a/includes/SimpleXLSX.php b/includes/SimpleXLSX.php new file mode 100644 index 0000000..292da12 --- /dev/null +++ b/includes/SimpleXLSX.php @@ -0,0 +1,1231 @@ +rows() as $r) { + * print_r( $r ); + * } + * } else { + * echo SimpleXLSX::parseError(); + * } + * + * Example 2: html table + * if ( $xlsx = SimpleXLSX::parse('book.xlsx') ) { + * echo $xlsx->toHTML(); + * } else { + * echo SimpleXLSX::parseError(); + * } + * + * Example 3: rowsEx + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * foreach ( $xlsx->rowsEx() as $r ) { + * print_r( $r ); + * } + * + * Example 4: select worksheet + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * foreach( $xlsx->rows(1) as $r ) { // second worksheet + * print_t( $r ); + * } + * + * Example 5: IDs and worksheet names + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * print_r( $xlsx->sheetNames() ); // array( 0 => 'Sheet 1', 1 => 'Catalog' ); + * + * Example 6: get sheet name by index + * $xlsx = SimpleXLSX::parse('book.xlsx'); + * echo 'Sheet Name 2 = '.$xlsx->sheetName(1); + * + * Example 7: getCell (very slow) + * echo $xlsx->getCell(1,'D12'); // reads D12 cell from second sheet + * + * Example 8: read data + * if ( $xlsx = SimpleXLSX::parseData( file_get_contents('http://www.example.com/example.xlsx') ) ) { + * $dim = $xlsx->dimension(1); + * $num_cols = $dim[0]; + * $num_rows = $dim[1]; + * echo $xlsx->sheetName(1).':'.$num_cols.'x'.$num_rows; + * } else { + * echo SimpleXLSX::parseError(); + * } + * + * Example 9: old style + * $xlsx = new SimpleXLSX('book.xlsx'); + * if ( $xlsx->success() ) { + * print_r( $xlsx->rows() ); + * } else { + * echo 'xlsx error: '.$xlsx->error(); + * } + */ +class SimpleXLSX +{ + // Don't remove this string! Created by Sergey Shuchkin sergey.shuchkin@gmail.com + public static $CF = [ // Cell formats + 0 => 'General', + 1 => '0', + 2 => '0.00', + 3 => '#,##0', + 4 => '#,##0.00', + 9 => '0%', + 10 => '0.00%', + 11 => '0.00E+00', + 12 => '# ?/?', + 13 => '# ??/??', + 14 => 'mm-dd-yy', + 15 => 'd-mmm-yy', + 16 => 'd-mmm', + 17 => 'mmm-yy', + 18 => 'h:mm AM/PM', + 19 => 'h:mm:ss AM/PM', + 20 => 'h:mm', + 21 => 'h:mm:ss', + 22 => 'm/d/yy h:mm', + + 37 => '#,##0 ;(#,##0)', + 38 => '#,##0 ;[Red](#,##0)', + 39 => '#,##0.00;(#,##0.00)', + 40 => '#,##0.00;[Red](#,##0.00)', + + 44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)', + 45 => 'mm:ss', + 46 => '[h]:mm:ss', + 47 => 'mmss.0', + 48 => '##0.0E+0', + 49 => '@', + + 27 => '[$-404]e/m/d', + 30 => 'm/d/yy', + 36 => '[$-404]e/m/d', + 50 => '[$-404]e/m/d', + 57 => '[$-404]e/m/d', + + 59 => 't0', + 60 => 't0.00', + 61 => 't#,##0', + 62 => 't#,##0.00', + 67 => 't0%', + 68 => 't0.00%', + 69 => 't# ?/?', + 70 => 't# ??/??', + ]; + public $nf = []; // number formats + public $cellFormats = []; // cellXfs + public $datetimeFormat = 'Y-m-d H:i:s'; + public $debug; + public $activeSheet = 0; + public $rowsExReader; + + /* @var SimpleXMLElement[] $sheets */ + public $sheets; + public $sheetFiles = []; + public $sheetMetaData = []; + public $sheetRels = []; + // scheme + public $styles; + /* @var array[] $package */ + public $package; + public $sharedstrings; + public $date1904 = 0; + + + /* + private $date_formats = array( + 0xe => "d/m/Y", + 0xf => "d-M-Y", + 0x10 => "d-M", + 0x11 => "M-Y", + 0x12 => "h:i a", + 0x13 => "h:i:s a", + 0x14 => "H:i", + 0x15 => "H:i:s", + 0x16 => "d/m/Y H:i", + 0x2d => "i:s", + 0x2e => "H:i:s", + 0x2f => "i:s.S" + ); + private $number_formats = array( + 0x1 => "%1.0f", // "0" + 0x2 => "%1.2f", // "0.00", + 0x3 => "%1.0f", //"#,##0", + 0x4 => "%1.2f", //"#,##0.00", + 0x5 => "%1.0f", //"$#,##0;($#,##0)", + 0x6 => '$%1.0f', //"$#,##0;($#,##0)", + 0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x9 => '%1.0f%%', //"0%" + 0xa => '%1.2f%%', //"0.00%" + 0xb => '%1.2f', //"0.00E00", + 0x25 => '%1.0f', //"#,##0;(#,##0)", + 0x26 => '%1.0f', //"#,##0;(#,##0)", + 0x27 => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x28 => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x29 => '%1.0f', //"#,##0;(#,##0)", + 0x2a => '$%1.0f', //"$#,##0;($#,##0)", + 0x2b => '%1.2f', //"#,##0.00;(#,##0.00)", + 0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)", + 0x30 => '%1.0f'); //"##0.0E0"; + // }}} + */ + public $errno = 0; + public $error = false; + /** + * @var false|SimpleXMLElement + */ + public $theme; + + + public function __construct($filename = null, $is_data = null, $debug = null) + { + if ($debug !== null) { + $this->debug = $debug; + } + $this->package = [ + 'filename' => '', + 'mtime' => 0, + 'size' => 0, + 'comment' => '', + 'entries' => [] + ]; + if ($filename && $this->unzip($filename, $is_data)) { + $this->parseEntries(); + } + } + + public function unzip($filename, $is_data = false) + { + + if ($is_data) { + $this->package['filename'] = 'default.xlsx'; + $this->package['mtime'] = time(); + $this->package['size'] = self::strlen($filename); + + $vZ = $filename; + } else { + if (!is_readable($filename)) { + $this->error(1, 'File not found ' . $filename); + + return false; + } + + // Package information + $this->package['filename'] = $filename; + $this->package['mtime'] = filemtime($filename); + $this->package['size'] = filesize($filename); + + // Read file + $vZ = file_get_contents($filename); + } + // Cut end of central directory + /* $aE = explode("\x50\x4b\x05\x06", $vZ); + + if (count($aE) == 1) { + $this->error('Unknown format'); + return false; + } + */ + // Explode to each part + $aE = explode("\x50\x4b\x03\x04", $vZ); + array_shift($aE); + + $aEL = count($aE); + if ($aEL === 0) { + $this->error(2, 'Unknown archive format'); + + return false; + } + // Search central directory end record + $last = $aE[$aEL - 1]; + $last = explode("\x50\x4b\x05\x06", $last); + if (count($last) !== 2) { + $this->error(2, 'Unknown archive format'); + + return false; + } + // Search central directory + $last = explode("\x50\x4b\x01\x02", $last[0]); + if (count($last) < 2) { + $this->error(2, 'Unknown archive format'); + + return false; + } + $aE[$aEL - 1] = $last[0]; + + // Loop through the entries + foreach ($aE as $vZ) { + $aI = []; + $aI['E'] = 0; + $aI['EM'] = ''; + // Retrieving local file header information +// $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL', $vZ); + $aP = unpack('v1VN/v1GPF/v1CM/v1FT/v1FD/V1CRC/V1CS/V1UCS/v1FNL/v1EFL', $vZ); + + // Check if data is encrypted +// $bE = ($aP['GPF'] && 0x0001) ? TRUE : FALSE; +// $bE = false; + $nF = $aP['FNL']; + $mF = $aP['EFL']; + + // Special case : value block after the compressed data + if ($aP['GPF'] & 0x0008) { + $aP1 = unpack('V1CRC/V1CS/V1UCS', self::substr($vZ, -12)); + + $aP['CRC'] = $aP1['CRC']; + $aP['CS'] = $aP1['CS']; + $aP['UCS'] = $aP1['UCS']; + // 2013-08-10 + $vZ = self::substr($vZ, 0, -12); + if (self::substr($vZ, -4) === "\x50\x4b\x07\x08") { + $vZ = self::substr($vZ, 0, -4); + } + } + + // Getting stored filename + $aI['N'] = self::substr($vZ, 26, $nF); + $aI['N'] = str_replace('\\', '/', $aI['N']); + + if (self::substr($aI['N'], -1) === '/') { + // is a directory entry - will be skipped + continue; + } + + // Truncate full filename in path and filename + $aI['P'] = dirname($aI['N']); + $aI['P'] = ($aI['P'] === '.') ? '' : $aI['P']; + $aI['N'] = basename($aI['N']); + + $vZ = self::substr($vZ, 26 + $nF + $mF); + + if ($aP['CS'] > 0 && (self::strlen($vZ) !== (int)$aP['CS'])) { // check only if availabled + $aI['E'] = 1; + $aI['EM'] = 'Compressed size is not equal with the value in header information.'; + } +// } elseif ( $bE ) { +// $aI['E'] = 5; +// $aI['EM'] = 'File is encrypted, which is not supported from this class.'; +/* } else { + switch ($aP['CM']) { + case 0: // Stored + // Here is nothing to do, the file ist flat. + break; + case 8: // Deflated + $vZ = gzinflate($vZ); + break; + case 12: // BZIP2 + if (extension_loaded('bz2')) { + $vZ = bzdecompress($vZ); + } else { + $aI['E'] = 7; + $aI['EM'] = 'PHP BZIP2 extension not available.'; + } + break; + default: + $aI['E'] = 6; + $aI['EM'] = "De-/Compression method {$aP['CM']} is not supported."; + } + if (!$aI['E']) { + if ($vZ === false) { + $aI['E'] = 2; + $aI['EM'] = 'Decompression of data failed.'; + } elseif ($this->_strlen($vZ) !== (int)$aP['UCS']) { + $aI['E'] = 3; + $aI['EM'] = 'Uncompressed size is not equal with the value in header information.'; + } elseif (crc32($vZ) !== $aP['CRC']) { + $aI['E'] = 4; + $aI['EM'] = 'CRC32 checksum is not equal with the value in header information.'; + } + } + } +*/ + + // DOS to UNIX timestamp + $aI['T'] = mktime( + ($aP['FT'] & 0xf800) >> 11, + ($aP['FT'] & 0x07e0) >> 5, + ($aP['FT'] & 0x001f) << 1, + ($aP['FD'] & 0x01e0) >> 5, + $aP['FD'] & 0x001f, + (($aP['FD'] & 0xfe00) >> 9) + 1980 + ); + + $this->package['entries'][] = [ + 'data' => $vZ, + 'ucs' => (int)$aP['UCS'], // ucompresses size + 'cm' => $aP['CM'], // compressed method + 'cs' => isset($aP['CS']) ? (int) $aP['CS'] : 0, // compresses size + 'crc' => $aP['CRC'], + 'error' => $aI['E'], + 'error_msg' => $aI['EM'], + 'name' => $aI['N'], + 'path' => $aI['P'], + 'time' => $aI['T'] + ]; + } // end for each entries + + return true; + } + + + public function error($num = null, $str = null) + { + if ($num) { + $this->errno = $num; + $this->error = $str; + if ($this->debug) { + trigger_error(__CLASS__ . ': ' . $this->error, E_USER_WARNING); + } + } + + return $this->error; + } + + public function parseEntries() + { + // Document data holders + $this->sharedstrings = []; + $this->sheets = []; +// $this->styles = array(); +// $m1 = 0; // memory_get_peak_usage( true ); + // Read relations and search for officeDocument + if ($relations = $this->getEntryXML('_rels/.rels')) { + foreach ($relations->Relationship as $rel) { + $rel_type = basename(trim((string)$rel['Type'])); // officeDocument + $rel_target = self::getTarget('', (string)$rel['Target']); // /xl/workbook.xml or xl/workbook.xml + + if ($rel_type === 'officeDocument' + && $workbook = $this->getEntryXML($rel_target) + ) { + $index_rId = []; // [0 => rId1] + + $index = 0; + foreach ($workbook->sheets->sheet as $s) { + $a = []; + foreach ($s->attributes() as $k => $v) { + $a[(string)$k] = (string)$v; + } + $this->sheetMetaData[$index] = $a; + $index_rId[$index] = (string)$s['id']; + $index++; + } + if ((int)$workbook->workbookPr['date1904'] === 1) { + $this->date1904 = 1; + } + + + if ($workbookRelations = $this->getEntryXML(dirname($rel_target) . '/_rels/workbook.xml.rels')) { + // Loop relations for workbook and extract sheets... + foreach ($workbookRelations->Relationship as $workbookRelation) { + $wrel_type = basename(trim((string)$workbookRelation['Type'])); // worksheet + $wrel_target = self::getTarget(dirname($rel_target), (string)$workbookRelation['Target']); + if (!$this->entryExists($wrel_target)) { + continue; + } + + if ($wrel_type === 'worksheet') { // Sheets + if ($sheet = $this->getEntryXML($wrel_target)) { + $index = array_search((string)$workbookRelation['Id'], $index_rId, true); + $this->sheets[$index] = $sheet; + $this->sheetFiles[$index] = $wrel_target; + $srel_d = dirname($wrel_target); + $srel_f = basename($wrel_target); + $srel_file = $srel_d . '/_rels/' . $srel_f . '.rels'; + if ($this->entryExists($srel_file)) { + $this->sheetRels[$index] = $this->getEntryXML($srel_file); + } + } + } elseif ($wrel_type === 'sharedStrings') { + if ($sharedStrings = $this->getEntryXML($wrel_target)) { + foreach ($sharedStrings->si as $val) { + if (isset($val->t)) { + $this->sharedstrings[] = (string)$val->t; + } elseif (isset($val->r)) { + $this->sharedstrings[] = self::parseRichText($val); + } + } + } + } elseif ($wrel_type === 'styles') { + $this->styles = $this->getEntryXML($wrel_target); + + // number formats + $this->nf = []; + if (isset($this->styles->numFmts->numFmt)) { + foreach ($this->styles->numFmts->numFmt as $v) { + $this->nf[(int)$v['numFmtId']] = (string)$v['formatCode']; + } + } + + $this->cellFormats = []; + if (isset($this->styles->cellXfs->xf)) { + foreach ($this->styles->cellXfs->xf as $v) { + $x = [ + 'format' => null + ]; + foreach ($v->attributes() as $k1 => $v1) { + $x[ $k1 ] = (int) $v1; + } + if (isset($x['numFmtId'])) { + if (isset($this->nf[$x['numFmtId']])) { + $x['format'] = $this->nf[$x['numFmtId']]; + } elseif (isset(self::$CF[$x['numFmtId']])) { + $x['format'] = self::$CF[$x['numFmtId']]; + } + } + + $this->cellFormats[] = $x; + } + } + } elseif ($wrel_type === 'theme') { + $this->theme = $this->getEntryXML($wrel_target); + } + } + +// break; + } + // reptile hack :: find active sheet from workbook.xml + if ($workbook->bookViews->workbookView) { + foreach ($workbook->bookViews->workbookView as $v) { + if (!empty($v['activeTab'])) { + $this->activeSheet = (int)$v['activeTab']; + } + } + } + + break; + } + } + } + +// $m2 = memory_get_peak_usage(true); +// echo __FUNCTION__.' M='.round( ($m2-$m1) / 1048576, 2).'MB'.PHP_EOL; + + if (count($this->sheets)) { + // Sort sheets + ksort($this->sheets); + + return true; + } + + return false; + } + + public function getEntryXML($name) + { + if ($entry_xml = $this->getEntryData($name)) { + $this->deleteEntry($name); // economy memory + // dirty remove namespace prefixes and empty rows + $entry_xml = preg_replace('/xmlns[^=]*="[^"]*"/i', '', $entry_xml); // remove namespaces + $entry_xml .= ' '; // force run garbage collector + // remove namespaced attrs + $entry_xml = preg_replace('/[a-zA-Z0-9]+:([a-zA-Z0-9]+="[^"]+")/', '$1', $entry_xml); + $entry_xml .= ' '; + $entry_xml = preg_replace('/<[a-zA-Z0-9]+:([^>]+)>/', '<$1>', $entry_xml); // fix namespaced openned tags + $entry_xml .= ' '; + $entry_xml = preg_replace('/<\/[a-zA-Z0-9]+:([^>]+)>/', '', $entry_xml); // fix namespaced closed tags + $entry_xml .= ' '; + + if (strpos($name, '/sheet')) { // dirty skip empty rows + // remove + $cnt = $cnt2 = $cnt3 = null; + $entry_xml = preg_replace('/]+>\s*(\s*)+<\/row>/', '', $entry_xml, -1, $cnt); + $entry_xml .= ' '; + // remove + $entry_xml = preg_replace('/]*\/>/', '', $entry_xml, -1, $cnt2); + $entry_xml .= ' '; + // remove + $entry_xml = preg_replace('/]*><\/row>/', '', $entry_xml, -1, $cnt3); + $entry_xml .= ' '; + if ($cnt || $cnt2 || $cnt3) { + $entry_xml = preg_replace('//', '', $entry_xml); + $entry_xml .= ' '; + } +// file_put_contents( basename( $name ), $entry_xml ); // @to do comment!!! + } + $entry_xml = trim($entry_xml); + +// $m1 = memory_get_usage(); + // XML External Entity (XXE) Prevention, libxml_disable_entity_loader deprecated in PHP 8 + if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) { + $_old = libxml_disable_entity_loader(); + } + + $_old_uie = libxml_use_internal_errors(true); + + $entry_xmlobj = simplexml_load_string($entry_xml, 'SimpleXMLElement', LIBXML_COMPACT | LIBXML_PARSEHUGE); + + libxml_use_internal_errors($_old_uie); + + if (LIBXML_VERSION < 20900 && function_exists('libxml_disable_entity_loader')) { + /** @noinspection PhpUndefinedVariableInspection */ + libxml_disable_entity_loader($_old); + } + +// $m2 = memory_get_usage(); +// echo round( ($m2-$m1) / (1024 * 1024), 2).' MB'.PHP_EOL; + + if ($entry_xmlobj) { + return $entry_xmlobj; + } + $e = libxml_get_last_error(); + if ($e) { + $this->error(3, 'XML-entry ' . $name . ' parser error ' . $e->message . ' line ' . $e->line); + } + } else { + $this->error(4, 'XML-entry not found ' . $name); + } + + return false; + } + + // sheets numeration: 1,2,3.... + + public function getEntryData($name) + { + $name = ltrim(str_replace('\\', '/', $name), '/'); + $dir = self::strtoupper(dirname($name)); + $name = self::strtoupper(basename($name)); + foreach ($this->package['entries'] as &$entry) { + if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) { + if ($entry['error']) { + return false; + } + switch ($entry['cm']) { + case -1: + case 0: // Stored + // Here is nothing to do, the file ist flat. + break; + case 8: // Deflated + $entry['data'] = gzinflate($entry['data']); + break; + case 12: // BZIP2 + if (extension_loaded('bz2')) { + $entry['data'] = bzdecompress($entry['data']); + } else { + $entry['error'] = 7; + $entry['error_message'] = 'PHP BZIP2 extension not available.'; + } + break; + default: + $entry['error'] = 6; + $entry['error_msg'] = 'De-/Compression method '.$entry['cm'].' is not supported.'; + } + if (!$entry['error'] && $entry['cm'] > -1) { + $entry['cm'] = -1; + if ($entry['data'] === false) { + $entry['error'] = 2; + $entry['error_msg'] = 'Decompression of data failed.'; + } elseif ($entry['ucs'] > 0 && (self::strlen($entry['data']) !== (int)$entry['ucs'])) { + $entry['error'] = 3; + $entry['error_msg'] = 'Uncompressed size is not equal with the value in header information.'; + } elseif (crc32($entry['data']) !== $entry['crc']) { + $entry['error'] = 4; + $entry['error_msg'] = 'CRC32 checksum is not equal with the value in header information.'; + } + } + + return $entry['data']; + } + } + unset($entry); + $this->error(5, 'Entry not found ' . ($dir ? $dir . '/' : '') . $name); + + return false; + } + public function deleteEntry($name) + { + $name = ltrim(str_replace('\\', '/', $name), '/'); + $dir = self::strtoupper(dirname($name)); + $name = self::strtoupper(basename($name)); + foreach ($this->package['entries'] as $k => $entry) { + if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) { + unset($this->package['entries'][$k]); + return true; + } + } + return false; + } + + public static function strtoupper($str) + { + return (ini_get('mbstring.func_overload') & 2) ? mb_strtoupper($str, '8bit') : strtoupper($str); + } + + /* + * @param string $name Filename in archive + * @return SimpleXMLElement|bool + */ + + public function entryExists($name) + { + // 0.6.6 + $dir = self::strtoupper(dirname($name)); + $name = self::strtoupper(basename($name)); + foreach ($this->package['entries'] as $entry) { + if (self::strtoupper($entry['path']) === $dir && self::strtoupper($entry['name']) === $name) { + return true; + } + } + + return false; + } + + public static function parseFile($filename, $debug = false) + { + return self::parse($filename, false, $debug); + } + + public static function parse($filename, $is_data = false, $debug = false) + { + $xlsx = new self(); + $xlsx->debug = $debug; + if ($xlsx->unzip($filename, $is_data)) { + $xlsx->parseEntries(); + } + if ($xlsx->success()) { + return $xlsx; + } + self::parseError($xlsx->error()); + self::parseErrno($xlsx->errno()); + + return false; + } + + public function success() + { + return !$this->error; + } + + // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex + + public static function parseError($set = false) + { + static $error = false; + + return $set ? $error = $set : $error; + } + + public static function parseErrno($set = false) + { + static $errno = false; + + return $set ? $errno = $set : $errno; + } + + public function errno() + { + return $this->errno; + } + + public static function parseData($data, $debug = false) + { + return self::parse($data, true, $debug); + } + + + + public function worksheet($worksheetIndex = 0) + { + if (isset($this->sheets[$worksheetIndex])) { + return $this->sheets[$worksheetIndex]; + } + $this->error(6, 'Worksheet not found ' . $worksheetIndex); + + return false; + } + + /** + * returns [numCols,numRows] of worksheet + * + * @param int $worksheetIndex + * + * @return array + */ + public function dimension($worksheetIndex = 0) + { + + if (($ws = $this->worksheet($worksheetIndex)) === false) { + return [0, 0]; + } + /* @var SimpleXMLElement $ws */ + + $ref = (string)$ws->dimension['ref']; + + if (self::strpos($ref, ':') !== false) { + $d = explode(':', $ref); + $idx = $this->getIndex($d[1]); + + return [$idx[0] + 1, $idx[1] + 1]; + } + /* + if ( $ref !== '' ) { // 0.6.8 + $index = $this->getIndex( $ref ); + + return [ $index[0] + 1, $index[1] + 1 ]; + } + */ + + // slow method + $maxC = $maxR = 0; + $iR = -1; + foreach ($ws->sheetData->row as $row) { + $iR++; + $iC = -1; + foreach ($row->c as $c) { + $iC++; + $idx = $this->getIndex((string)$c['r']); + $x = $idx[0]; + $y = $idx[1]; + if ($x > -1) { + if ($x > $maxC) { + $maxC = $x; + } + if ($y > $maxR) { + $maxR = $y; + } + } else { + if ($iC > $maxC) { + $maxC = $iC; + } + if ($iR > $maxR) { + $maxR = $iR; + } + } + } + } + + return [$maxC + 1, $maxR + 1]; + } + + public function getIndex($cell = 'A1') + { + $m = null; + + if (preg_match('/([A-Z]+)(\d+)/', $cell, $m)) { + $col = $m[1]; + $row = $m[2]; + + $colLen = self::strlen($col); + $index = 0; + + for ($i = $colLen - 1; $i >= 0; $i--) { + $index += (ord($col[$i]) - 64) * pow(26, $colLen - $i - 1); + } + + return [$index - 1, $row - 1]; + } + +// $this->error( 'Invalid cell index ' . $cell ); + + return [-1, -1]; + } + + public function value($cell) + { + // Determine data type + $dataType = (string)$cell['t']; + + if ($dataType === '' || $dataType === 'n') { // number + $s = (int)$cell['s']; + if ($s > 0 && isset($this->cellFormats[$s])) { + if (array_key_exists('format', $this->cellFormats[$s])) { + $format = $this->cellFormats[$s]['format']; + if ($format && preg_match('/[mM]/', preg_replace('/\"[^"]+\"/', '', $format))) { // [mm]onth,AM|PM + $dataType = 'D'; + } + } else { + $dataType = 'n'; + } + } + } + + $value = ''; + + switch ($dataType) { + case 's': + // Value is a shared string + if ((string)$cell->v !== '') { + $value = $this->sharedstrings[(int)$cell->v]; + } + break; + + case 'str': // formula? + if ((string)$cell->v !== '') { + $value = (string)$cell->v; + } + break; + + case 'b': + // Value is boolean + $value = self::boolean((string)$cell->v); + + break; + + case 'inlineStr': + // Value is rich text inline + $value = self::parseRichText($cell->is); + + break; + + case 'e': + // Value is an error message + if ((string)$cell->v !== '') { + $value = (string)$cell->v; + } + break; + + case 'D': + // Date as float + if (!empty($cell->v)) { + $value = $this->datetimeFormat ? + gmdate($this->datetimeFormat, $this->unixstamp((float)$cell->v)) : (float)$cell->v; + } + break; + + case 'd': + // Date as ISO YYYY-MM-DD + if ((string)$cell->v !== '') { + $value = (string)$cell->v; + } + break; + + default: + // Value is a string + $value = (string)$cell->v; + + // Check for numeric values + if (is_numeric($value)) { + /** @noinspection TypeUnsafeComparisonInspection */ + if ($value == (int)$value) { + $value = (int)$value; + } /** @noinspection TypeUnsafeComparisonInspection */ elseif ($value == (float)$value) { + $value = (float)$value; + } + } + } + + return $value; + } + + public function unixstamp($excelDateTime) + { + + $d = floor($excelDateTime); // days since 1900 or 1904 + $t = $excelDateTime - $d; + + if ($this->date1904) { + $d += 1462; + } + + $t = (abs($d) > 0) ? ($d - 25569) * 86400 + round($t * 86400) : round($t * 86400); + + return (int)$t; + } + + public function toHTML($worksheetIndex = 0) + { + $s = ''; + foreach ($this->readRows($worksheetIndex) as $r) { + $s .= ''; + foreach ($r as $c) { + $s .= ''; + } + $s .= "\r\n"; + } + $s .= '
' . ($c === '' ? ' ' : htmlspecialchars($c, ENT_QUOTES)) . '
'; + + return $s; + } + public function toHTMLEx($worksheetIndex = 0) + { + $s = ''; + $y = 0; + foreach ($this->readRowsEx($worksheetIndex) as $r) { + $s .= ''; + $x = 0; + foreach ($r as $c) { + $tag = 'td'; + $css = $c['css']; + if ($y === 0) { + $tag = 'th'; + $css .= $c['width'] ? 'width: '.round($c['width'] * 0.47, 2).'em;' : ''; + } + + if ($x === 0 && $c['height']) { + $css .= 'height: '.round($c['height'] * 1.3333).'px;'; + } + $s .= '<'.$tag.' style="'.$css.'" nowrap>' + . ($c['value'] === '' ? ' ' : htmlspecialchars($c['value'], ENT_QUOTES)) . ''; + $x++; + } + $s .= "\r\n"; + $y++; + } + $s .= '
'; + + return $s; + } + public function rows($worksheetIndex = 0, $limit = 0) + { + return iterator_to_array($this->readRows($worksheetIndex, $limit), false); + } + // thx Gonzo + /** + * @param $worksheetIndex + * @param $limit + * @return \Generator + */ + public function readRows($worksheetIndex = 0, $limit = 0) + { + + if (($ws = $this->worksheet($worksheetIndex)) === false) { + return; + } + $dim = $this->dimension($worksheetIndex); + $numCols = $dim[0]; + $numRows = $dim[1]; + + $emptyRow = []; + for ($i = 0; $i < $numCols; $i++) { + $emptyRow[] = ''; + } + + $curR = 0; + $_limit = $limit; + /* @var SimpleXMLElement $ws */ + foreach ($ws->sheetData->row as $row) { + $r = $emptyRow; + $curC = 0; + foreach ($row->c as $c) { + // detect skipped cols + $idx = $this->getIndex((string)$c['r']); + $x = $idx[0]; + $y = $idx[1]; + + if ($x > -1) { + $curC = $x; + while ($curR < $y) { + yield $emptyRow; + $curR++; + $_limit--; + if ($_limit === 0) { + return; + } + } + } + $r[$curC] = $this->value($c); + $curC++; + } + yield $r; + + $curR++; + $_limit--; + if ($_limit === 0) { + return; + } + } + while ($curR < $numRows) { + yield $emptyRow; + $curR++; + $_limit--; + if ($_limit === 0) { + return; + } + } + } + + public function rowsEx($worksheetIndex = 0, $limit = 0) + { + return iterator_to_array($this->readRowsEx($worksheetIndex, $limit), false); + } + // https://github.com/shuchkin/simplexlsx#gets-extend-cell-info-by--rowsex + /** + * @param $worksheetIndex + * @param $limit + * @return \Generator|null + */ + public function readRowsEx($worksheetIndex = 0, $limit = 0) + { + if (!$this->rowsExReader) { + require_once __DIR__ . '/SimpleXLSXEx.php'; + $this->rowsExReader = new SimpleXLSXEx($this); + } + return $this->rowsExReader->readRowsEx($worksheetIndex, $limit); + } + + /** + * Returns cell value + * VERY SLOW! Use ->rows() or ->rowsEx() + * + * @param int $worksheetIndex + * @param string|array $cell ref or coords, D12 or [3,12] + * + * @return mixed Returns NULL if not found + */ + public function getCell($worksheetIndex = 0, $cell = 'A1') + { + + if (($ws = $this->worksheet($worksheetIndex)) === false) { + return false; + } + if (is_array($cell)) { + $cell = self::num2name($cell[0]) . $cell[1];// [3,21] -> D21 + } + if (is_string($cell)) { + $result = $ws->sheetData->xpath("row/c[@r='" . $cell . "']"); + if (count($result)) { + return $this->value($result[0]); + } + } + + return null; + } + + public function getSheets() + { + return $this->sheets; + } + + public function sheetsCount() + { + return count($this->sheets); + } + + public function sheetName($worksheetIndex) + { + $sn = $this->sheetNames(); + if (isset($sn[$worksheetIndex])) { + return $sn[$worksheetIndex]; + } + + return false; + } + + public function sheetNames() + { + $a = []; + foreach ($this->sheetMetaData as $k => $v) { + $a[$k] = $v['name']; + } + return $a; + } + public function sheetMeta($worksheetIndex = null) + { + if ($worksheetIndex === null) { + return $this->sheetMetaData; + } + return isset($this->sheetMetaData[$worksheetIndex]) ? $this->sheetMetaData[$worksheetIndex] : false; + } + public function isHiddenSheet($worksheetIndex) + { + return isset($this->sheetMetaData[$worksheetIndex]['state']) + && $this->sheetMetaData[$worksheetIndex]['state'] === 'hidden'; + } + + public function getStyles() + { + return $this->styles; + } + + public function getPackage() + { + return $this->package; + } + + public function setDateTimeFormat($value) + { + $this->datetimeFormat = is_string($value) ? $value : false; + } + + public static function getTarget($base, $target) + { + $target = trim($target); + if (strpos($target, '/') === 0) { + return self::substr($target, 1); + } + $target = ($base ? $base . '/' : '') . $target; + // a/b/../c -> a/c + $parts = explode('/', $target); + $abs = []; + foreach ($parts as $p) { + if ('.' === $p) { + continue; + } + if ('..' === $p) { + array_pop($abs); + } else { + $abs[] = $p; + } + } + return implode('/', $abs); + } + + public static function parseRichText($is = null) + { + $value = []; + + if (isset($is->t)) { + $value[] = (string)$is->t; + } elseif (isset($is->r)) { + foreach ($is->r as $run) { + $value[] = (string)$run->t; + } + } + + return implode('', $value); + } + + public static function num2name($num) + { + $numeric = ($num - 1) % 26; + $letter = chr(65 + $numeric); + $num2 = (int)(($num - 1) / 26); + if ($num2 > 0) { + return self::num2name($num2) . $letter; + } + return $letter; + } + + public static function strlen($str) + { + return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str, '8bit') : strlen($str); + } + + public static function substr($str, $start, $length = null) + { + return (ini_get('mbstring.func_overload') & 2) ? + mb_substr($str, $start, ($length === null) ? mb_strlen($str, '8bit') : $length, '8bit') + : substr($str, $start, ($length === null) ? strlen($str) : $length); + } + + public static function strpos($haystack, $needle, $offset = 0) + { + return (ini_get('mbstring.func_overload') & 2) ? + mb_strpos($haystack, $needle, $offset, '8bit') : strpos($haystack, $needle, $offset); + } + public static function boolean($value) + { + if (is_numeric($value)) { + return (bool) $value; + } + + return $value === 'true' || $value === 'TRUE'; + } +} diff --git a/index.php b/index.php index 4ff0fdb..9894280 100644 --- a/index.php +++ b/index.php @@ -1317,14 +1317,216 @@ function getPromotionalPrice($item) { redirectWithMessage("Expense recorded!", "index.php?page=expenses"); } + if (isset($_POST['import_customers'])) { + if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { + $tmpPath = $_FILES['excel_file']['tmp_name']; + $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + $count = 0; $errors = 0; + + if ($firstBytes === "PK\x03\x04") { + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + foreach ($xlsx->rows() as $i => $data) { + if ($i === 0) continue; + $name = trim($data[0] ?? ''); + if (!$name) { $errors++; continue; } + $email = trim($data[1] ?? ''); + $phone = trim($data[2] ?? ''); + $tax_id = trim($data[3] ?? ''); + $balance = (float)($data[4] ?? 0); + + db()->prepare("INSERT INTO customers (name, email, phone, tax_id, balance) VALUES (?, ?, ?, ?, ?)") + ->execute([$name, $email, $phone, $tax_id, $balance]); + $count++; + } + } + } else { + // CSV fallback + $handle = fopen($tmpPath, "r"); + fgetcsv($handle); // skip header + while (($data = fgetcsv($handle)) !== FALSE) { + $name = trim($data[0] ?? ''); + if (!$name) { $errors++; continue; } + $email = trim($data[1] ?? ''); + $phone = trim($data[2] ?? ''); + $tax_id = trim($data[3] ?? ''); + $balance = (float)($data[4] ?? 0); + db()->prepare("INSERT INTO customers (name, email, phone, tax_id, balance) VALUES (?, ?, ?, ?, ?)") + ->execute([$name, $email, $phone, $tax_id, $balance]); + $count++; + } + fclose($handle); + } + redirectWithMessage("Customers imported! $count processed." . ($errors ? " ($errors skipped)" : ""), "index.php?page=customers"); + } + } + + if (isset($_POST['import_suppliers'])) { + if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { + $tmpPath = $_FILES['excel_file']['tmp_name']; + $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + $count = 0; $errors = 0; + + if ($firstBytes === "PK\x03\x04") { + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + foreach ($xlsx->rows() as $i => $data) { + if ($i === 0) continue; + $name = trim($data[0] ?? ''); + if (!$name) { $errors++; continue; } + $email = trim($data[1] ?? ''); + $phone = trim($data[2] ?? ''); + $tax_id = trim($data[3] ?? ''); + $balance = (float)($data[4] ?? 0); + + db()->prepare("INSERT INTO suppliers (name, email, phone, tax_id, balance) VALUES (?, ?, ?, ?, ?)") + ->execute([$name, $email, $phone, $tax_id, $balance]); + $count++; + } + } + } else { + // CSV fallback + $handle = fopen($tmpPath, "r"); + fgetcsv($handle); // skip header + while (($data = fgetcsv($handle)) !== FALSE) { + $name = trim($data[0] ?? ''); + if (!$name) { $errors++; continue; } + $email = trim($data[1] ?? ''); + $phone = trim($data[2] ?? ''); + $tax_id = trim($data[3] ?? ''); + $balance = (float)($data[4] ?? 0); + db()->prepare("INSERT INTO suppliers (name, email, phone, tax_id, balance) VALUES (?, ?, ?, ?, ?)") + ->execute([$name, $email, $phone, $tax_id, $balance]); + $count++; + } + fclose($handle); + } + redirectWithMessage("Suppliers imported! $count processed." . ($errors ? " ($errors skipped)" : ""), "index.php?page=suppliers"); + } + } + + if (isset($_POST['import_categories'])) { + if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { + $tmpPath = $_FILES['excel_file']['tmp_name']; + $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + $count = 0; $errors = 0; + + if ($firstBytes === "PK\x03\x04") { + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + foreach ($xlsx->rows() as $i => $data) { + if ($i === 0) continue; + $name_en = trim($data[0] ?? ''); + if (!$name_en) { $errors++; continue; } + $name_ar = trim($data[1] ?? ''); + + db()->prepare("INSERT INTO stock_categories (name_en, name_ar) VALUES (?, ?)") + ->execute([$name_en, $name_ar]); + $count++; + } + } + } else { + $handle = fopen($tmpPath, "r"); + fgetcsv($handle); // skip header + while (($data = fgetcsv($handle)) !== FALSE) { + $name_en = trim($data[0] ?? ''); + if (!$name_en) { $errors++; continue; } + $name_ar = trim($data[1] ?? ''); + db()->prepare("INSERT INTO stock_categories (name_en, name_ar) VALUES (?, ?)") + ->execute([$name_en, $name_ar]); + $count++; + } + fclose($handle); + } + redirectWithMessage("Categories imported! $count processed.", "index.php?page=stock_categories"); + } + } + + if (isset($_POST['import_units'])) { + if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { + $tmpPath = $_FILES['excel_file']['tmp_name']; + $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + $count = 0; $errors = 0; + + if ($firstBytes === "PK\x03\x04") { + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + foreach ($xlsx->rows() as $i => $data) { + if ($i === 0) continue; + $name_en = trim($data[0] ?? ''); + if (!$name_en) { $errors++; continue; } + $name_ar = trim($data[1] ?? ''); + $short_name_en = trim($data[2] ?? ''); + $short_name_ar = trim($data[3] ?? ''); + + db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_name_en, short_name_ar) VALUES (?, ?, ?, ?)") + ->execute([$name_en, $name_ar, $short_name_en, $short_name_ar]); + $count++; + } + } + } else { + $handle = fopen($tmpPath, "r"); + fgetcsv($handle); + while (($data = fgetcsv($handle)) !== FALSE) { + $name_en = trim($data[0] ?? ''); + if (!$name_en) { $errors++; continue; } + $name_ar = trim($data[1] ?? ''); + $short_name_en = trim($data[2] ?? ''); + $short_name_ar = trim($data[3] ?? ''); + db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_name_en, short_name_ar) VALUES (?, ?, ?, ?)") + ->execute([$name_en, $name_ar, $short_name_en, $short_name_ar]); + $count++; + } + fclose($handle); + } + redirectWithMessage("Units imported! $count processed.", "index.php?page=stock_units"); + } + } + if (isset($_POST['import_items'])) { error_log("Import items triggered. POST: " . print_r($_POST, true)); if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { $tmpPath = $_FILES['excel_file']['tmp_name']; error_log("File uploaded to: $tmpPath"); $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + if ($firstBytes === "PK\x03\x04") { - $message = "Error: It looks like you uploaded an Excel (.xlsx) file. Please save it as CSV (UTF-8) and try again."; + // EXCEL IMPORT VIA SimpleXLSX + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + $rows = $xlsx->rows(); + $rowNum = 0; + $count = 0; + $errors = 0; + foreach ($rows as $data) { + $rowNum++; + if ($rowNum === 1) continue; // Skip header + + $sku = trim($data[0] ?? ''); + $name_en = trim($data[1] ?? ''); + $name_ar = trim($data[2] ?? ''); + $sale_price = (float)($data[3] ?? 0); + $purchase_price = (float)($data[4] ?? 0); + $qty = (float)($data[5] ?? 0); + $vat_rate = (float)($data[6] ?? 0); + + if (!$sku && !$name_en) { $errors++; continue; } + + $check = db()->prepare("SELECT id FROM stock_items WHERE sku = ?"); + $check->execute([$sku]); + if ($check->fetch()) { + db()->prepare("UPDATE stock_items SET name_en = ?, name_ar = ?, sale_price = ?, purchase_price = ?, stock_quantity = ?, vat_rate = ? WHERE sku = ?") + ->execute([$name_en, $name_ar, $sale_price, $purchase_price, $qty, $vat_rate, $sku]); + } else { + db()->prepare("INSERT INTO stock_items (sku, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate) VALUES (?, ?, ?, ?, ?, ?, ?)") + ->execute([$sku, $name_en, $name_ar, $sale_price, $purchase_price, $qty, $vat_rate]); + } + $count++; + } + redirectWithMessage("Excel Import completed! $count items processed." . ($errors > 0 ? " ($errors rows skipped.)" : ""), "index.php?page=items"); + } else { + $message = "Error parsing Excel file: " . \Shuchkin\SimpleXLSX::parseError(); + } } else { // Check for BOM and skip it if (substr($firstBytes, 0, 3) === "\xEF\xBB\xBF") { @@ -1666,14 +1868,216 @@ function getPromotionalPrice($item) { $message = "Expense recorded!"; } + if (isset($_POST['import_customers'])) { + if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { + $tmpPath = $_FILES['excel_file']['tmp_name']; + $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + $count = 0; $errors = 0; + + if ($firstBytes === "PK\x03\x04") { + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + foreach ($xlsx->rows() as $i => $data) { + if ($i === 0) continue; + $name = trim($data[0] ?? ''); + if (!$name) { $errors++; continue; } + $email = trim($data[1] ?? ''); + $phone = trim($data[2] ?? ''); + $tax_id = trim($data[3] ?? ''); + $balance = (float)($data[4] ?? 0); + + db()->prepare("INSERT INTO customers (name, email, phone, tax_id, balance) VALUES (?, ?, ?, ?, ?)") + ->execute([$name, $email, $phone, $tax_id, $balance]); + $count++; + } + } + } else { + // CSV fallback + $handle = fopen($tmpPath, "r"); + fgetcsv($handle); // skip header + while (($data = fgetcsv($handle)) !== FALSE) { + $name = trim($data[0] ?? ''); + if (!$name) { $errors++; continue; } + $email = trim($data[1] ?? ''); + $phone = trim($data[2] ?? ''); + $tax_id = trim($data[3] ?? ''); + $balance = (float)($data[4] ?? 0); + db()->prepare("INSERT INTO customers (name, email, phone, tax_id, balance) VALUES (?, ?, ?, ?, ?)") + ->execute([$name, $email, $phone, $tax_id, $balance]); + $count++; + } + fclose($handle); + } + redirectWithMessage("Customers imported! $count processed." . ($errors ? " ($errors skipped)" : ""), "index.php?page=customers"); + } + } + + if (isset($_POST['import_suppliers'])) { + if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { + $tmpPath = $_FILES['excel_file']['tmp_name']; + $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + $count = 0; $errors = 0; + + if ($firstBytes === "PK\x03\x04") { + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + foreach ($xlsx->rows() as $i => $data) { + if ($i === 0) continue; + $name = trim($data[0] ?? ''); + if (!$name) { $errors++; continue; } + $email = trim($data[1] ?? ''); + $phone = trim($data[2] ?? ''); + $tax_id = trim($data[3] ?? ''); + $balance = (float)($data[4] ?? 0); + + db()->prepare("INSERT INTO suppliers (name, email, phone, tax_id, balance) VALUES (?, ?, ?, ?, ?)") + ->execute([$name, $email, $phone, $tax_id, $balance]); + $count++; + } + } + } else { + // CSV fallback + $handle = fopen($tmpPath, "r"); + fgetcsv($handle); // skip header + while (($data = fgetcsv($handle)) !== FALSE) { + $name = trim($data[0] ?? ''); + if (!$name) { $errors++; continue; } + $email = trim($data[1] ?? ''); + $phone = trim($data[2] ?? ''); + $tax_id = trim($data[3] ?? ''); + $balance = (float)($data[4] ?? 0); + db()->prepare("INSERT INTO suppliers (name, email, phone, tax_id, balance) VALUES (?, ?, ?, ?, ?)") + ->execute([$name, $email, $phone, $tax_id, $balance]); + $count++; + } + fclose($handle); + } + redirectWithMessage("Suppliers imported! $count processed." . ($errors ? " ($errors skipped)" : ""), "index.php?page=suppliers"); + } + } + + if (isset($_POST['import_categories'])) { + if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { + $tmpPath = $_FILES['excel_file']['tmp_name']; + $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + $count = 0; $errors = 0; + + if ($firstBytes === "PK\x03\x04") { + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + foreach ($xlsx->rows() as $i => $data) { + if ($i === 0) continue; + $name_en = trim($data[0] ?? ''); + if (!$name_en) { $errors++; continue; } + $name_ar = trim($data[1] ?? ''); + + db()->prepare("INSERT INTO stock_categories (name_en, name_ar) VALUES (?, ?)") + ->execute([$name_en, $name_ar]); + $count++; + } + } + } else { + $handle = fopen($tmpPath, "r"); + fgetcsv($handle); // skip header + while (($data = fgetcsv($handle)) !== FALSE) { + $name_en = trim($data[0] ?? ''); + if (!$name_en) { $errors++; continue; } + $name_ar = trim($data[1] ?? ''); + db()->prepare("INSERT INTO stock_categories (name_en, name_ar) VALUES (?, ?)") + ->execute([$name_en, $name_ar]); + $count++; + } + fclose($handle); + } + redirectWithMessage("Categories imported! $count processed.", "index.php?page=stock_categories"); + } + } + + if (isset($_POST['import_units'])) { + if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { + $tmpPath = $_FILES['excel_file']['tmp_name']; + $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + $count = 0; $errors = 0; + + if ($firstBytes === "PK\x03\x04") { + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + foreach ($xlsx->rows() as $i => $data) { + if ($i === 0) continue; + $name_en = trim($data[0] ?? ''); + if (!$name_en) { $errors++; continue; } + $name_ar = trim($data[1] ?? ''); + $short_name_en = trim($data[2] ?? ''); + $short_name_ar = trim($data[3] ?? ''); + + db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_name_en, short_name_ar) VALUES (?, ?, ?, ?)") + ->execute([$name_en, $name_ar, $short_name_en, $short_name_ar]); + $count++; + } + } + } else { + $handle = fopen($tmpPath, "r"); + fgetcsv($handle); + while (($data = fgetcsv($handle)) !== FALSE) { + $name_en = trim($data[0] ?? ''); + if (!$name_en) { $errors++; continue; } + $name_ar = trim($data[1] ?? ''); + $short_name_en = trim($data[2] ?? ''); + $short_name_ar = trim($data[3] ?? ''); + db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_name_en, short_name_ar) VALUES (?, ?, ?, ?)") + ->execute([$name_en, $name_ar, $short_name_en, $short_name_ar]); + $count++; + } + fclose($handle); + } + redirectWithMessage("Units imported! $count processed.", "index.php?page=stock_units"); + } + } + if (isset($_POST['import_items'])) { error_log("Import items triggered. POST: " . print_r($_POST, true)); if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) { $tmpPath = $_FILES['excel_file']['tmp_name']; error_log("File uploaded to: $tmpPath"); $firstBytes = file_get_contents($tmpPath, false, null, 0, 4); + if ($firstBytes === "PK\x03\x04") { - $message = "Error: It looks like you uploaded an Excel (.xlsx) file. Please save it as CSV (UTF-8) and try again."; + // EXCEL IMPORT VIA SimpleXLSX + require_once __DIR__ . '/includes/SimpleXLSX.php'; + if ($xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath)) { + $rows = $xlsx->rows(); + $rowNum = 0; + $count = 0; + $errors = 0; + foreach ($rows as $data) { + $rowNum++; + if ($rowNum === 1) continue; // Skip header + + $sku = trim($data[0] ?? ''); + $name_en = trim($data[1] ?? ''); + $name_ar = trim($data[2] ?? ''); + $sale_price = (float)($data[3] ?? 0); + $purchase_price = (float)($data[4] ?? 0); + $qty = (float)($data[5] ?? 0); + $vat_rate = (float)($data[6] ?? 0); + + if (!$sku && !$name_en) { $errors++; continue; } + + $check = db()->prepare("SELECT id FROM stock_items WHERE sku = ?"); + $check->execute([$sku]); + if ($check->fetch()) { + db()->prepare("UPDATE stock_items SET name_en = ?, name_ar = ?, sale_price = ?, purchase_price = ?, stock_quantity = ?, vat_rate = ? WHERE sku = ?") + ->execute([$name_en, $name_ar, $sale_price, $purchase_price, $qty, $vat_rate, $sku]); + } else { + db()->prepare("INSERT INTO stock_items (sku, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate) VALUES (?, ?, ?, ?, ?, ?, ?)") + ->execute([$sku, $name_en, $name_ar, $sale_price, $purchase_price, $qty, $vat_rate]); + } + $count++; + } + redirectWithMessage("Excel Import completed! $count items processed." . ($errors > 0 ? " ($errors rows skipped.)" : ""), "index.php?page=items"); + } else { + $message = "Error parsing Excel file: " . \Shuchkin\SimpleXLSX::parseError(); + } } else { // Check for BOM and skip it if (substr($firstBytes, 0, 3) === "\xEF\xBB\xBF") { @@ -2254,7 +2658,7 @@ if (isset($_POST['add_hr_department'])) { $group_id = (int)($_POST['group_id'] ?? 0) ?: null; if ($username && $password) { $hashed_password = password_hash($password, PASSWORD_DEFAULT); - $stmt = db()->prepare("INSERT INTO users (outlet_id, username, password, email, phone, group_id) VALUES (?, ?, ?, ?, ?, ?)"); + $stmt = db()->prepare("INSERT INTO users (username, password, email, phone, group_id) VALUES (?, ?, ?, ?, ?)"); try { $stmt->execute([$username, $hashed_password, $email, $phone, $group_id]); $message = "User added successfully!"; @@ -10844,8 +11248,8 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';