diff --git a/assets/css/custom.css b/assets/css/custom.css index ea58ee6..1b2dfec 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -79,8 +79,10 @@ body { z-index: 1000; } -[dir="ltr"] .sidebar { left: 0; } -[dir="rtl"] .sidebar { right: 0; } +@media (min-width: 1200px) { + [dir="ltr"] .sidebar { left: 0; } + [dir="rtl"] .sidebar { right: 0; } +} .sidebar-header { padding: 1rem; @@ -255,12 +257,13 @@ body { margin-right: var(--sidebar-width); } -@media (max-width: 991.98px) { - .sidebar { +@media (max-width: 1199.98px) { + [dir="ltr"] .sidebar { left: calc(-1 * var(--sidebar-width)); } - .sidebar.show { + [dir="ltr"] .sidebar.show { left: 0; + box-shadow: 0 0 50px rgba(0,0,0,0.3); } [dir="rtl"] .sidebar { right: calc(-1 * var(--sidebar-width)); @@ -268,11 +271,12 @@ body { } [dir="rtl"] .sidebar.show { right: 0; + box-shadow: 0 0 50px rgba(0,0,0,0.3); } .main-content { margin-left: 0 !important; margin-right: 0 !important; - width: 100%; + width: 100% !important; } } diff --git a/assets/js/main.js b/assets/js/main.js index 4773bb5..71b4bf4 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -31,6 +31,17 @@ document.addEventListener('DOMContentLoaded', function() { overlay.classList.remove('show'); }); + // Close sidebar when a link is clicked on mobile + const sidebarLinks = document.querySelectorAll('.sidebar .nav-link'); + sidebarLinks.forEach(link => { + link.addEventListener('click', function() { + if (window.innerWidth <= 1199.98) { + sidebar.classList.remove('show'); + overlay.classList.remove('show'); + } + }); + }); + // Sidebar Accordion logic: Close other collapses when one is opened const sidebarCollapses = document.querySelectorAll('.sidebar .collapse'); sidebarCollapses.forEach(collapseEl => { diff --git a/get_schema.php b/get_schema.php new file mode 100644 index 0000000..c9246d7 --- /dev/null +++ b/get_schema.php @@ -0,0 +1,10 @@ +query("SHOW TABLES")->fetchAll(PDO::FETCH_COLUMN); +foreach ($tables as $t) { + echo "TABLE: $t\n"; + $cols = db()->query("SHOW COLUMNS FROM $t")->fetchAll(PDO::FETCH_ASSOC); + foreach ($cols as $c) { + echo " - {$c['Field']} ({$c['Type']})\n"; + } +} diff --git a/includes/lang.php b/includes/lang.php index 9c93b55..b75b43d 100644 --- a/includes/lang.php +++ b/includes/lang.php @@ -34,6 +34,7 @@ $translations = [ 'cashflow_report' => 'Cashflow Report', 'expiry_report' => 'Expiry Report', 'low_stock_report' => 'Low Stock Report', + 'expense_report' => 'Expense Report', 'loyalty_history' => 'Loyalty History', 'management' => 'Management', 'payment_methods' => 'Payment Methods', @@ -128,6 +129,7 @@ $translations = [ 'cashflow_report' => 'تقرير التدفق النقدي', 'expiry_report' => 'تقرير الصلاحية', 'low_stock_report' => 'تقرير انخفاض المخزون', + 'expense_report' => 'تقرير المصاريف', 'loyalty_history' => 'سجل الولاء', 'management' => 'الإدارة', 'payment_methods' => 'طرق الدفع', diff --git a/index.php b/index.php index 8ba2269..be38c72 100644 --- a/index.php +++ b/index.php @@ -1871,15 +1871,28 @@ if (isset($_POST['add_hr_department'])) { } if (isset($_POST['save_backup_settings'])) { - if (can('users_view')) { $limit = (int)($_POST['backup_limit'] ?? 5); $auto = $_POST['backup_auto_enabled'] ?? '0'; $time = $_POST['backup_time'] ?? '00:00'; - $db = db(); $stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES ('backup_limit', ?), ('backup_auto_enabled', ?), ('backup_time', ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)"); $stmt->execute([$limit, $auto, $time]); - $message = "Backup settings saved successfully!"; + $message = "Backup settings updated."; + } + + if (isset($_GET['download_backup'])) { + $filename = basename($_GET['download_backup']); + $filepath = __DIR__ . '/backups/' . $filename; + if (file_exists($filepath)) { + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + header('Content-Length: ' . filesize($filepath)); + readfile($filepath); + exit; } } @@ -1983,6 +1996,7 @@ $page_permissions = [ 'accounting' => 'accounting_view', 'expense_categories' => 'expense_categories_view', 'expenses' => 'expenses_view', + 'expense_report' => 'expenses_view', 'items' => 'items_view', 'categories' => 'categories_view', 'units' => 'units_view', @@ -2085,6 +2099,7 @@ $permission_groups = [ 'Reports' => [ 'customer_statement' => 'Customer Statement', 'supplier_statement' => 'Supplier Statement', + 'expense_report' => 'Expense Report', 'cashflow_report' => 'Cashflow Report', 'expiry_report' => 'Expiry Report', 'low_stock_report' => 'Low Stock Report', @@ -2107,19 +2122,31 @@ $permission_groups = [ if ($page === 'export') { $type = $_GET['type'] ?? 'sales'; - $filename = $type . "_export_" . date('Y-m-d') . ".csv"; + $format = $_GET['format'] ?? 'csv'; + $filename = $type . "_export_" . date('Y-m-d') . ($format === 'excel' ? ".xls" : ".csv"); - header('Content-Type: text/csv; charset=utf-8'); - header('Content-Disposition: attachment; filename=' . $filename); - $output = fopen('php://output', 'w'); - - // Add UTF-8 BOM for Excel - fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF)); + if ($format === 'excel') { + header('Content-Type: application/vnd.ms-excel; charset=utf-8'); + header('Content-Disposition: attachment; filename=' . $filename); + echo ""; + } else { + header('Content-Type: text/csv; charset=utf-8'); + header('Content-Disposition: attachment; filename=' . $filename); + $output = fopen('php://output', 'w'); + // Add UTF-8 BOM for Excel + fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF)); + } + + $headers = []; + $rows = []; if ($type === 'sales' || $type === 'purchases') { - $invType = ($type === 'sales') ? 'sale' : 'purchase'; - $where = ["v.type = ?"]; - $params = [$invType]; + $table = ($type === 'sales') ? 'invoices' : 'purchases'; + $cust_table = ($type === 'sales') ? 'customers' : 'suppliers'; + $cust_col = ($type === 'sales') ? 'customer_id' : 'supplier_id'; + + $where = ["1=1"]; + $params = []; if (!empty($_GET['search'])) { $s = $_GET['search']; $clean_id = preg_replace('/[^0-9]/', '', $s); @@ -2134,16 +2161,17 @@ if ($page === 'export') { $params[] = "%$s%"; } } - if (!empty($_GET['customer_id'])) { $where[] = "v.customer_id = ?"; $params[] = $_GET['customer_id']; } + if (!empty($_GET['customer_id'])) { $where[] = "v.$cust_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); + $stmt = db()->prepare("SELECT v.id, c.name as customer_name, v.invoice_date, v.payment_type, v.status, v.total_with_vat, v.paid_amount, (v.total_with_vat - v.paid_amount) as balance - FROM invoices v LEFT JOIN customers c ON v.customer_id = c.id + FROM $table v LEFT JOIN $cust_table c ON v.$cust_col = c.id WHERE $whereSql ORDER BY v.id DESC"); $stmt->execute($params); - fputcsv($output, ['Invoice ID', 'Customer/Supplier', 'Date', 'Payment', 'Status', 'Total', 'Paid', 'Balance']); - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row); + $headers = ['Invoice ID', 'Customer/Supplier', 'Date', 'Payment', 'Status', 'Total', 'Paid', 'Balance']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; } elseif ($type === 'customers' || $type === 'suppliers') { $table = ($type === 'suppliers') ? 'suppliers' : 'customers'; $where = ["1=1"]; @@ -2154,8 +2182,8 @@ if ($page === 'export') { $whereSql = implode(" AND ", $where); $stmt = db()->prepare("SELECT id, name, email, phone, tax_id, balance, created_at FROM $table WHERE $whereSql ORDER BY id DESC"); $stmt->execute($params); - fputcsv($output, ['ID', 'Name', 'Email', 'Phone', 'Tax ID', 'Balance', 'Created At']); - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row); + $headers = ['ID', 'Name', 'Email', 'Phone', 'Tax ID', 'Balance', 'Created At']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; } elseif ($type === 'items') { $where = ["1=1"]; $params = []; @@ -2165,10 +2193,84 @@ if ($page === 'export') { FROM stock_items i LEFT JOIN stock_categories c ON i.category_id = c.id WHERE $whereSql ORDER BY i.id DESC"); $stmt->execute($params); - fputcsv($output, ['SKU', 'Name (EN)', 'Name (AR)', 'Category', 'Purchase Price', 'Sale Price', 'Quantity', 'VAT %']); - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) fputcsv($output, $row); + $headers = ['SKU', 'Name (EN)', 'Name (AR)', 'Category', 'Purchase Price', 'Sale Price', 'Quantity', 'VAT %']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'expenses') { + $where = ["1=1"]; + $params = []; + $stmt = db()->prepare("SELECT e.id, c.name_en as category, e.amount, e.expense_date, e.reference_no, e.description + FROM expenses e JOIN expense_categories c ON e.category_id = c.id + ORDER BY e.expense_date DESC"); + $stmt->execute(); + $headers = ['ID', 'Category', 'Amount', 'Date', 'Reference', 'Description']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'quotations') { + $stmt = db()->prepare("SELECT q.id, c.name as customer_name, q.quotation_date, q.total_with_vat, q.status + FROM quotations q JOIN customers c ON q.customer_id = c.id + ORDER BY q.id DESC"); + $stmt->execute(); + $headers = ['Quotation #', 'Customer', 'Date', 'Total', 'Status']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'lpos') { + $stmt = db()->prepare("SELECT q.id, s.name as supplier_name, q.lpo_date, q.total_with_vat, q.status + FROM lpos q JOIN suppliers s ON q.supplier_id = s.id + ORDER BY q.id DESC"); + $stmt->execute(); + $headers = ['LPO #', 'Supplier', 'Date', 'Total', 'Status']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'categories') { + $stmt = db()->query("SELECT id, name_en, name_ar FROM stock_categories ORDER BY id DESC"); + $headers = ['ID', 'Name (EN)', 'Name (AR)']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'units') { + $stmt = db()->query("SELECT id, name_en, name_ar, short_name_en, short_name_ar FROM stock_units ORDER BY id DESC"); + $headers = ['ID', 'Name (EN)', 'Name (AR)', 'Short (EN)', 'Short (AR)']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'sales_returns') { + $stmt = db()->query("SELECT sr.id, sr.invoice_id, c.name as customer, sr.return_date, sr.total_amount FROM sales_returns sr LEFT JOIN customers c ON sr.customer_id = c.id ORDER BY sr.id DESC"); + $headers = ['Return ID', 'Invoice ID', 'Customer', 'Date', 'Amount']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'purchase_returns') { + $stmt = db()->query("SELECT pr.id, pr.purchase_id, s.name as supplier, pr.return_date, pr.total_amount FROM purchase_returns pr LEFT JOIN suppliers s ON pr.supplier_id = s.id ORDER BY pr.id DESC"); + $headers = ['Return ID', 'Purchase ID', 'Supplier', 'Date', 'Amount']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'hr_employees') { + $stmt = db()->query("SELECT e.id, e.name, d.name as department, e.position, e.salary, e.status FROM hr_employees e LEFT JOIN hr_departments d ON e.department_id = d.id ORDER BY e.id DESC"); + $headers = ['ID', 'Name', 'Department', 'Position', 'Salary', 'Status']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'hr_departments') { + $stmt = db()->query("SELECT id, name FROM hr_departments ORDER BY id DESC"); + $headers = ['ID', 'Name']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'hr_attendance') { + $stmt = db()->query("SELECT a.attendance_date, e.name, a.status, a.clock_in, a.clock_out FROM hr_attendance a JOIN hr_employees e ON a.employee_id = e.id ORDER BY a.attendance_date DESC, e.name ASC"); + $headers = ['Date', 'Employee', 'Status', 'In', 'Out']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'hr_payroll') { + $stmt = db()->query("SELECT p.payroll_month, p.payroll_year, e.name, p.basic_salary, p.bonus, p.deductions, p.net_salary, p.status FROM hr_payroll p JOIN hr_employees e ON p.employee_id = e.id ORDER BY p.payroll_year DESC, p.payroll_month DESC"); + $headers = ['Month', 'Year', 'Employee', 'Salary', 'Bonus', 'Deductions', 'Net', 'Status']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } elseif ($type === 'users') { + $stmt = db()->query("SELECT u.id, u.username, u.email, g.name as role, u.status FROM users u LEFT JOIN role_groups g ON u.group_id = g.id ORDER BY u.id DESC"); + $headers = ['ID', 'Username', 'Email', 'Role', 'Status']; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row; + } + + if ($format === 'excel') { + echo ""; + foreach ($headers as $h) echo ""; + echo ""; + foreach ($rows as $row) { + echo ""; + foreach ($row as $val) echo ""; + echo ""; + } + echo "
" . htmlspecialchars($h) . "
" . htmlspecialchars((string)$val) . "
"; + } else { + fputcsv($output, $headers); + foreach ($rows as $row) fputcsv($output, $row); + fclose($output); } - fclose($output); exit; } @@ -2396,6 +2498,15 @@ switch ($page) { $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); @@ -2609,19 +2720,30 @@ switch ($page) { 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 e.expense_date BETWEEN ? AND ? + $where GROUP BY c.id ORDER BY total DESC"); - $stmt->execute([$start_date, $end_date]); + $stmt->execute($params); $data['report_by_category'] = $stmt->fetchAll(); - $stmt = db()->prepare("SELECT SUM(amount) FROM expenses WHERE expense_date BETWEEN ? AND ?"); - $stmt->execute([$start_date, $end_date]); + $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"]; @@ -2830,9 +2952,19 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; @@ -3029,16 +3161,18 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; -