diff --git a/index.php.bak b/index.php.bak
new file mode 100644
index 0000000..b15ee60
--- /dev/null
+++ b/index.php.bak
@@ -0,0 +1,14185 @@
+ 0,
+ 'path' => '/',
+ 'secure' => true,
+ 'httponly' => true,
+ 'samesite' => 'None',
+ ]);
+}
+
+session_start();
+if (isset($_GET['action']) && $_GET['action'] === 'download_items_template') {
+ header('Content-Type: text/csv; charset=utf-8');
+ header('Content-Disposition: attachment; filename=items_import_template.csv');
+ $output = fopen('php://output', 'w');
+ // Add BOM for Excel UTF-8 compatibility
+ fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
+ fputcsv($output, ['SKU', 'English Name', 'Arabic Name', 'Sale Price', 'Cost Price']);
+ fclose($output);
+ exit;
+}
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ file_put_contents('post_debug.log', date('Y-m-d H:i:s') . " - POST: " . json_encode($_POST) . "\n", FILE_APPEND);
+}
+require_once 'db/config.php';
+require_once 'includes/DatabaseInstaller.php';
+
+// Auto-install database if not installed
+if (!DatabaseInstaller::isInstalled()) {
+ try {
+ DatabaseInstaller::install();
+ } catch (Exception $e) {
+ die("Installation Error: " . $e->getMessage());
+ }
+}
+
+require_once 'lib/LicenseService.php';
+require_once 'includes/lang.php';
+
+// Language Setup
+if (isset($_GET['lang'])) {
+ $_SESSION['lang'] = in_array($_GET['lang'], ['en', 'ar']) ? $_GET['lang'] : 'ar';
+}
+if (!isset($_SESSION['lang'])) {
+ $_SESSION['lang'] = 'ar'; // Default to Arabic as requested
+}
+$lang = $_SESSION['lang'];
+$dir = ($lang === 'ar') ? 'rtl' : 'ltr';
+
+// Licensing Middleware
+try {
+ $is_activated = LicenseService::isActivated();
+ $trial_days = LicenseService::getTrialRemainingDays();
+ $can_access = LicenseService::canAccess();
+} catch (PDOException $e) {
+ die("Database Connection Error: " . $e->getMessage() . "
Please check your
db/config.php settings.");
+} catch (Exception $e) {
+ die("Application Error: " . $e->getMessage());
+}
+$page = $_GET['page'] ?? 'dashboard';
+
+if (!$can_access && $page !== 'activate') {
+ header("Location: index.php?page=activate");
+ exit;
+}
+
+// Activation Page UI (accessible without login)
+if ($page === 'activate') {
+ $error = '';
+ $success = '';
+ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['activate'])) {
+ $res = LicenseService::activate($_POST['license_key'] ?? '');
+ if ($res['success']) {
+ $success = "System activated successfully! Redirecting...";
+ header("refresh:2;url=index.php");
+ } else {
+ $error = $res['error'];
+ }
+ }
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['start_trial'])) {
+ // We need a way to call startTrial() which is private in LicenseService
+ // I'll make it public or use a public wrapper
+ $res = LicenseService::initTrial();
+ if ($res['success']) {
+ $success = "Trial period started! Redirecting...";
+ header("refresh:2;url=index.php");
+ } else {
+ $error = $res['error'];
+ }
+ }
+ ?>
+
+
+
+
+
+
= __('activate_product') ?>
+
+
+
+
+
+
+
= __('activate_product') ?>
+
+
+ = $lang === 'ar' ? 'العربية' : 'English' ?>
+
+
+
+
+
= $lang === 'ar' ? 'يرجى إدخال مفتاح التسلسل للمتابعة.' : 'Please enter your serial key to continue using the application.' ?>
+
+
+
= $error ?>
+
+
+
= $success ?>
+
+
+
+
+
+
+
+
= $lang === 'ar' ? 'أو ابدأ الفترة التجريبية (15 يوم)' : 'Or start your 15-day trial period' ?>
+
+
+
+
+
+
+
+ query("SELECT p.id, p.due_date, p.total_with_vat, s.name as supplier_name
+ FROM purchases p
+ LEFT JOIN suppliers s ON p.supplier_id = s.id
+ WHERE p.status != 'paid'
+ AND p.due_date IS NOT NULL
+ AND p.due_date <= DATE_ADD(CURDATE(), INTERVAL 7 DAY)
+ ORDER BY p.due_date ASC");
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+// Missing helper functions
+function getLoyaltyMultiplier($tier) {
+ switch (strtolower((string)$tier)) {
+ case 'gold': return 2.0;
+ case 'silver': return 1.5;
+ default: return 1.0;
+ }
+}
+
+function numberToWords($num) {
+ $num = (int)$num;
+ if ($num == 0) return "zero";
+ $ones = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"];
+ $tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
+ if ($num < 20) return $ones[$num];
+ if ($num < 100) return $tens[(int)($num / 10)] . ($num % 10 ? "-" . $ones[$num % 10] : "");
+ if ($num < 1000) return $ones[(int)($num / 100)] . " hundred" . ($num % 100 ? " and " . numberToWords($num % 100) : "");
+ if ($num < 1000000) return numberToWords((int)($num / 1000)) . " thousand" . ($num % 1000 ? " " . numberToWords($num % 1000) : "");
+ return (string)$num;
+}
+
+function numberToWordsArabic($num) {
+ $num = (int)$num;
+ if ($num == 0) return "صفر";
+ $ones = ["", "واحد", "اثنان", "ثلاثة", "أربعة", "خمسة", "ستة", "سبعة", "ثمانية", "تسعة", "عشرة", "أحد عشر", "اثنا عشر", "ثلاثة عشر", "أربعة عشر", "خمسة عشر", "ستة عشر", "سبعة عشر", "ثمانية عشر", "تسعة عشر"];
+ $tens = ["", "", "عشرون", "ثلاثون", "أربعون", "خمسون", "ستون", "سبعون", "ثمانون", "تسعون"];
+ $hundreds = ["", "مائة", "مائتان", "ثلاثمائة", "أربعمائة", "خمسمائة", "ستمائة", "سبعمائة", "ثمانمائة", "تسعمائة"];
+
+ if ($num < 20) return $ones[$num];
+ if ($num < 100) return ($num % 10 ? $ones[$num % 10] . " و " : "") . $tens[(int)($num / 10)];
+ if ($num < 1000) return $hundreds[(int)($num / 100)] . ($num % 100 ? " و " . numberToWordsArabic($num % 100) : "");
+ if ($num < 1000000) {
+ $thousands = (int)($num / 1000);
+ $rem = $num % 1000;
+ $tStr = "ألف";
+ if ($thousands == 1) $tStr = "ألف";
+ else if ($thousands == 2) $tStr = "ألفين";
+ else if ($thousands >= 3 && $thousands <= 10) $tStr = numberToWordsArabic($thousands) . " آلاف";
+ else $tStr = numberToWordsArabic($thousands) . " ألف";
+
+ return $tStr . ($rem ? " و " . numberToWordsArabic($rem) : "");
+ }
+ return (string)$num;
+}
+
+function renderPagination($currentPage, $totalPages) {
+ $query = $_GET;
+ unset($query['p']);
+ $url = 'index.php?' . http_build_query($query) . '&p=';
+
+ $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
+ $limitHtml = "
+
+ Rows per page:
+
+ 5, 'p' => 1])) . "' " . ($limit == 5 ? 'selected' : '') . ">5
+ 20, 'p' => 1])) . "' " . ($limit == 20 ? 'selected' : '') . ">20
+ 40, 'p' => 1])) . "' " . ($limit == 40 ? 'selected' : '') . ">40
+ 100, 'p' => 1])) . "' " . ($limit == 100 ? 'selected' : '') . ">100
+ 200, 'p' => 1])) . "' " . ($limit == 200 ? 'selected' : '') . ">200
+ 500, 'p' => 1])) . "' " . ($limit == 500 ? 'selected' : '') . ">500
+
+
+
+ ";
+
+ if ($totalPages <= 1) return $limitHtml;
+
+ $html = '
';
+ return $html;
+}
+
+// Login Logic
+$login_error = '';
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
+ $user = $_POST['username'] ?? '';
+ $pass = $_POST['password'] ?? '';
+ $stmt = db()->prepare("SELECT u.*, g.name as role_name FROM users u LEFT JOIN role_groups g ON u.group_id = g.id WHERE u.username = ? AND u.status = 'active'");
+ $stmt->execute([$user]);
+ $u = $stmt->fetch();
+ if ($u && password_verify($pass, $u['password'])) {
+ $_SESSION['user_id'] = $u['id'];
+ $_SESSION['username'] = $u['username'];
+ $_SESSION['user_role_name'] = $u['role_name'];
+
+ // Fetch permissions from the new role_permissions table
+ $permStmt = db()->prepare("SELECT permission FROM role_permissions WHERE role_id = ?");
+ $permStmt->execute([$u['group_id']]);
+ $permissions = $permStmt->fetchAll(PDO::FETCH_COLUMN);
+ $_SESSION['user_permissions'] = $permissions;
+
+ $_SESSION['profile_pic'] = $u['profile_pic'];
+ $_SESSION['theme'] = $u['theme'] ?? 'default';
+ header("Location: index.php");
+ exit;
+ } else {
+ $login_error = "Invalid username or password";
+ // Debugging
+ $reason = (!$u) ? "User not found or inactive" : "Password mismatch";
+ file_put_contents('login_debug.log', date('Y-m-d H:i:s') . " - Failed login for '$user'. Reason: $reason\n", FILE_APPEND);
+ }
+}
+
+// Logout
+if (isset($_GET['action']) && $_GET['action'] === 'logout') {
+ session_destroy();
+ header("Location: index.php");
+ exit;
+}
+
+// --- POS AJAX Handlers ---
+if (isset($_GET['action']) || isset($_POST['action'])) {
+ $action = $_GET['action'] ?? $_POST['action'] ?? '';
+
+ if ($action === 'validate_discount') {
+ header('Content-Type: application/json');
+ $code = $_GET['code'] ?? '';
+ $stmt = db()->prepare("SELECT * FROM discount_codes WHERE code = ? AND status = 'active' AND (expiry_date IS NULL OR expiry_date >= CURDATE())");
+ $stmt->execute([$code]);
+ $discount = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($discount) {
+ echo json_encode(['success' => true, 'discount' => $discount]);
+ } else {
+ echo json_encode(['success' => false, 'error' => 'Invalid or expired discount code']);
+ }
+ exit;
+ }
+
+ if ($action === 'search_items') {
+ file_put_contents('search_debug.log', date('Y-m-d H:i:s') . " - search_items call: q=" . ($_GET['q'] ?? '') . "\n", FILE_APPEND);
+ header('Content-Type: application/json');
+ $q = $_GET['q'] ?? '';
+ if (strlen($q) < 1) {
+ echo json_encode([]);
+ exit;
+ }
+ $searchTerm = "%$q%";
+ $stmt = db()->prepare("SELECT * FROM stock_items WHERE name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ? LIMIT 15");
+ $stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
+ echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
+ exit;
+ }
+
+ if ($action === 'get_payments') {
+ header('Content-Type: application/json');
+ $invoice_id = (int)$_GET['invoice_id'];
+ $stmt = db()->prepare("SELECT * FROM payments WHERE invoice_id = ? ORDER BY payment_date DESC");
+ $stmt->execute([$invoice_id]);
+ echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
+ exit;
+ }
+
+ if ($action === 'get_payment_details') {
+ header('Content-Type: application/json');
+ $payment_id = (int)$_GET['payment_id'];
+ $stmt = db()->prepare("SELECT p.*, i.customer_id, c.name as customer_name
+ FROM payments p
+ JOIN invoices i ON p.invoice_id = i.id
+ JOIN customers c ON i.customer_id = c.id
+ WHERE p.id = ?");
+ $stmt->execute([$payment_id]);
+ echo json_encode($stmt->fetch(PDO::FETCH_ASSOC));
+ exit;
+ }
+
+ if ($action === 'get_held_carts') {
+ header('Content-Type: application/json');
+ $stmt = db()->query("SELECT h.*, c.name as customer_name FROM pos_held_carts h LEFT JOIN customers c ON h.customer_id = c.id ORDER BY h.created_at DESC");
+ echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
+ exit;
+ }
+
+ if ($action === 'hold_pos_cart') {
+ header('Content-Type: application/json');
+ $name = $_POST['cart_name'] ?? 'Untitled Cart';
+ $items = $_POST['items'] ?? '[]';
+ $customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
+ $stmt = db()->prepare("INSERT INTO pos_held_carts (cart_name, items_json, customer_id) VALUES (?, ?, ?)");
+ $stmt->execute([$name, $items, $customer_id]);
+ echo json_encode(['success' => true]);
+ exit;
+ }
+
+ if ($action === 'delete_held_cart') {
+ header('Content-Type: application/json');
+ $id = (int)$_POST['id'];
+ $stmt = db()->prepare("DELETE FROM pos_held_carts WHERE id = ?");
+ $stmt->execute([$id]);
+ echo json_encode(['success' => true]);
+ exit;
+ }
+
+ if ($action === 'save_pos_transaction') {
+ header('Content-Type: application/json');
+ $db = db();
+ try {
+ $db->beginTransaction();
+
+ $customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
+ $payments = json_decode($_POST['payments'] ?? '[]', true);
+ $items = json_decode($_POST['items'] ?? '[]', true);
+ $total_amount = (float)($_POST['total_amount'] ?? 0);
+ $tax_amount = (float)($_POST['tax_amount'] ?? 0);
+ $discount_code_id = !empty($_POST['discount_code_id']) ? (int)$_POST['discount_code_id'] : null;
+ $discount_amount = (float)($_POST['discount_amount'] ?? 0);
+ $loyalty_redeemed = (float)($_POST['loyalty_redeemed'] ?? 0);
+ $net_amount = $total_amount - $discount_amount - $loyalty_redeemed;
+
+ $transaction_no = 'POS-' . time() . rand(10, 99);
+ $session_id = $_SESSION['register_session_id'] ?? null;
+
+ if (!$session_id) {
+ // Fallback: try to find an open session for this user
+ $check_session = $db->prepare("SELECT id FROM register_sessions WHERE user_id = ? AND status = 'open' LIMIT 1");
+ $check_session->execute([$_SESSION['user_id']]);
+ $session_id = $check_session->fetchColumn() ?: null;
+ if ($session_id) {
+ $_SESSION['register_session_id'] = $session_id;
+ }
+ }
+
+ // Insert into unified Invoice table
+ $items_for_journal = [];
+ foreach ($items as $item) {
+ $items_for_journal[] = ['id' => $item['id'], 'qty' => $item['qty']];
+ }
+
+ $stmt = $db->prepare("INSERT INTO invoices (transaction_no, customer_id, invoice_date, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, status, register_session_id, is_pos, discount_amount, loyalty_points_redeemed, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, 1, ?, ?, ?)");
+ $stmt->execute([$transaction_no, $customer_id, date('Y-m-d'), 'pos', $total_amount, $tax_amount, $net_amount, $net_amount, $session_id, $discount_amount, $loyalty_redeemed, $_SESSION['user_id']]);
+ $transaction_id = (int)$db->lastInsertId();
+
+ // Insert Items & Update Stock
+ $stmtItem = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)");
+ $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
+
+ foreach ($items as $item) {
+ $sub = (float)$item['price'] * (float)$item['qty'];
+ $va = (float)($item['vat_amount'] ?? 0);
+ $stmtItem->execute([$transaction_id, $item['id'], $item['qty'], $item['price'], $va, $sub]);
+ $stmtStock->execute([$item['qty'], $item['id']]);
+ }
+
+ // Insert Payments
+ require_once 'includes/accounting_helper.php';
+ $stmtPay = $db->prepare("INSERT INTO payments (invoice_id, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)");
+ foreach ($payments as $p) {
+ $stmtPay->execute([$transaction_id, $p['amount'], date('Y-m-d'), $p['method'], 'POS Transaction']);
+ $payment_id = $db->lastInsertId();
+ recordPaymentReceivedJournal((int)$payment_id, $p['amount'], date('Y-m-d'), $p['method']);
+ }
+
+ // Update Loyalty Points if customer exists
+ if ($customer_id) {
+ // Earn points
+ $points_earned = floor($net_amount);
+ $stmtPoints = $db->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? + ? WHERE id = ?");
+ $stmtPoints->execute([$loyalty_redeemed * 100, $points_earned, $customer_id]);
+
+ // Record transactions
+ if ($points_earned > 0) {
+ $db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'earned', ?)")
+ ->execute([$customer_id, $transaction_id, $points_earned, "Earned from POS order #$transaction_no"]);
+ }
+ if ($loyalty_redeemed > 0) {
+ $db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'redeemed', ?)")
+ ->execute([$customer_id, $transaction_id, -$loyalty_redeemed * 100, "Redeemed for POS order #$transaction_no"]);
+ }
+
+ // Update invoice with points earned
+ $db->prepare("UPDATE invoices SET loyalty_points_earned = ? WHERE id = ?")->execute([$points_earned, $transaction_id]);
+ }
+
+ // Record Sale Journal for POS
+ recordSaleJournal($transaction_id, $net_amount, date('Y-m-d'), $items_for_journal, $tax_amount);
+
+ $db->commit();
+ echo json_encode(['success' => true, 'invoice_id' => $transaction_id, 'transaction_no' => $transaction_no]);
+ } catch (Exception $e) {
+ $db->rollBack();
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+ }
+ exit;
+ }
+
+ if ($action === 'save_theme') {
+ header('Content-Type: application/json');
+ if (!isset($_SESSION['user_id'])) {
+ echo json_encode(['success' => false, 'error' => 'Not authenticated']);
+ exit;
+ }
+ $theme = $_POST['theme'] ?? 'default';
+ $allowed = ['default', 'dark', 'ocean', 'forest', 'sunset', 'nord', 'dracula', 'citrus'];
+ if (!in_array($theme, $allowed)) $theme = 'default';
+
+ $stmt = db()->prepare("UPDATE users SET theme = ? WHERE id = ?");
+ $stmt->execute([$theme, $_SESSION['user_id']]);
+ $_SESSION['theme'] = $theme;
+ echo json_encode(['success' => true]);
+ exit;
+ }
+
+ if ($action === 'get_invoice_items') {
+ header('Content-Type: application/json');
+ $invoice_id = (int)$_GET['invoice_id'];
+ $type = $_GET['type'] ?? 'sale';
+
+ if ($type === 'purchase') {
+ $stmt = db()->prepare("SELECT pi.*, i.name_en, i.name_ar, i.sku
+ FROM purchase_items pi
+ JOIN stock_items i ON pi.item_id = i.id
+ WHERE pi.purchase_id = ?");
+ } else {
+ $stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku
+ FROM invoice_items ii
+ JOIN stock_items i ON ii.item_id = i.id
+ WHERE ii.invoice_id = ?");
+ }
+ $stmt->execute([$invoice_id]);
+ echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
+ exit;
+ }
+
+ if ($action === 'get_return_details') {
+ header('Content-Type: application/json');
+ $return_id = (int)$_GET['return_id'];
+ $type = $_GET['type'] ?? 'sale';
+
+ if ($type === 'purchase') {
+ $stmt = db()->prepare("SELECT pr.*, c.name as party_name FROM purchase_returns pr LEFT JOIN suppliers c ON pr.supplier_id = c.id WHERE pr.id = ?");
+ $stmt->execute([$return_id]);
+ $return = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($return) {
+ $stmtItems = db()->prepare("SELECT pri.*, i.name_en, i.name_ar, i.sku FROM purchase_return_items pri JOIN stock_items i ON pri.item_id = i.id WHERE pri.return_id = ?");
+ $stmtItems->execute([$return_id]);
+ $return['items'] = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
+ }
+ } else {
+ $stmt = db()->prepare("SELECT sr.*, c.name as party_name FROM sales_returns sr LEFT JOIN customers c ON sr.customer_id = c.id WHERE sr.id = ?");
+ $stmt->execute([$return_id]);
+ $return = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($return) {
+ $stmtItems = db()->prepare("SELECT sri.*, i.name_en, i.name_ar, i.sku FROM sales_return_items sri JOIN stock_items i ON sri.item_id = i.id WHERE sri.return_id = ?");
+ $stmtItems->execute([$return_id]);
+ $return['items'] = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
+ }
+ }
+ echo json_encode($return);
+ exit;
+ }
+
+ if ($action === 'translate') {
+ header('Content-Type: application/json');
+ require_once __DIR__ . '/ai/LocalAIApi.php';
+ $text = $_POST['text'] ?? '';
+ $target = $_POST['target'] ?? 'ar';
+
+ if (empty($text)) {
+ echo json_encode(['success' => false, 'error' => 'No text provided']);
+ exit;
+ }
+
+ $prompt = "Translate the following product name to " . ($target === 'ar' ? 'Arabic' : 'English') . ". Return ONLY the translation, no extra text or explanations.\n\nText: " . $text;
+
+ $resp = LocalAIApi::createResponse([
+ 'input' => [
+ ['role' => 'system', 'content' => 'You are a translation assistant. You translate product names between English and Arabic.'],
+ ['role' => 'user', 'content' => $prompt],
+ ],
+ ]);
+
+ if (!empty($resp['success'])) {
+ $translated = trim(LocalAIApi::extractText($resp));
+ echo json_encode(['success' => true, 'translated' => $translated]);
+ } else {
+ error_log("Translation failed for text '$text': " . json_encode($resp));
+ echo json_encode(['success' => false, 'error' => $resp['error'] ?? 'Translation failed']);
+ }
+ exit;
+ }
+}
+
+// Redirect to login if not authenticated
+if (!isset($_SESSION['user_id'])) {
+ ?>
+
+
+
+
+
+
= __('sign_in') ?> - Admin Panel
+
+
+
+
+
+
+
+
+
+
+
+
= __('welcome_back') ?>
+
= $lang === 'ar' ? 'يرجى إدخال تفاصيلك لتسجيل الدخول' : 'Please enter your details to sign in' ?>
+
+
+
= $login_error ?>
+
+
+
+
+
+
+ prepare("SELECT theme FROM users WHERE id = ?");
+ $stmt->execute([$_SESSION['user_id']]);
+ $_SESSION['theme'] = $stmt->fetchColumn() ?: 'default';
+}
+
+function numberToWordsOMR($number) {
+ $number = number_format((float)$number, 3, '.', '');
+ list($rials, $baisas) = explode('.', $number);
+
+ $rialsWordsEn = numberToWords((int)$rials);
+ $baisasWordsEn = numberToWords((int)$baisas);
+
+ $enResult = $rialsWordsEn . " Omani Rials";
+ if ((int)$baisas > 0) {
+ $enResult .= " and " . $baisasWordsEn . " Baisas";
+ }
+ $enResult .= " Only";
+
+ $rialsWordsAr = numberToWordsArabic((int)$rials);
+ $baisasWordsAr = numberToWordsArabic((int)$baisas);
+
+ $arResult = $rialsWordsAr . " ريال عماني";
+ if ((int)$baisas > 0) {
+ $arResult .= " و " . $baisasWordsAr . " بيسة";
+ }
+ $arResult .= " فقط";
+
+ return $enResult . " / " . $arResult;
+}
+
+function getPromotionalPrice($item) {
+ $price = (float)$item['sale_price'];
+ if (isset($item['is_promotion']) && $item['is_promotion']) {
+ $today = date('Y-m-d');
+ $start = !empty($item['promotion_start']) ? $item['promotion_start'] : null;
+ $end = !empty($item['promotion_end']) ? $item['promotion_end'] : null;
+
+ $active = true;
+ if ($start && $today < $start) $active = false;
+ if ($end && $today > $end) $active = false;
+
+ if ($active) {
+ $price = $price * (1 - (float)$item['promotion_percent'] / 100);
+ }
+ }
+ return $price;
+}
+
+// --- Inventory & Core Handlers ---
+ if (isset($_POST['add_item'])) {
+ $name_en = $_POST['name_en'] ?? '';
+ $name_ar = $_POST['name_ar'] ?? '';
+ $category_id = (int)$_POST['category_id'] ?: null;
+ $unit_id = (int)$_POST['unit_id'] ?: null;
+ $supplier_id = (int)$_POST['supplier_id'] ?: null;
+ $sku = $_POST['sku'] ?? '';
+ $sale_price = (float)($_POST['sale_price'] ?? 0);
+ $purchase_price = (float)($_POST['purchase_price'] ?? 0);
+ $stock_quantity = (float)($_POST['stock_quantity'] ?? 0);
+ $min_stock_level = (float)($_POST['min_stock_level'] ?? 0);
+ $vat_rate = (float)($_POST['vat_rate'] ?? 0);
+ $expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null;
+ $is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
+ $promotion_start = !empty($_POST['promotion_start']) ? $_POST['promotion_start'] : null;
+ $promotion_end = !empty($_POST['promotion_end']) ? $_POST['promotion_end'] : null;
+ $promotion_percent = (float)($_POST['promotion_percent'] ?? 0);
+
+ $image_path = null;
+ if (isset($_FILES['image']) && $_FILES['image']['error'] === 0) {
+ $ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
+ $filename = 'uploads/item_' . time() . '.' . $ext;
+ if (!is_dir('uploads')) mkdir('uploads', 0777, true);
+ if (move_uploaded_file($_FILES['image']['tmp_name'], $filename)) $image_path = $filename;
+ }
+ $stmt = db()->prepare("INSERT INTO stock_items (name_en, name_ar, category_id, unit_id, supplier_id, sku, sale_price, purchase_price, stock_quantity, min_stock_level, image_path, vat_rate, expiry_date, is_promotion, promotion_start, promotion_end, promotion_percent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$name_en, $name_ar, $category_id, $unit_id, $supplier_id, $sku, $sale_price, $purchase_price, $stock_quantity, $min_stock_level, $image_path, $vat_rate, $expiry_date, $is_promotion, $promotion_start, $promotion_end, $promotion_percent]);
+ redirectWithMessage("Item added successfully!");
+ }
+
+ if (isset($_POST['edit_item'])) {
+ $id = (int)$_POST['id'];
+ $name_en = $_POST['name_en'] ?? '';
+ $name_ar = $_POST['name_ar'] ?? '';
+ $category_id = (int)$_POST['category_id'] ?: null;
+ $unit_id = (int)$_POST['unit_id'] ?: null;
+ $supplier_id = (int)$_POST['supplier_id'] ?: null;
+ $sku = $_POST['sku'] ?? '';
+ $sale_price = (float)($_POST['sale_price'] ?? 0);
+ $purchase_price = (float)($_POST['purchase_price'] ?? 0);
+ $stock_quantity = (float)($_POST['stock_quantity'] ?? 0);
+ $min_stock_level = (float)($_POST['min_stock_level'] ?? 0);
+ $vat_rate = (float)($_POST['vat_rate'] ?? 0);
+ $expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null;
+ $is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
+ $promotion_start = !empty($_POST['promotion_start']) ? $_POST['promotion_start'] : null;
+ $promotion_end = !empty($_POST['promotion_end']) ? $_POST['promotion_end'] : null;
+ $promotion_percent = (float)($_POST['promotion_percent'] ?? 0);
+ $stmt = db()->prepare("UPDATE stock_items SET name_en = ?, name_ar = ?, category_id = ?, unit_id = ?, supplier_id = ?, sku = ?, sale_price = ?, purchase_price = ?, stock_quantity = ?, min_stock_level = ?, vat_rate = ?, expiry_date = ?, is_promotion = ?, promotion_start = ?, promotion_end = ?, promotion_percent = ? WHERE id = ?");
+ $stmt->execute([$name_en, $name_ar, $category_id, $unit_id, $supplier_id, $sku, $sale_price, $purchase_price, $stock_quantity, $min_stock_level, $vat_rate, $expiry_date, $is_promotion, $promotion_start, $promotion_end, $promotion_percent, $id]);
+ if (isset($_FILES['image']) && $_FILES['image']['error'] === 0) {
+ $ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
+ $filename = 'uploads/item_' . $id . '_' . time() . '.' . $ext;
+ if (move_uploaded_file($_FILES['image']['tmp_name'], $filename)) db()->prepare("UPDATE stock_items SET image_path = ? WHERE id = ?")->execute([$filename, $id]);
+ }
+ redirectWithMessage("Item updated successfully!");
+ }
+
+ if (isset($_POST['delete_item'])) {
+ db()->prepare("DELETE FROM stock_items WHERE id = ?")->execute([(int)$_POST['id']]);
+ redirectWithMessage("Item deleted successfully!");
+ }
+
+ if (isset($_POST['cancel_all_promotions'])) {
+ db()->query("UPDATE stock_items SET is_promotion = 0 WHERE is_promotion = 1");
+ redirectWithMessage("All active promotions have been cancelled.");
+ }
+
+ // Auto-expire finished promotions
+ db()->query("UPDATE stock_items SET is_promotion = 0 WHERE is_promotion = 1 AND promotion_end IS NOT NULL AND promotion_end < '" . date('Y-m-d') . "'");
+
+ if (isset($_POST['add_category'])) {
+ db()->prepare("INSERT INTO stock_categories (name_en, name_ar) VALUES (?, ?)")->execute([$_POST['name_en'] ?? '', $_POST['name_ar'] ?? '']);
+ redirectWithMessage("Category added!");
+ }
+ if (isset($_POST['add_unit'])) {
+ db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_name_en, short_name_ar) VALUES (?, ?, ?, ?)")->execute([$_POST['name_en'] ?? '', $_POST['name_ar'] ?? '', $_POST['short_en'] ?? '', $_POST['short_ar'] ?? '']);
+ redirectWithMessage("Unit added!");
+ }
+
+ if (isset($_POST['edit_category'])) {
+ db()->prepare("UPDATE stock_categories SET name_en = ?, name_ar = ? WHERE id = ?")->execute([$_POST['name_en'] ?? '', $_POST['name_ar'] ?? '', (int)$_POST['id']]);
+ redirectWithMessage("Category updated!");
+ }
+ if (isset($_POST['delete_category'])) {
+ db()->prepare("DELETE FROM stock_categories WHERE id = ?")->execute([(int)$_POST['id']]);
+ redirectWithMessage("Category deleted!");
+ }
+ if (isset($_POST['edit_unit'])) {
+ db()->prepare("UPDATE stock_units SET name_en = ?, name_ar = ?, short_name_en = ?, short_name_ar = ? WHERE id = ?")->execute([$_POST['name_en'] ?? '', $_POST['name_ar'] ?? '', $_POST['short_en'] ?? '', $_POST['short_ar'] ?? '', (int)$_POST['id']]);
+ redirectWithMessage("Unit updated!");
+ }
+ if (isset($_POST['delete_unit'])) {
+ db()->prepare("DELETE FROM stock_units WHERE id = ?")->execute([(int)$_POST['id']]);
+ redirectWithMessage("Unit deleted!");
+ }
+
+ if (isset($_POST['add_customer'])) {
+ $table = ($_POST['type'] ?? '') === 'supplier' ? 'suppliers' : 'customers';
+ $sql = "INSERT INTO $table (name, email, phone, tax_id, balance" . ($table === 'customers' ? ", loyalty_points" : "") . ") VALUES (?, ?, ?, ?, ?" . ($table === 'customers' ? ", 0" : "") . ")";
+ db()->prepare($sql)->execute([$_POST['name'] ?? '', $_POST['email'] ?? '', $_POST['phone'] ?? '', $_POST['tax_id'] ?? '', (float)($_POST['balance'] ?? 0)]);
+ redirectWithMessage("Entity added!");
+ }
+ if (isset($_POST['edit_customer'])) {
+ $table = ($_POST['type'] ?? '') === 'supplier' ? 'suppliers' : 'customers';
+ db()->prepare("UPDATE $table SET name = ?, email = ?, phone = ?, tax_id = ?, balance = ? WHERE id = ?")->execute([$_POST['name'] ?? '', $_POST['email'] ?? '', $_POST['phone'] ?? '', $_POST['tax_id'] ?? '', (float)($_POST['balance'] ?? 0), (int)$_POST['id']]);
+ redirectWithMessage("Entity updated!");
+ }
+ if (isset($_POST['delete_customer'])) {
+ $table = ($_POST['type'] ?? '') === 'supplier' ? 'suppliers' : 'customers';
+ db()->prepare("DELETE FROM $table WHERE id = ?")->execute([(int)$_POST['id']]);
+ redirectWithMessage("Entity deleted!");
+ }
+
+ // Invoices
+ if (isset($_POST['add_invoice'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $type = $_POST['type'] ?? 'sale';
+ $table = ($type === 'purchase') ? 'purchases' : 'invoices';
+ $item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
+ $cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
+ $fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
+
+ $cust_id = (int)$_POST['customer_id'];
+ $inv_date = $_POST['invoice_date'] ?: date('Y-m-d');
+ $due_date = $_POST['due_date'] ?: null;
+ $status = $_POST['status'] ?? 'pending';
+ $pay_type = $_POST['payment_type'] ?? 'cash';
+
+ $items = $_POST['item_ids'] ?? [];
+ if (empty($items)) {
+ throw new Exception("Please add at least one item.");
+ }
+ $qtys = $_POST['quantities'] ?? [];
+ $prices = $_POST['prices'] ?? [];
+
+ $total_subtotal = 0;
+ $total_vat = 0;
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+
+ $vatAmount = $subtotal * ($vatRate / 100);
+ $total_subtotal += $subtotal;
+ $total_vat += $vatAmount;
+ }
+
+ $total_with_vat = $total_subtotal + $total_vat;
+ $paid = (float)($_POST['paid_amount'] ?? 0);
+ if ($status === 'paid') $paid = $total_with_vat;
+
+ $stmt = $db->prepare("INSERT INTO $table ($cust_supplier_col, invoice_date, due_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$cust_id, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]);
+ $inv_id = $db->lastInsertId();
+
+ $items_for_journal = [];
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+ $vatAmount = $subtotal * ($vatRate / 100);
+
+ $db->prepare("INSERT INTO $item_table ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item_id, $qty, $price, $vatAmount, $subtotal]);
+
+ // Update stock
+ $change = ($type === 'sale') ? -$qty : $qty;
+ $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $item_id]);
+ $items_for_journal[] = ['id' => $item_id, 'qty' => $qty];
+ }
+
+ // Accounting
+ if ($type === 'sale') {
+ recordSaleJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat);
+ } else {
+ // For purchases, you might have recordPurchaseJournal, but let's check if it exists
+ if (function_exists('recordPurchaseJournal')) {
+ recordPurchaseJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat);
+ }
+ }
+
+ $db->commit();
+ $msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " #$inv_id created!";
+ redirectWithMessage($msg, "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST["add_quotation"])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $cust_id = (int)$_POST["customer_id"];
+ $quot_date = $_POST["quotation_date"] ?: date("Y-m-d");
+ $valid_until = $_POST["valid_until"] ?: null;
+ $status = $_POST["status"] ?? "pending";
+
+ $items = $_POST["item_ids"] ?? [];
+ $qtys = $_POST["quantities"] ?? [];
+ $prices = $_POST["prices"] ?? [];
+
+ $total_subtotal = 0;
+ $total_vat = 0;
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+
+ $vatAmount = $subtotal * ($vatRate / 100);
+ $total_subtotal += $subtotal;
+ $total_vat += $vatAmount;
+ }
+
+ $total_with_vat = $total_subtotal + $total_vat;
+
+ $stmt = $db->prepare("INSERT INTO quotations (customer_id, quotation_date, valid_until, status, total_amount, vat_amount, total_with_vat) VALUES (?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$cust_id, $quot_date, $valid_until, $status, $total_subtotal, $total_vat, $total_with_vat]);
+ $quot_id = $db->lastInsertId();
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+ $vatAmount = $subtotal * ($vatRate / 100);
+ $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)")->execute([$quot_id, $item_id, $qty, $price, $subtotal]);
+ }
+ $db->commit();
+ $msg = "Quotation #$quot_id created!";
+ redirectWithMessage($msg, "index.php?page=quotations");
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['edit_quotation'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $quot_id = (int)$_POST['quotation_id'];
+ $cust_id = (int)$_POST['customer_id'];
+ $quot_date = $_POST['quotation_date'];
+ $valid_until = $_POST['valid_until'] ?: null;
+ $status = $_POST['status'] ?? 'pending';
+
+ $items = $_POST['item_ids'] ?? [];
+ $qtys = $_POST['quantities'] ?? [];
+ $prices = $_POST['prices'] ?? [];
+
+ $total_subtotal = 0;
+ $total_vat = 0;
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+
+ $vatAmount = $subtotal * ($vatRate / 100);
+ $total_subtotal += $subtotal;
+ $total_vat += $vatAmount;
+ }
+
+ $total_with_vat = $total_subtotal + $total_vat;
+
+ $stmt = $db->prepare("UPDATE quotations SET customer_id = ?, quotation_date = ?, valid_until = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ? WHERE id = ?");
+ $stmt->execute([$cust_id, $quot_date, $valid_until, $status, $total_subtotal, $total_vat, $total_with_vat, $quot_id]);
+
+ // Delete old items
+ $db->prepare("DELETE FROM quotation_items WHERE quotation_id = ?")->execute([$quot_id]);
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+ $vatAmount = $subtotal * ($vatRate / 100);
+
+ $db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)")->execute([$quot_id, $item_id, $qty, $price, $subtotal]);
+ }
+ $db->commit();
+ $msg = "Quotation #$quot_id updated!";
+ redirectWithMessage($msg, "index.php?page=quotations");
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['delete_quotation'])) {
+ $id = (int)$_POST['id'];
+ db()->prepare("DELETE FROM quotations WHERE id = ?")->execute([$id]);
+ redirectWithMessage("Quotation deleted!", "index.php?page=quotations");
+ }
+
+ if (isset($_POST['add_lpo'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $supp_id = (int)$_POST['supplier_id'];
+ $lpo_date = $_POST['lpo_date'] ?: date('Y-m-d');
+ $delivery_date = $_POST['delivery_date'] ?: null;
+ $status = 'pending';
+ $terms = $_POST['terms_conditions'] ?? '';
+
+ $items = $_POST['item_ids'] ?? [];
+ $qtys = $_POST['quantities'] ?? [];
+ $prices = $_POST['prices'] ?? [];
+
+ $total_subtotal = 0;
+ $total_vat = 0;
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+
+ $vatAmount = $subtotal * ($vatRate / 100);
+ $total_subtotal += $subtotal;
+ $total_vat += $vatAmount;
+ }
+
+ $total_with_vat = $total_subtotal + $total_vat;
+
+ $stmt = $db->prepare("INSERT INTO lpos (supplier_id, lpo_date, delivery_date, status, total_amount, vat_amount, total_with_vat, terms_conditions) VALUES (?, ?, ?, 'pending', ?, ?, ?, ?)");
+ $stmt->execute([$supp_id, $lpo_date, $delivery_date, $total_subtotal, $total_vat, $total_with_vat, $terms]);
+ $lpo_id = $db->lastInsertId();
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+ $vatAmount = $subtotal * ($vatRate / 100);
+
+ $db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]);
+ }
+ $db->commit();
+ redirectWithMessage("LPO #$lpo_id created!", "index.php?page=lpos");
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['edit_lpo'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $lpo_id = (int)$_POST['lpo_id'];
+ $supp_id = (int)$_POST['supplier_id'];
+ $lpo_date = $_POST['lpo_date'];
+ $delivery_date = $_POST['delivery_date'] ?: null;
+ $status = $_POST['status'] ?? 'pending';
+ $terms = $_POST['terms_conditions'] ?? '';
+
+ $items = $_POST['item_ids'] ?? [];
+ $qtys = $_POST['quantities'] ?? [];
+ $prices = $_POST['prices'] ?? [];
+
+ $total_subtotal = 0;
+ $total_vat = 0;
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+
+ $vatAmount = $subtotal * ($vatRate / 100);
+ $total_subtotal += $subtotal;
+ $total_vat += $vatAmount;
+ }
+
+ $total_with_vat = $total_subtotal + $total_vat;
+
+ $stmt = $db->prepare("UPDATE lpos SET supplier_id = ?, lpo_date = ?, delivery_date = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, terms_conditions = ? WHERE id = ?");
+ $stmt->execute([$supp_id, $lpo_date, $delivery_date, $status, $total_subtotal, $total_vat, $total_with_vat, $terms, $lpo_id]);
+
+ $db->prepare("DELETE FROM lpo_items WHERE lpo_id = ?")->execute([$lpo_id]);
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+ $vatAmount = $subtotal * ($vatRate / 100);
+
+ $db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]);
+ }
+ $db->commit();
+ redirectWithMessage("LPO #$lpo_id updated!", "index.php?page=lpos");
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['delete_lpo'])) {
+ $id = (int)$_POST['id'];
+ db()->prepare("DELETE FROM lpos WHERE id = ?")->execute([$id]);
+ redirectWithMessage("LPO deleted!", "index.php?page=lpos");
+ }
+
+ if (isset($_POST['convert_to_invoice'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $quot_id = (int)$_POST['quotation_id'];
+
+ $stmt = $db->prepare("SELECT * FROM quotations WHERE id = ?");
+ $stmt->execute([$quot_id]);
+ $quot = $stmt->fetch();
+
+ if (!$quot) throw new Exception("Quotation not found.");
+ if ($quot['status'] === 'converted') throw new Exception("Quotation already converted.");
+
+ $stmtItems = $db->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
+ $stmtItems->execute([$quot_id]);
+ $qItems = $stmtItems->fetchAll();
+
+ // Create Invoice
+ $inv_date = date('Y-m-d');
+ $stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0)");
+ $stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat']]);
+ $inv_id = $db->lastInsertId();
+
+ $items_for_journal = [];
+ foreach ($qItems as $item) {
+ $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_price']]);
+
+ // Update stock
+ $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?")->execute([$item['quantity'], $item['item_id']]);
+ $items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
+ }
+
+ // Update Quotation status
+ $db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?")->execute([$quot_id]);
+
+ // Accounting
+ recordSaleJournal($inv_id, $quot['total_with_vat'], $inv_date, $items_for_journal, $quot['vat_amount']);
+
+ $db->commit();
+ redirectWithMessage("Quotation converted to Invoice #$inv_id successfully!", "index.php?page=sales");
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['convert_lpo_to_purchase'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $lpo_id = (int)$_POST['lpo_id'];
+
+ $stmt = $db->prepare("SELECT * FROM lpos WHERE id = ?");
+ $stmt->execute([$lpo_id]);
+ $lpo = $stmt->fetch();
+
+ if (!$lpo) throw new Exception("LPO not found.");
+ if ($lpo['status'] === 'converted') throw new Exception("LPO already converted.");
+
+ $stmtItems = $db->prepare("SELECT * FROM lpo_items WHERE lpo_id = ?");
+ $stmtItems->execute([$lpo_id]);
+ $lItems = $stmtItems->fetchAll();
+
+ // Create Purchase Invoice
+ $pur_date = date('Y-m-d');
+ $stmtPur = $db->prepare("INSERT INTO purchases (supplier_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0)");
+ $stmtPur->execute([$lpo['supplier_id'], $pur_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat']]);
+ $pur_id = $db->lastInsertId();
+
+ $items_for_journal = [];
+ foreach ($lItems as $item) {
+ $db->prepare("INSERT INTO purchase_items (purchase_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$pur_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_amount']]);
+
+ // Update stock
+ $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$item['quantity'], $item['item_id']]);
+ $items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
+ }
+
+ // Update LPO status
+ $db->prepare("UPDATE lpos SET status = 'converted' WHERE id = ?")->execute([$lpo_id]);
+
+ $db->commit();
+ redirectWithMessage("LPO converted to Purchase Invoice #$pur_id successfully!", "index.php?page=purchases");
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['record_payment'])) {
+ $id = (int)$_POST['invoice_id'];
+ $amount = (float)$_POST['amount'];
+ $date = $_POST['payment_date'] ?: date('Y-m-d');
+ $method = $_POST['payment_method'] ?? 'Cash';
+ $type = ($page === 'purchases') ? 'purchase' : 'sale';
+ $table = ($type === 'purchase') ? 'purchases' : 'invoices';
+ $payment_table = ($type === 'purchase') ? 'purchase_payments' : 'payments';
+ $fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
+
+ $db = db();
+ $db->prepare("INSERT INTO $payment_table ($fk_col, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)")->execute([$id, $amount, $date, $method, $_POST['notes'] ?? '']);
+ $pay_id = $db->lastInsertId();
+ $db->prepare("UPDATE $table SET paid_amount = paid_amount + ?, status = IF(paid_amount + ? >= total_with_vat, 'paid', 'partially_paid') WHERE id = ?")->execute([$amount, $amount, $id]);
+
+ if ($type === 'sale') recordPaymentReceivedJournal((int)$pay_id, $amount, $date, $method);
+ else recordPaymentMadeJournal((int)$pay_id, $amount, $date, $method);
+
+ $_SESSION['trigger_receipt_modal'] = true;
+ $_SESSION['show_receipt_id'] = $pay_id;
+ redirectWithMessage("Payment recorded!", "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
+ }
+
+ if (isset($_POST['add_expense'])) {
+ $amt = (float)$_POST['amount'];
+ $date = $_POST['expense_date'] ?: date('Y-m-d');
+ $desc = $_POST['description'] ?? '';
+ db()->prepare("INSERT INTO expenses (category_id, amount, expense_date, reference_no, description) VALUES (?, ?, ?, ?, ?)")->execute([(int)$_POST['category_id'], $amt, $date, $_POST['reference_no'] ?? '', $desc]);
+ recordExpenseJournal(db()->lastInsertId(), $amt, $date, $desc);
+ redirectWithMessage("Expense recorded!", "index.php?page=expenses");
+ }
+
+ 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.";
+ } else {
+ // Check for BOM and skip it
+ if (substr($firstBytes, 0, 3) === "\xEF\xBB\xBF") {
+ $handle = fopen($tmpPath, "r");
+ fseek($handle, 3);
+ } else {
+ $handle = fopen($tmpPath, "r");
+ }
+
+ $firstLine = fgets($handle);
+ rewind($handle);
+ if (substr($firstBytes, 0, 3) === "\xEF\xBB\xBF") fseek($handle, 3);
+
+ error_log("First line of CSV: " . $firstLine);
+
+ $seps = [",", ";", "\t", "|"];
+ $sep = ",";
+ $maxCount = 0;
+ foreach ($seps as $s) {
+ $count = substr_count($firstLine, $s);
+ if ($count > $maxCount) {
+ $maxCount = $count;
+ $sep = $s;
+ }
+ }
+ error_log("Detected separator: $sep");
+
+ $count = 0;
+ $errors = 0;
+ $rowNum = 0;
+ while (($data = fgetcsv($handle, 10000, $sep)) !== FALSE) {
+ $rowNum++;
+ if ($rowNum === 1) continue; // Skip header
+
+ if (count($data) < 4) { $errors++; continue; }
+
+ $sku = trim($data[0]);
+ $name_en = trim($data[1]);
+ $name_ar = trim($data[2]);
+ $price = (float)($data[3] ?? 0);
+ $qty = (float)($data[4] ?? 0);
+ $vat_rate = (float)($data[5] ?? 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 = ?, stock_quantity = ?, vat_rate = ? WHERE sku = ?")
+ ->execute([$name_en, $name_ar, $price, $qty, $vat_rate, $sku]);
+ } else {
+ db()->prepare("INSERT INTO stock_items (sku, name_en, name_ar, sale_price, stock_quantity, vat_rate) VALUES (?, ?, ?, ?, ?, ?)")
+ ->execute([$sku, $name_en, $name_ar, $price, $qty, $vat_rate]);
+ }
+ $count++;
+ }
+ fclose($handle);
+ redirectWithMessage("Import completed! $count items processed." . ($errors > 0 ? " ($errors rows skipped due to data errors.)" : ""), "index.php?page=items");
+ }
+ } else {
+ $errCode = $_FILES['excel_file']['error'] ?? 'no file';
+ $message = "Error: File upload failed. (Error code: $errCode)";
+ }
+ }
+
+ if (isset($_POST['add_expense_category'])) {
+ $name_en = $_POST['name_en'] ?? '';
+ $name_ar = $_POST['name_ar'] ?? '';
+ db()->prepare("INSERT INTO expense_categories (name_en, name_ar) VALUES (?, ?)")->execute([$name_en, $name_ar]);
+ redirectWithMessage("Expense category added!", "index.php?page=expense_categories");
+ }
+
+ if (isset($_POST['add_payment_method'])) {
+ $name = $_POST['name'] ?? '';
+ db()->prepare("INSERT INTO payment_methods (name) VALUES (?)")->execute([$name]);
+ redirectWithMessage("Payment method added!", "index.php?page=payment_methods");
+ }
+
+ if (isset($_POST['delete_invoice'])) {
+ $id = (int)$_POST['id'];
+ $type = ($page === 'purchases') ? 'purchase' : 'sale';
+ $table = ($type === 'purchase') ? 'purchases' : 'invoices';
+ $item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
+ $fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
+
+ db()->prepare("DELETE FROM $table WHERE id = ?")->execute([$id]);
+ db()->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]);
+ redirectWithMessage(($type === 'purchase' ? "Purchase" : "Invoice") . " deleted!", "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
+ }
+
+ if (isset($_POST['delete_quotation'])) {
+ $id = (int)$_POST['id'];
+ db()->prepare("DELETE FROM quotations WHERE id = ?")->execute([$id]);
+ $message = "Quotation deleted!";
+ }
+
+ if (isset($_POST['add_lpo'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $supp_id = (int)$_POST['supplier_id'];
+ $lpo_date = $_POST['lpo_date'] ?: date('Y-m-d');
+ $delivery_date = $_POST['delivery_date'] ?: null;
+ $terms = $_POST['terms_conditions'] ?? '';
+
+ $items = $_POST['item_ids'] ?? [];
+ if (empty($items)) {
+ throw new Exception("Please add at least one item.");
+ }
+ $qtys = $_POST['quantities'] ?? [];
+ $prices = $_POST['prices'] ?? [];
+
+ $total_subtotal = 0;
+ $total_vat = 0;
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+
+ $vatAmount = $subtotal * ($vatRate / 100);
+ $total_subtotal += $subtotal;
+ $total_vat += $vatAmount;
+ }
+
+ $total_with_vat = $total_subtotal + $total_vat;
+
+ $stmt = $db->prepare("INSERT INTO lpos (supplier_id, lpo_date, delivery_date, status, total_amount, vat_amount, total_with_vat, terms_conditions) VALUES (?, ?, ?, 'pending', ?, ?, ?, ?)");
+ $stmt->execute([$supp_id, $lpo_date, $delivery_date, $total_subtotal, $total_vat, $total_with_vat, $terms]);
+ $lpo_id = $db->lastInsertId();
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+ $vatAmount = $subtotal * ($vatRate / 100);
+
+ $db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]);
+ }
+ $db->commit();
+ redirectWithMessage("LPO #$lpo_id created!", "index.php?page=lpos");
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['edit_lpo'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $lpo_id = (int)$_POST['lpo_id'];
+ $supp_id = (int)$_POST['supplier_id'];
+ $lpo_date = $_POST['lpo_date'];
+ $delivery_date = $_POST['delivery_date'] ?: null;
+ $status = $_POST['status'] ?? 'pending';
+ $terms = $_POST['terms_conditions'] ?? '';
+
+ $items = $_POST['item_ids'] ?? [];
+ if (empty($items)) {
+ throw new Exception("Please add at least one item.");
+ }
+ $qtys = $_POST['quantities'] ?? [];
+ $prices = $_POST['prices'] ?? [];
+
+ $total_subtotal = 0;
+ $total_vat = 0;
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+
+ $vatAmount = $subtotal * ($vatRate / 100);
+ $total_subtotal += $subtotal;
+ $total_vat += $vatAmount;
+ }
+
+ $total_with_vat = $total_subtotal + $total_vat;
+
+ $stmt = $db->prepare("UPDATE lpos SET supplier_id = ?, lpo_date = ?, delivery_date = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, terms_conditions = ? WHERE id = ?");
+ $stmt->execute([$supp_id, $lpo_date, $delivery_date, $status, $total_subtotal, $total_vat, $total_with_vat, $terms, $lpo_id]);
+
+ $db->prepare("DELETE FROM lpo_items WHERE lpo_id = ?")->execute([$lpo_id]);
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+ $vatAmount = $subtotal * ($vatRate / 100);
+
+ $db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]);
+ }
+ $db->commit();
+ $message = "LPO #$lpo_id updated!";
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['delete_lpo'])) {
+ $id = (int)$_POST['id'];
+ db()->prepare("DELETE FROM lpos WHERE id = ?")->execute([$id]);
+ $message = "LPO deleted!";
+ }
+
+ if (isset($_POST['convert_to_invoice'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $quot_id = (int)$_POST['quotation_id'];
+
+ $stmt = $db->prepare("SELECT * FROM quotations WHERE id = ?");
+ $stmt->execute([$quot_id]);
+ $quot = $stmt->fetch();
+
+ if (!$quot) throw new Exception("Quotation not found.");
+ if ($quot['status'] === 'converted') throw new Exception("Quotation already converted.");
+
+ $stmtItems = $db->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
+ $stmtItems->execute([$quot_id]);
+ $qItems = $stmtItems->fetchAll();
+
+ // Create Invoice
+ $inv_date = date('Y-m-d');
+ $stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0)");
+ $stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat']]);
+ $inv_id = $db->lastInsertId();
+
+ $items_for_journal = [];
+ foreach ($qItems as $item) {
+ $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_price']]);
+
+ // Update stock
+ $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?")->execute([$item['quantity'], $item['item_id']]);
+ $items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
+ }
+
+ // Update Quotation status
+ $db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?")->execute([$quot_id]);
+
+ // Accounting
+ recordSaleJournal($inv_id, $quot['total_with_vat'], $inv_date, $items_for_journal, $quot['vat_amount']);
+
+ $db->commit();
+ $message = "Quotation converted to Invoice #$inv_id successfully!";
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['convert_lpo_to_purchase'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $lpo_id = (int)$_POST['lpo_id'];
+
+ $stmt = $db->prepare("SELECT * FROM lpos WHERE id = ?");
+ $stmt->execute([$lpo_id]);
+ $lpo = $stmt->fetch();
+
+ if (!$lpo) throw new Exception("LPO not found.");
+ if ($lpo['status'] === 'converted') throw new Exception("LPO already converted.");
+
+ $stmtItems = $db->prepare("SELECT * FROM lpo_items WHERE lpo_id = ?");
+ $stmtItems->execute([$lpo_id]);
+ $lItems = $stmtItems->fetchAll();
+
+ // Create Purchase Invoice
+ $inv_date = date('Y-m-d');
+ $stmtPur = $db->prepare("INSERT INTO purchases (supplier_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0)");
+ $stmtPur->execute([$lpo['supplier_id'], $inv_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat']]);
+ $pur_id = $db->lastInsertId();
+
+ $items_for_journal = [];
+ foreach ($lItems as $item) {
+ $db->prepare("INSERT INTO purchase_items (purchase_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$pur_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_amount']]);
+
+ // Update stock
+ $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$item['quantity'], $item['item_id']]);
+ $items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
+ }
+
+ // Update LPO status
+ $db->prepare("UPDATE lpos SET status = 'converted' WHERE id = ?")->execute([$lpo_id]);
+
+ // Accounting (if exists)
+ if (function_exists('recordPurchaseJournal')) {
+ recordPurchaseJournal($pur_id, $lpo['total_with_vat'], $inv_date, $items_for_journal, $lpo['vat_amount']);
+ }
+
+ $db->commit();
+ $message = "LPO converted to Purchase Invoice #$pur_id successfully!";
+ header("Location: index.php?page=purchases");
+ exit;
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ if (isset($_POST['record_payment'])) {
+ $id = (int)$_POST['invoice_id'];
+ $amount = (float)$_POST['amount'];
+ $date = $_POST['payment_date'] ?: date('Y-m-d');
+ $method = $_POST['payment_method'] ?? 'Cash';
+ $type = ($page === 'purchases') ? 'purchase' : 'sale';
+ $table = ($type === 'purchase') ? 'purchases' : 'invoices';
+ $payment_table = ($type === 'purchase') ? 'purchase_payments' : 'payments';
+ $fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
+
+ $db = db();
+ $db->prepare("INSERT INTO $payment_table ($fk_col, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)")->execute([$id, $amount, $date, $method, $_POST['notes'] ?? '']);
+ $pay_id = $db->lastInsertId();
+ $db->prepare("UPDATE $table SET paid_amount = paid_amount + ?, status = IF(paid_amount + ? >= total_with_vat, 'paid', 'partially_paid') WHERE id = ?")->execute([$amount, $amount, $id]);
+
+ if ($type === 'sale') recordPaymentReceivedJournal((int)$pay_id, $amount, $date, $method);
+ else recordPaymentMadeJournal((int)$pay_id, $amount, $date, $method);
+ $message = "Payment recorded!";
+ $_SESSION['trigger_receipt_modal'] = true; $_SESSION['show_receipt_id'] = $pay_id;
+ }
+
+ if (isset($_POST['add_expense'])) {
+ $amt = (float)$_POST['amount'];
+ $date = $_POST['expense_date'] ?: date('Y-m-d');
+ $desc = $_POST['description'] ?? '';
+ db()->prepare("INSERT INTO expenses (category_id, amount, expense_date, reference_no, description) VALUES (?, ?, ?, ?, ?)")->execute([(int)$_POST['category_id'], $amt, $date, $_POST['reference_no'] ?? '', $desc]);
+ recordExpenseJournal(db()->lastInsertId(), $amt, $date, $desc);
+ $message = "Expense recorded!";
+ }
+
+ 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.";
+ } else {
+ // Check for BOM and skip it
+ if (substr($firstBytes, 0, 3) === "\xEF\xBB\xBF") {
+ $handle = fopen($tmpPath, "r");
+ fseek($handle, 3);
+ } else {
+ $handle = fopen($tmpPath, "r");
+ }
+
+ $firstLine = fgets($handle);
+ rewind($handle);
+ if (substr($firstBytes, 0, 3) === "\xEF\xBB\xBF") fseek($handle, 3);
+
+ error_log("First line of CSV: " . $firstLine);
+
+ $seps = [",", ";", "\t", "|"];
+ $sep = ",";
+ $maxCount = 0;
+ foreach ($seps as $s) {
+ $count = substr_count($firstLine, $s);
+ if ($count > $maxCount) {
+ $maxCount = $count;
+ $sep = $s;
+ }
+ }
+ error_log("Detected separator: '$sep' (count: $maxCount)");
+
+ // Try to detect if first line is header
+ $firstRow = fgetcsv($handle, 0, $sep);
+ $isHeader = true;
+ // If the first row's 4th or 5th columns are numeric, it's probably NOT a header
+ if (isset($firstRow[3]) && is_numeric(str_replace(',', '', trim($firstRow[3])))) $isHeader = false;
+ if (isset($firstRow[4]) && is_numeric(str_replace(',', '', trim($firstRow[4])))) $isHeader = false;
+
+ if (!$isHeader) {
+ rewind($handle);
+ if (substr($firstBytes, 0, 3) === "\xEF\xBB\xBF") fseek($handle, 3);
+ error_log("No header detected, starting from first row.");
+ } else {
+ error_log("Header detected and skipped: " . print_r($firstRow, true));
+ }
+
+ $count = 0; $errors = 0;
+ while (($data = fgetcsv($handle, 0, $sep)) !== FALSE) {
+ if (count($data) < 2) continue; // Skip empty or single-column rows that aren't SKUs
+ if (empty($data[0]) && empty($data[1]) && empty($data[2])) continue;
+ try {
+ foreach ($data as &$val) {
+ if ($val === null) continue;
+ if (!mb_check_encoding($val, 'UTF-8')) {
+ $val = mb_convert_encoding($val, 'UTF-8', 'Windows-1252');
+ }
+ $val = mb_convert_encoding($val, 'UTF-8', 'UTF-8');
+ $val = trim($val);
+ }
+
+ // Map: 0:sku, 1:name_en, 2:name_ar, 3:sale_price, 4:cost_price
+ $sku = substr(trim($data[0] ?? ''), 0, 100);
+ $name_en = $data[1] ?? '';
+ $name_ar = $data[2] ?? '';
+ $sale_price = str_replace(',', '', $data[3] ?? 0);
+ $purchase_price = str_replace(',', '', $data[4] ?? 0);
+
+ if (empty($sku) && empty($name_en)) continue;
+
+ $stmt = db()->prepare("INSERT INTO stock_items (sku, name_en, name_ar, sale_price, purchase_price)
+ VALUES (?, ?, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE name_en=VALUES(name_en), name_ar=VALUES(name_ar),
+ sale_price=VALUES(sale_price), purchase_price=VALUES(purchase_price)");
+ $stmt->execute([$sku, $name_en, $name_ar, (float)$sale_price, (float)$purchase_price]);
+ $count++;
+ } catch (Throwable $e) {
+ file_put_contents('import_debug.log', date('Y-m-d H:i:s') . " - Row error: " . $e->getMessage() . " Data: " . json_encode($data) . "\n", FILE_APPEND);
+ error_log("Import row error: " . $e->getMessage() . " Data: " . print_r($data, true));
+ $errors++;
+ }
+ }
+ fclose($handle);
+ $message = "Import completed! $count items processed." . ($errors > 0 ? " ($errors rows skipped due to data errors.)" : "");
+ error_log("Import finished. Processed: $count, Errors: $errors");
+ }
+ } else {
+ $errCode = $_FILES['excel_file']['error'] ?? 'no file';
+ error_log("File upload failed or missing. Error code: $errCode");
+ $message = "Error: File upload failed. (Error code: $errCode)";
+ }
+ }
+
+ if (isset($_POST['add_expense_category'])) {
+ db()->prepare("INSERT INTO expense_categories (name_en, name_ar) VALUES (?, ?)")->execute([$_POST['name_en'] ?? '', $_POST['name_ar'] ?? '']);
+ $message = "Expense category added!";
+ }
+
+ if (isset($_POST['add_payment_method'])) {
+ db()->prepare("INSERT INTO payment_methods (name, type) VALUES (?, ?)")->execute([$_POST['name'] ?? '', $_POST['type'] ?? 'Cash']);
+ $message = "Payment method added!";
+ }
+
+ if (isset($_POST['delete_invoice'])) {
+ $id = (int)$_POST['id'];
+ $type = ($page === 'purchases') ? 'purchase' : 'sale';
+ $table = ($type === 'purchase') ? 'purchases' : 'invoices';
+ $item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
+ $fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
+
+ db()->prepare("DELETE FROM $table WHERE id = ?")->execute([$id]);
+ db()->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]);
+ $message = ($type === 'purchase' ? "Purchase" : "Invoice") . " deleted!";
+ }
+
+ if (isset($_POST['edit_invoice'])) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $id = (int)$_POST['invoice_id'];
+ $type = ($page === 'purchases') ? 'purchase' : 'sale';
+ $table = ($type === 'purchase') ? 'purchases' : 'invoices';
+ $item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
+ $cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
+ $fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
+
+ $cust_id = (int)$_POST['customer_id'];
+ $date = $_POST['invoice_date'] ?: date('Y-m-d');
+ $due_date = $_POST['due_date'] ?: null;
+ $status = $_POST['status'] ?? 'pending';
+ $pay_type = $_POST['payment_type'] ?? 'cash';
+
+ $items = $_POST['item_ids'] ?? [];
+ $qtys = $_POST['quantities'] ?? [];
+ $prices = $_POST['prices'] ?? [];
+
+ $total_subtotal = 0;
+ $total_vat = 0;
+
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+
+ $vatAmount = $subtotal * ($vatRate / 100);
+ $total_subtotal += $subtotal;
+ $total_vat += $vatAmount;
+ }
+
+ $total_with_vat = $total_subtotal + $total_vat;
+ $paid = (float)($_POST['paid_amount'] ?? 0);
+ if ($status === 'paid') $paid = $total_with_vat;
+
+ $db->prepare("UPDATE $table SET $cust_supplier_col = ?, invoice_date = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?")
+ ->execute([$cust_id, $date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $id]);
+
+ // Revert stock for old items
+ $stmtOld = $db->prepare("SELECT item_id, quantity FROM $item_table WHERE $fk_col = ?");
+ $stmtOld->execute([$id]);
+ $oldItems = $stmtOld->fetchAll();
+ foreach ($oldItems as $old) {
+ $change = ($type === 'sale') ? (float)$old['quantity'] : -(float)$old['quantity'];
+ $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $old['item_id']]);
+ }
+
+ // Delete old items
+ $db->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]);
+
+ // Insert new items and update stock
+ foreach ($items as $i => $item_id) {
+ if (!$item_id) continue;
+ $qty = (float)$qtys[$i];
+ $price = (float)$prices[$i];
+ $subtotal = $qty * $price;
+
+ $stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
+ $stmtVat->execute([$item_id]);
+ $vatRate = (float)$stmtVat->fetchColumn();
+ $vatAmount = $subtotal * ($vatRate / 100);
+
+ $db->prepare("INSERT INTO $item_table ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$id, $item_id, $qty, $price, $vatAmount, $subtotal]);
+
+ $change = ($type === 'sale') ? -$qty : $qty;
+ $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?")->execute([$change, $item_id]);
+ }
+
+ $db->commit();
+ $msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " updated successfully!";
+ redirectWithMessage($msg, "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
+ } catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
+ }
+
+ // --- HR Handlers ---
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ error_log("POST Request detected. Action: " . (print_r($_POST, true)));
+}
+if (isset($_POST['add_hr_department'])) {
+ $name = $_POST['name'] ?? '';
+ if ($name) {
+ $stmt = db()->prepare("INSERT INTO hr_departments (name) VALUES (?)");
+ $stmt->execute([$name]);
+ $message = "Department added successfully!";
+ }
+ }
+ if (isset($_POST['edit_hr_department'])) {
+ $id = (int)$_POST['id'];
+ $name = $_POST['name'] ?? '';
+ if ($id && $name) {
+ $stmt = db()->prepare("UPDATE hr_departments SET name = ? WHERE id = ?");
+ $stmt->execute([$name, $id]);
+ redirectWithMessage("Department updated successfully!", "index.php?page=hr_departments");
+ }
+ }
+ if (isset($_POST['delete_hr_department'])) {
+ $id = (int)$_POST['id'];
+ if ($id) {
+ $stmt = db()->prepare("DELETE FROM hr_departments WHERE id = ?");
+ $stmt->execute([$id]);
+ redirectWithMessage("Department deleted successfully!", "index.php?page=hr_departments");
+ }
+ }
+ if (isset($_POST['add_hr_employee'])) {
+ $dept_id = (int)$_POST['department_id'];
+ $biometric_id = $_POST['biometric_id'] ?? '';
+ $name = $_POST['name'] ?? '';
+ $email = $_POST['email'] ?? '';
+ $phone = $_POST['phone'] ?? '';
+ $pos = $_POST['position'] ?? '';
+ $salary = (float)$_POST['salary'];
+ $j_date = $_POST['joining_date'] ?: date('Y-m-d');
+
+ if ($name) {
+ $stmt = db()->prepare("INSERT INTO hr_employees (department_id, biometric_id, name, email, phone, position, salary, joining_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$dept_id, $biometric_id, $name, $email, $phone, $pos, $salary, $j_date]);
+ redirectWithMessage("Employee added successfully!", "index.php?page=hr_employees");
+ }
+ }
+ if (isset($_POST['edit_hr_employee'])) {
+ $id = (int)$_POST['id'];
+ $dept_id = (int)$_POST['department_id'];
+ $biometric_id = $_POST['biometric_id'] ?? '';
+ $name = $_POST['name'] ?? '';
+ $email = $_POST['email'] ?? '';
+ $phone = $_POST['phone'] ?? '';
+ $pos = $_POST['position'] ?? '';
+ $salary = (float)$_POST['salary'];
+ $j_date = $_POST['joining_date'];
+ $status = $_POST['status'] ?? 'active';
+
+ if ($id && $name) {
+ $stmt = db()->prepare("UPDATE hr_employees SET department_id = ?, biometric_id = ?, name = ?, email = ?, phone = ?, position = ?, salary = ?, joining_date = ?, status = ? WHERE id = ?");
+ $stmt->execute([$dept_id, $biometric_id, $name, $email, $phone, $pos, $salary, $j_date, $status, $id]);
+ redirectWithMessage("Employee updated successfully!", "index.php?page=hr_employees");
+ }
+ }
+ if (isset($_POST['delete_hr_employee'])) {
+ $id = (int)$_POST['id'];
+ if ($id) {
+ $stmt = db()->prepare("DELETE FROM hr_employees WHERE id = ?");
+ $stmt->execute([$id]);
+ redirectWithMessage("Employee deleted successfully!", "index.php?page=hr_employees");
+ }
+ }
+ if (isset($_POST['mark_hr_attendance'])) {
+ $emp_id = (int)$_POST['employee_id'];
+ $date = $_POST['attendance_date'] ?: date('Y-m-d');
+ $status = $_POST['status'] ?? 'present';
+ $in = $_POST['clock_in'] ?: null;
+ $out = $_POST['clock_out'] ?: null;
+
+ if ($emp_id) {
+ $check = db()->prepare("SELECT id FROM hr_attendance WHERE employee_id = ? AND attendance_date = ?");
+ $check->execute([$emp_id, $date]);
+ if ($check->fetch()) {
+ $stmt = db()->prepare("UPDATE hr_attendance SET status = ?, clock_in = ?, clock_out = ? WHERE employee_id = ? AND attendance_date = ?");
+ $stmt->execute([$status, $in, $out, $emp_id, $date]);
+ } else {
+ $stmt = db()->prepare("INSERT INTO hr_attendance (employee_id, attendance_date, status, clock_in, clock_out) VALUES (?, ?, ?, ?, ?)");
+ $stmt->execute([$emp_id, $date, $status, $in, $out]);
+ }
+ redirectWithMessage("Attendance marked successfully!", "index.php?page=hr_attendance&date=$date");
+ }
+ }
+ if (isset($_POST['generate_payroll'])) {
+ $emp_id = (int)$_POST['employee_id'];
+ $month = (int)$_POST['month'];
+ $year = (int)$_POST['year'];
+ $bonus = (float)$_POST['bonus'];
+ $deduct = (float)$_POST['deductions'];
+ $notes = $_POST['notes'] ?? '';
+
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $emp = $db->query("SELECT salary FROM hr_employees WHERE id = $emp_id")->fetch();
+ if (!$emp) throw new Exception("Employee not found.");
+
+ $basic = (float)$emp['salary'];
+ $net = $basic + $bonus - $deduct;
+
+ $check = $db->prepare("SELECT id FROM hr_payroll WHERE employee_id = ? AND payroll_month = ? AND payroll_year = ?");
+ $check->execute([$emp_id, $month, $year]);
+ if ($check->fetch()) {
+ throw new Exception("Payroll already exists for this employee in the selected period.");
+ }
+
+ $stmt = $db->prepare("INSERT INTO hr_payroll (employee_id, payroll_month, payroll_year, basic_salary, bonus, deductions, net_salary, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$emp_id, $month, $year, $basic, $bonus, $deduct, $net, $notes]);
+ $db->commit();
+ redirectWithMessage("Payroll generated successfully!", "index.php?page=hr_payroll&month=$month&year=$year");
+ } catch (Exception $e) {
+ $db->rollBack();
+ $message = "Error: " . $e->getMessage();
+ }
+ }
+ if (isset($_POST['pay_payroll'])) {
+ $id = (int)$_POST['id'];
+ $db = db();
+ try {
+ $db->beginTransaction();
+ $stmt = $db->prepare("SELECT * FROM hr_payroll WHERE id = ? AND status = 'unpaid'");
+ $stmt->execute([$id]);
+ $p = $stmt->fetch();
+ if ($p) {
+ $db->prepare("UPDATE hr_payroll SET status = 'paid', payment_date = CURDATE() WHERE id = ?")->execute([$id]);
+ // Accounting
+ recordExpenseJournalForPayroll($id, (float)$p['net_salary'], date('Y-m-d'));
+ $db->commit();
+ redirectWithMessage("Payroll marked as paid and recorded in accounting!", "index.php?page=hr_payroll&month={$p['payroll_month']}&year={$p['payroll_year']}");
+ } else {
+ throw new Exception("Payroll already paid or not found.");
+ }
+ } catch (Exception $e) {
+ $db->rollBack();
+ $message = "Error: " . $e->getMessage();
+ }
+ }
+ if (isset($_POST['delete_payroll'])) {
+ $id = (int)$_POST['id'];
+ $stmt = db()->prepare("SELECT payroll_month, payroll_year FROM hr_payroll WHERE id = ?");
+ $stmt->execute([$id]);
+ $p = $stmt->fetch();
+ if ($p) {
+ db()->prepare("DELETE FROM hr_payroll WHERE id = ?")->execute([$id]);
+ redirectWithMessage("Payroll record deleted successfully!", "index.php?page=hr_payroll&month={$p['payroll_month']}&year={$p['payroll_year']}");
+ }
+ }
+
+ if (isset($_POST['add_sales_return'])) {
+ $invoice_id = (int)$_POST['invoice_id'];
+ $return_date = $_POST['return_date'] ?: date('Y-m-d');
+ $notes = $_POST['notes'] ?? '';
+ $item_ids = $_POST['item_ids'] ?? [];
+ $quantities = $_POST['quantities'] ?? [];
+ $prices = $_POST['prices'] ?? [];
+
+ if ($invoice_id && !empty($item_ids)) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+
+ // Get customer_id from invoice
+ $stmtInv = $db->prepare("SELECT customer_id FROM invoices WHERE id = ?");
+ $stmtInv->execute([$invoice_id]);
+ $customer_id = $stmtInv->fetchColumn();
+
+ $total_return = 0;
+ foreach ($quantities as $i => $qty) {
+ $total_return += (float)$qty * (float)$prices[$i];
+ }
+
+ // Insert Sales Return
+ $stmt = $db->prepare("INSERT INTO sales_returns (invoice_id, customer_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)");
+ $stmt->execute([$invoice_id, $customer_id, $return_date, $total_return, $notes]);
+ $return_id = $db->lastInsertId();
+
+ // Insert Return Items and Update Stock
+ $stmtItem = $db->prepare("INSERT INTO sales_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
+ $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
+
+ foreach ($item_ids as $i => $item_id) {
+ $qty = (float)$quantities[$i];
+ if ($qty > 0) {
+ $price = (float)$prices[$i];
+ $line_total = $qty * $price;
+ $stmtItem->execute([$return_id, $item_id, $qty, $price, $line_total]);
+ $stmtStock->execute([$qty, $item_id]);
+ }
+ }
+
+ $db->commit();
+ redirectWithMessage("Sales Return processed successfully!");
+ } catch (Exception $e) {
+ $db->rollBack();
+ $message = "Error processing return: " . $e->getMessage();
+ }
+ }
+ }
+
+ if (isset($_POST['add_purchase_return'])) {
+ $invoice_id = (int)$_POST['invoice_id'];
+ $return_date = $_POST['return_date'] ?: date('Y-m-d');
+ $notes = $_POST['notes'] ?? '';
+ $item_ids = $_POST['item_ids'] ?? [];
+ $quantities = $_POST['quantities'] ?? [];
+ $prices = $_POST['prices'] ?? [];
+
+ if ($invoice_id && !empty($item_ids)) {
+ $db = db();
+ try {
+ $db->beginTransaction();
+
+ // Get supplier_id from purchase
+ $stmtInv = $db->prepare("SELECT supplier_id FROM purchases WHERE id = ?");
+ $stmtInv->execute([$invoice_id]);
+ $supplier_id = $stmtInv->fetchColumn();
+
+ $total_return = 0;
+ foreach ($quantities as $i => $qty) {
+ $total_return += (float)$qty * (float)$prices[$i];
+ }
+
+ // Insert Purchase Return
+ $stmt = $db->prepare("INSERT INTO purchase_returns (invoice_id, supplier_id, return_date, total_amount, notes) VALUES (?, ?, ?, ?, ?)");
+ $stmt->execute([$invoice_id, $supplier_id, $return_date, $total_return, $notes]);
+ $return_id = $db->lastInsertId();
+
+ // Insert Return Items and Update Stock
+ $stmtItem = $db->prepare("INSERT INTO purchase_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
+ $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
+
+ foreach ($item_ids as $i => $item_id) {
+ $qty = (float)$quantities[$i];
+ if ($qty > 0) {
+ $price = (float)$prices[$i];
+ $line_total = $qty * $price;
+ $stmtItem->execute([$return_id, $item_id, $qty, $price, $line_total]);
+ $stmtStock->execute([$qty, $item_id]);
+ }
+ }
+
+ $db->commit();
+ redirectWithMessage("Purchase Return processed successfully!");
+ } catch (Exception $e) {
+ $db->rollBack();
+ $message = "Error processing return: " . $e->getMessage();
+ }
+ }
+ }
+
+ // --- Biometric Devices Handlers ---
+ if (isset($_POST['add_biometric_device'])) {
+ $name = $_POST['device_name'] ?? '';
+ $ip = $_POST['ip_address'] ?? '';
+ $port = (int)($_POST['port'] ?? 4370);
+ $io = $_POST['io_address'] ?? '';
+ $serial = $_POST['serial_number'] ?? '';
+ if ($name && $ip) {
+ $stmt = db()->prepare("INSERT INTO hr_biometric_devices (device_name, ip_address, port, io_address, serial_number) VALUES (?, ?, ?, ?, ?)");
+ $stmt->execute([$name, $ip, $port, $io, $serial]);
+ $message = "Device added successfully!";
+ }
+ }
+ if (isset($_POST['edit_biometric_device'])) {
+ $id = (int)$_POST['id'];
+ $name = $_POST['device_name'] ?? '';
+ $ip = $_POST['ip_address'] ?? '';
+ $port = (int)($_POST['port'] ?? 4370);
+ $io = $_POST['io_address'] ?? '';
+ $serial = $_POST['serial_number'] ?? '';
+ if ($id && $name && $ip) {
+ $stmt = db()->prepare("UPDATE hr_biometric_devices SET device_name = ?, ip_address = ?, port = ?, io_address = ?, serial_number = ? WHERE id = ?");
+ $stmt->execute([$name, $ip, $port, $io, $serial, $id]);
+ $message = "Device updated successfully!";
+ }
+ }
+ if (isset($_POST['delete_biometric_device'])) {
+ $id = (int)$_POST['id'];
+ if ($id) {
+ $stmt = db()->prepare("DELETE FROM hr_biometric_devices WHERE id = ?");
+ $stmt->execute([$id]);
+ $message = "Device deleted successfully!";
+ }
+ }
+
+ if (isset($_POST['pull_biometric_data'])) {
+ $devices = db()->query("SELECT * FROM hr_biometric_devices WHERE status = 'active'")->fetchAll();
+ if (empty($devices)) {
+ $message = "No active biometric devices found to pull data from.";
+ } else {
+ // Simulation of pulling data from multiple devices
+ $employees = db()->query("SELECT id, biometric_id FROM hr_employees WHERE biometric_id IS NOT NULL")->fetchAll();
+ $pulled_count = 0;
+ $device_count = 0;
+ $date = date('Y-m-d');
+
+ foreach ($devices as $device) {
+ $device_pulled = 0;
+ foreach ($employees as $emp) {
+ // Randomly simulate logs for each employee for this device
+ if (rand(0, 1)) {
+ $check_in = $date . ' ' . str_pad((string)rand(7, 9), 2, '0', STR_PAD_LEFT) . ':' . str_pad((string)rand(0, 59), 2, '0', STR_PAD_LEFT) . ':00';
+ $check_out = $date . ' ' . str_pad((string)rand(16, 18), 2, '0', STR_PAD_LEFT) . ':' . str_pad((string)rand(0, 59), 2, '0', STR_PAD_LEFT) . ':00';
+
+ // Log check-in
+ $stmt = db()->prepare("INSERT INTO hr_biometric_logs (biometric_id, device_id, employee_id, timestamp, type) VALUES (?, ?, ?, ?, 'check_in')");
+ $stmt->execute([$emp['biometric_id'], $device['id'], $emp['id'], $check_in]);
+
+ // Log check-out
+ $stmt = db()->prepare("INSERT INTO hr_biometric_logs (biometric_id, device_id, employee_id, timestamp, type) VALUES (?, ?, ?, ?, 'check_out')");
+ $stmt->execute([$emp['biometric_id'], $device['id'], $emp['id'], $check_out]);
+
+ $device_pulled += 2;
+ $pulled_count += 2;
+
+ $in_time = date('H:i:s', strtotime($check_in));
+ $out_time = date('H:i:s', strtotime($check_out));
+
+ // Update attendance record (earliest in, latest out)
+ $stmt = db()->prepare("INSERT INTO hr_attendance (employee_id, attendance_date, status, clock_in, clock_out)
+ VALUES (?, ?, 'present', ?, ?)
+ ON DUPLICATE KEY UPDATE status = 'present',
+ clock_in = IF(clock_in IS NULL OR ? < clock_in, ?, clock_in),
+ clock_out = IF(clock_out IS NULL OR ? > clock_out, ?, clock_out)");
+ $stmt->execute([$emp['id'], $date, $in_time, $out_time, $in_time, $in_time, $out_time, $out_time]);
+ }
+ }
+ db()->prepare("UPDATE hr_biometric_devices SET last_sync = CURRENT_TIMESTAMP WHERE id = ?")->execute([$device['id']]);
+ $device_count++;
+ }
+ $message = "Successfully synced $device_count devices and pulled $pulled_count records.";
+ }
+ }
+
+ if (isset($_POST['test_device_connection'])) {
+ $id = (int)$_POST['id'];
+ $device = db()->prepare("SELECT * FROM hr_biometric_devices WHERE id = ?");
+ $device->execute([$id]);
+ $d = $device->fetch();
+ if ($d) {
+ // Simulated connection check
+ $message = "Connection to device '{$d['device_name']}' ({$d['ip_address']}) was successful! (Simulated)";
+ }
+ }
+
+ // --- User & Role Groups Handlers ---
+ if (isset($_POST['add_role_group'])) {
+ $name = $_POST['name'] ?? '';
+ $permissions = isset($_POST['permissions']) ? $_POST['permissions'] : [];
+ if ($name) {
+ try {
+ $db = db();
+ $db->beginTransaction();
+ $stmt = $db->prepare("INSERT INTO role_groups (name) VALUES (?)");
+ $stmt->execute([$name]);
+ $role_id = $db->lastInsertId();
+
+ if (!empty($permissions)) {
+ $stmtPerm = $db->prepare("INSERT INTO role_permissions (role_id, permission) VALUES (?, ?)");
+ foreach ($permissions as $p) {
+ $stmtPerm->execute([$role_id, $p]);
+ }
+ }
+ $db->commit();
+ $message = "Role Group added successfully!";
+ } catch (PDOException $e) {
+ if ($db->inTransaction()) $db->rollBack();
+ $message = "Error adding role group: " . $e->getMessage();
+ }
+ }
+ }
+
+ if (isset($_POST['add_user'])) {
+ $username = $_POST['username'] ?? '';
+ $password = $_POST['password'] ?? '';
+ $email = $_POST['email'] ?? '';
+ $phone = $_POST['phone'] ?? '';
+ $group_id = (int)($_POST['group_id'] ?? 0) ?: null;
+ if ($username && $password) {
+ $hashed_password = password_hash($password, PASSWORD_DEFAULT);
+ $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!";
+ } catch (PDOException $e) {
+ if ($e->getCode() == '23000') {
+ $message = "Error: Username already exists.";
+ } else {
+ $message = "Error adding user: " . $e->getMessage();
+ }
+ }
+ }
+ }
+ if (isset($_POST['edit_role_group'])) {
+ $id = (int)$_POST['id'];
+ $name = $_POST['name'] ?? '';
+ $permissions = isset($_POST['permissions']) ? $_POST['permissions'] : [];
+ if ($id && $name) {
+ try {
+ $db = db();
+ $db->beginTransaction();
+ $stmt = $db->prepare("UPDATE role_groups SET name = ? WHERE id = ?");
+ $stmt->execute([$name, $id]);
+
+ // Refresh permissions
+ $stmtDel = $db->prepare("DELETE FROM role_permissions WHERE role_id = ?");
+ $stmtDel->execute([$id]);
+
+ if (!empty($permissions)) {
+ $stmtPerm = $db->prepare("INSERT INTO role_permissions (role_id, permission) VALUES (?, ?)");
+ foreach ($permissions as $p) {
+ $stmtPerm->execute([$id, $p]);
+ }
+ }
+ $db->commit();
+ $message = "Role Group updated successfully!";
+ } catch (PDOException $e) {
+ if ($db->inTransaction()) $db->rollBack();
+ $message = "Error updating role group: " . $e->getMessage();
+ }
+ }
+ }
+ if (isset($_POST['delete_role_group'])) {
+ $id = (int)$_POST['id'];
+ if ($id) {
+ $stmt = db()->prepare("DELETE FROM role_groups WHERE id = ?");
+ $stmt->execute([$id]);
+ $message = "Role Group deleted successfully!";
+ }
+ }
+ if (isset($_POST['edit_user'])) {
+ $id = (int)$_POST['id'];
+ $username = $_POST['username'] ?? '';
+ $email = $_POST['email'] ?? '';
+ $phone = $_POST['phone'] ?? '';
+ $group_id = (int)($_POST['group_id'] ?? 0) ?: null;
+ $status = $_POST['status'] ?? 'active';
+ if ($id && $username) {
+ $stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ?, group_id = ?, status = ? WHERE id = ?");
+ $stmt->execute([$username, $email, $phone, $group_id, $status, $id]);
+
+ if (!empty($_POST['password'])) {
+ $hashed_password = password_hash($_POST['password'], PASSWORD_DEFAULT);
+ $stmt = db()->prepare("UPDATE users SET password = ? WHERE id = ?");
+ $stmt->execute([$hashed_password, $id]);
+ }
+ $message = "User updated successfully!";
+ }
+ }
+ if (isset($_POST['delete_user'])) {
+ $id = (int)$_POST['id'];
+ if ($id) {
+ $stmt = db()->prepare("DELETE FROM users WHERE id = ?");
+ $stmt->execute([$id]);
+ $message = "User deleted successfully!";
+ }
+ }
+
+ // --- POS Devices Handlers ---
+ if (isset($_POST['add_pos_device'])) {
+ $name = $_POST['device_name'] ?? '';
+ $type = $_POST['device_type'] ?? 'scale';
+ $conn = $_POST['connection_type'] ?? 'usb';
+ $ip = $_POST['ip_address'] ?? '';
+ $port = $_POST['port'] ? (int)$_POST['port'] : null;
+ $baud = $_POST['baud_rate'] ? (int)$_POST['baud_rate'] : null;
+ if ($name) {
+ $stmt = db()->prepare("INSERT INTO pos_devices (device_name, device_type, connection_type, ip_address, port, baud_rate) VALUES (?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$name, $type, $conn, $ip, $port, $baud]);
+ redirectWithMessage("Device added successfully!", "index.php?page=scale_devices");
+ }
+ }
+ if (isset($_POST['edit_pos_device'])) {
+ $id = (int)$_POST['id'];
+ $name = $_POST['device_name'] ?? '';
+ $type = $_POST['device_type'] ?? 'scale';
+ $conn = $_POST['connection_type'] ?? 'usb';
+ $ip = $_POST['ip_address'] ?? '';
+ $port = $_POST['port'] ? (int)$_POST['port'] : null;
+ $baud = $_POST['baud_rate'] ? (int)$_POST['baud_rate'] : null;
+ $status = $_POST['status'] ?? 'active';
+ if ($id && $name) {
+ $stmt = db()->prepare("UPDATE pos_devices SET device_name = ?, device_type = ?, connection_type = ?, ip_address = ?, port = ?, baud_rate = ?, status = ? WHERE id = ?");
+ $stmt->execute([$name, $type, $conn, $ip, $port, $baud, $status, $id]);
+ redirectWithMessage("Device updated successfully!", "index.php?page=scale_devices");
+ }
+ }
+ if (isset($_POST['delete_pos_device'])) {
+ $id = (int)$_POST['id'];
+ if ($id) {
+ $stmt = db()->prepare("DELETE FROM pos_devices WHERE id = ?");
+ $stmt->execute([$id]);
+ redirectWithMessage("Device deleted successfully!", "index.php?page=scale_devices");
+ }
+ }
+
+ if (isset($_POST['update_profile'])) {
+ $id = $_SESSION['user_id'];
+ $username = $_POST['username'] ?? '';
+ $email = $_POST['email'] ?? '';
+ $phone = $_POST['phone'] ?? '';
+
+ if ($id && $username) {
+ $stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ? WHERE id = ?");
+ $stmt->execute([$username, $email, $phone, $id]);
+ $_SESSION['username'] = $username;
+
+ if (!empty($_POST['password'])) {
+ $hashed_password = password_hash($_POST['password'], PASSWORD_DEFAULT);
+ $stmt = db()->prepare("UPDATE users SET password = ? WHERE id = ?");
+ $stmt->execute([$hashed_password, $id]);
+ }
+
+ if (isset($_FILES['profile_pic']) && $_FILES['profile_pic']['error'] === 0) {
+ $ext = pathinfo($_FILES['profile_pic']['name'], PATHINFO_EXTENSION);
+ $filename = 'uploads/profile_' . $id . '_' . time() . '.' . $ext;
+ if (!is_dir('uploads')) mkdir('uploads', 0777, true);
+ if (move_uploaded_file($_FILES['profile_pic']['tmp_name'], $filename)) {
+ $stmt = db()->prepare("UPDATE users SET profile_pic = ? WHERE id = ?");
+ $stmt->execute([$filename, $id]);
+ $_SESSION['profile_pic'] = $filename;
+ }
+ }
+ redirectWithMessage("Profile updated successfully!", "index.php?page=my_profile");
+ }
+ }
+
+ if (isset($_POST['update_settings'])) {
+ if (can('settings_view')) {
+ $db = db();
+ if (isset($_POST['settings']) && is_array($_POST['settings'])) {
+ foreach ($_POST['settings'] as $key => $value) {
+ $stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?");
+ $stmt->execute([$key, $value, $value]);
+ }
+ }
+
+ // Handle file uploads
+ $files = ['company_logo', 'favicon', 'manager_signature', 'display_slide_1', 'display_slide_2', 'display_slide_3'];
+ foreach ($files as $file_key) {
+ if (isset($_FILES[$file_key]) && $_FILES[$file_key]['error'] === 0) {
+ $ext = pathinfo($_FILES[$file_key]['name'], PATHINFO_EXTENSION);
+ $filename = 'uploads/' . $file_key . '_' . time() . '.' . $ext;
+ if (!is_dir('uploads')) mkdir('uploads', 0777, true);
+ if (move_uploaded_file($_FILES[$file_key]['tmp_name'], $filename)) {
+ $stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?");
+ $stmt->execute([$file_key, $filename, $filename]);
+ }
+ }
+ }
+ redirectWithMessage("Settings updated successfully!", "index.php?page=settings");
+ }
+ }
+
+ // --- Backup Handlers ---
+ if (isset($_POST['create_backup'])) {
+ if (can('users_view')) { // Admin check
+ $res = BackupService::createBackup();
+ $message = $res['success'] ? "Backup created: " . $res['file'] : "Error: " . $res['error'];
+ }
+ }
+
+ if (isset($_POST['restore_backup'])) {
+ if (can('users_view')) {
+ $filename = $_POST['filename'] ?? '';
+ $res = BackupService::restoreBackup($filename);
+ redirectWithMessage($res['success'] ? "Database restored successfully from $filename!" : "Error: " . $res['error'], "index.php?page=backups");
+ }
+ }
+
+ if (isset($_POST['delete_backup'])) {
+ if (can('users_view')) {
+ $filename = basename($_POST['filename'] ?? '');
+ if (unlink(__DIR__ . '/backups/' . $filename)) {
+ redirectWithMessage("Backup deleted successfully.", "index.php?page=backups");
+ } else {
+ redirectWithMessage("Error deleting backup.", "index.php?page=backups");
+ }
+ }
+ }
+
+ if (isset($_POST['save_backup_settings'])) {
+ $limit = (int)($_POST['backup_limit'] ?? 5);
+ $auto = $_POST['backup_auto_enabled'] ?? '0';
+ $time = $_POST['backup_time'] ?? '00:00';
+
+ $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]);
+ redirectWithMessage("Backup settings updated.", "index.php?page=backups");
+ }
+
+ 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;
+ }
+ }
+
+ // --- Cash Register & Session Handlers ---
+ if (isset($_POST['add_cash_register'])) {
+ $name = $_POST['name'] ?? '';
+ if ($name) {
+ // Check license limit
+ $allowed = LicenseService::getAllowedActivations();
+ $stmt = db()->query("SELECT COUNT(*) FROM cash_registers");
+ $current_count = (int)$stmt->fetchColumn();
+
+ if ($current_count >= $allowed) {
+ $message = "Error: Activation Limit Reached. Your license only allows $allowed register(s).";
+ } else {
+ $stmt = db()->prepare("INSERT INTO cash_registers (name) VALUES (?)");
+ $stmt->execute([$name]);
+ redirectWithMessage("Cash Register added successfully!", "index.php?page=cash_registers");
+ }
+ }
+ }
+ if (isset($_POST['edit_cash_register'])) {
+ $id = (int)$_POST['id'];
+ $name = $_POST['name'] ?? '';
+ $status = $_POST['status'] ?? 'active';
+ if ($id && $name) {
+ $stmt = db()->prepare("UPDATE cash_registers SET name = ?, status = ? WHERE id = ?");
+ $stmt->execute([$name, $status, $id]);
+ redirectWithMessage("Cash Register updated successfully!", "index.php?page=cash_registers");
+ }
+ }
+ if (isset($_POST['delete_cash_register'])) {
+ $id = (int)$_POST['id'];
+ if ($id) {
+ $stmt = db()->prepare("DELETE FROM cash_registers WHERE id = ?");
+ $stmt->execute([$id]);
+ redirectWithMessage("Cash Register deleted successfully!", "index.php?page=cash_registers");
+ }
+ }
+
+ if (isset($_POST['open_register'])) {
+ $register_id = (int)$_POST['register_id'];
+ $user_id = $_SESSION['user_id'];
+ $opening_balance = (float)$_POST['opening_balance'];
+
+ // Check if user already has an open session
+ $check = db()->prepare("SELECT id FROM register_sessions WHERE user_id = ? AND status = 'open'");
+ $check->execute([$user_id]);
+ if ($check->fetch()) {
+ $message = "Error: You already have an open register session.";
+ } else {
+ $stmt = db()->prepare("INSERT INTO register_sessions (register_id, user_id, opening_balance, status) VALUES (?, ?, ?, 'open')");
+ $stmt->execute([$register_id, $user_id, $opening_balance]);
+ $_SESSION['register_session_id'] = db()->lastInsertId();
+ redirectWithMessage("Register opened successfully!", "index.php?page=pos");
+ }
+ }
+
+ if (isset($_POST['close_register'])) {
+ $session_id = $_SESSION['register_session_id'] ?? null;
+ $closing_balance = (float)$_POST['closing_balance'];
+ $notes = $_POST['notes'] ?? '';
+
+ if ($session_id) {
+ $stmt = db()->prepare("UPDATE register_sessions SET closing_balance = ?, closed_at = NOW(), status = 'closed', notes = ? WHERE id = ?");
+ $stmt->execute([$closing_balance, $notes, $session_id]);
+ unset($_SESSION['register_session_id']);
+ redirectWithMessage("Register closed successfully!", "index.php?page=pos");
+ }
+ }
+
+ if (isset($_POST['delete_backup'])) {
+ if (can('users_view')) {
+ $filename = basename($_POST['filename'] ?? '');
+ if (unlink(__DIR__ . '/backups/' . $filename)) {
+ $message = "Backup deleted successfully.";
+ } else {
+ $message = "Error deleting backup.";
+ }
+ }
+ }
+
+ if (isset($_POST['save_backup_settings'])) {
+ $limit = (int)($_POST['backup_limit'] ?? 5);
+ $auto = $_POST['backup_auto_enabled'] ?? '0';
+ $time = $_POST['backup_time'] ?? '00:00';
+
+ $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 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;
+ }
+ }
+
+ // --- Cash Register & Session Handlers ---
+ if (isset($_POST['add_cash_register'])) {
+ $name = $_POST['name'] ?? '';
+ if ($name) {
+ // Check license limit
+ $allowed = LicenseService::getAllowedActivations();
+ $stmt = db()->query("SELECT COUNT(*) FROM cash_registers");
+ $current_count = (int)$stmt->fetchColumn();
+
+ if ($current_count >= $allowed) {
+ $message = "Error: Activation Limit Reached. Your license only allows $allowed register(s).";
+ } else {
+ $stmt = db()->prepare("INSERT INTO cash_registers (name) VALUES (?)");
+ $stmt->execute([$name]);
+ $message = "Cash Register added successfully!";
+ }
+ }
+ }
+ if (isset($_POST['edit_cash_register'])) {
+ $id = (int)$_POST['id'];
+ $name = $_POST['name'] ?? '';
+ $status = $_POST['status'] ?? 'active';
+ if ($id && $name) {
+ $stmt = db()->prepare("UPDATE cash_registers SET name = ?, status = ? WHERE id = ?");
+ $stmt->execute([$name, $status, $id]);
+ $message = "Cash Register updated successfully!";
+ }
+ }
+ if (isset($_POST['delete_cash_register'])) {
+ $id = (int)$_POST['id'];
+ if ($id) {
+ $stmt = db()->prepare("DELETE FROM cash_registers WHERE id = ?");
+ $stmt->execute([$id]);
+ $message = "Cash Register deleted successfully!";
+ }
+ }
+
+ if (isset($_POST['open_register'])) {
+ $register_id = (int)$_POST['register_id'];
+ $user_id = $_SESSION['user_id'];
+ $opening_balance = (float)$_POST['opening_balance'];
+
+ // Check if user already has an open session
+ $check = db()->prepare("SELECT id FROM register_sessions WHERE user_id = ? AND status = 'open'");
+ $check->execute([$user_id]);
+ if ($check->fetch()) {
+ $message = "Error: You already have an open register session.";
+ } else {
+ $stmt = db()->prepare("INSERT INTO register_sessions (register_id, user_id, opening_balance, status) VALUES (?, ?, ?, 'open')");
+ $stmt->execute([$register_id, $user_id, $opening_balance]);
+ $_SESSION['register_session_id'] = db()->lastInsertId();
+ $message = "Register opened successfully!";
+ header("Location: index.php?page=pos");
+ exit;
+ }
+ }
+
+ if (isset($_POST['close_register'])) {
+ $session_id = (int)$_POST['session_id'];
+ $cash_in_hand = (float)$_POST['cash_in_hand'];
+ $notes = $_POST['notes'] ?? '';
+
+ // Calculate expected closing balance
+ // Opening + Sum of POS Transactions (Cash) - Any cash outflows (if any)
+ $session = db()->prepare("SELECT opening_balance FROM register_sessions WHERE id = ?");
+ $session->execute([$session_id]);
+ $opening = (float)$session->fetchColumn();
+
+ $sales = db()->prepare("SELECT SUM(p.amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1 AND p.payment_method = 'cash'");
+ $sales->execute([$session_id]);
+ $cash_sales = (float)$sales->fetchColumn();
+
+ $expected = $opening + $cash_sales;
+
+ $stmt = db()->prepare("UPDATE register_sessions SET closing_balance = ?, cash_in_hand = ?, closed_at = CURRENT_TIMESTAMP, status = 'closed', notes = ? WHERE id = ?");
+ $stmt->execute([$expected, $cash_in_hand, $notes, $session_id]);
+
+ unset($_SESSION['register_session_id']);
+ $message = "Register closed successfully!";
+ header("Location: index.php?page=dashboard");
+ exit;
+ }
+
+
+// Routing & Data Fetching
+$page = $_GET['page'] ?? 'dashboard';
+
+// Permission map for pages
+$page_permissions = [
+ 'pos' => 'pos_view',
+ 'sales' => 'sales_view',
+ 'sales_returns' => 'sales_returns_view',
+ 'purchases' => 'purchases_view',
+ 'purchase_returns' => 'purchase_returns_view',
+ 'quotations' => 'quotations_view',
+ 'lpos' => 'lpos_view',
+ 'accounting' => 'accounting_view',
+ 'expense_categories' => 'expense_categories_view',
+ 'expenses' => 'expenses_view',
+ 'expense_report' => 'expenses_view',
+ 'items' => 'items_view',
+ 'categories' => 'categories_view',
+ 'units' => 'units_view',
+ 'customers' => 'customers_view',
+ 'suppliers' => 'suppliers_view',
+ 'customer_statement' => 'customer_statement_view',
+ 'supplier_statement' => 'supplier_statement_view',
+ 'cashflow_report' => 'cashflow_report_view',
+ 'expiry_report' => 'expiry_report_view',
+ 'low_stock_report' => 'low_stock_report_view',
+ 'loyalty_history' => 'loyalty_history_view',
+ 'payment_methods' => 'payment_methods_view',
+ 'settings' => 'settings_view',
+ 'devices' => 'devices_view',
+ 'hr_departments' => 'hr_departments_view',
+ 'hr_employees' => 'hr_employees_view',
+ 'hr_attendance' => 'hr_attendance_view',
+ 'hr_payroll' => 'hr_payroll_view',
+ 'role_groups' => 'role_groups_view',
+ 'users' => 'users_view',
+ 'scale_devices' => 'scale_devices_view',
+ 'customer_display_settings' => 'customer_display_settings_view',
+ 'backups' => 'backups_view',
+ 'logs' => 'logs_view',
+ 'cash_registers' => 'cash_registers_view',
+ 'register_sessions' => 'register_sessions_view',
+];
+
+if (isset($page_permissions[$page]) && !can($page_permissions[$page])) {
+ $page = 'dashboard';
+ $message = "Access Denied: You don't have permission to view that module.";
+}
+
+$currTitle = ['en' => 'Dashboard', 'ar' => 'لوحة القيادة'];
+$titles = [
+ 'dashboard' => ['en' => 'Dashboard', 'ar' => 'لوحة القيادة'],
+ 'pos' => ['en' => 'Point of Sale', 'ar' => 'نقطة البيع'],
+ 'sales' => ['en' => 'Sales', 'ar' => 'المبيعات'],
+ 'sales_returns' => ['en' => 'Sales Returns', 'ar' => 'مرتجعات المبيعات'],
+ 'purchases' => ['en' => 'Purchases', 'ar' => 'المشتريات'],
+ 'purchase_returns' => ['en' => 'Purchase Returns', 'ar' => 'مرتجعات المشتريات'],
+ 'quotations' => ['en' => 'Quotations', 'ar' => 'عروض الأسعار'],
+ 'lpos' => ['en' => 'LPOs', 'ar' => 'أوامر الشراء'],
+ 'accounting' => ['en' => 'Accounting', 'ar' => 'المحاسبة'],
+ 'expense_categories' => ['en' => 'Expense Categories', 'ar' => 'فئات المصروفات'],
+ 'expenses' => ['en' => 'Expenses', 'ar' => 'المصروفات'],
+ 'expense_report' => ['en' => 'Expense Report', 'ar' => 'تقرير المصروفات'],
+ 'items' => ['en' => 'Items', 'ar' => 'الأصناف'],
+ 'categories' => ['en' => 'Categories', 'ar' => 'الفئات'],
+ 'units' => ['en' => 'Units', 'ar' => 'الوحدات'],
+ 'customers' => ['en' => 'Customers', 'ar' => 'العملاء'],
+ 'suppliers' => ['en' => 'Suppliers', 'ar' => 'الموردين'],
+ 'customer_statement' => ['en' => 'Customer Statement', 'ar' => 'كشف حساب عميل'],
+ 'supplier_statement' => ['en' => 'Supplier Statement', 'ar' => 'كشف حساب مورد'],
+ 'cashflow_report' => ['en' => 'Cashflow Report', 'ar' => 'تقرير التدفق النقدي'],
+ 'expiry_report' => ['en' => 'Expiry Report', 'ar' => 'تقرير الصلاحية'],
+ 'low_stock_report' => ['en' => 'Low Stock Report', 'ar' => 'تقرير المخزون المنخفض'],
+ 'loyalty_history' => ['en' => 'Loyalty History', 'ar' => 'سجل الولاء'],
+ 'payment_methods' => ['en' => 'Payment Methods', 'ar' => 'طرق الدفع'],
+ 'settings' => ['en' => 'Settings', 'ar' => 'الإعدادات'],
+ 'devices' => ['en' => 'Biometric Devices', 'ar' => 'أجهزة البصمة'],
+ 'hr_departments' => ['en' => 'Departments', 'ar' => 'الأقسام'],
+ 'hr_employees' => ['en' => 'Employees', 'ar' => 'الموظفين'],
+ 'hr_attendance' => ['en' => 'Attendance', 'ar' => 'الحضور'],
+ 'hr_payroll' => ['en' => 'Payroll', 'ar' => 'الرواتب'],
+ 'role_groups' => ['en' => 'Roles & Permissions', 'ar' => 'الأدوار والصلاحيات'],
+ 'users' => ['en' => 'Users', 'ar' => 'المستخدمين'],
+ 'cash_registers' => ['en' => 'Cash Registers', 'ar' => 'صناديق الكاشير'],
+ 'register_sessions' => ['en' => 'Register Sessions', 'ar' => 'جلسات الصناديق']
+];
+if (isset($titles[$page])) {
+ $currTitle = $titles[$page];
+}
+
+$data = [
+ 'payment_methods' => [],
+ 'role_groups' => [],
+ 'users' => [],
+ 'expiry_items' => [],
+ 'low_stock_items' => [],
+ 'items' => [],
+ 'cash_transactions' => [],
+ 'monthly_sales' => [],
+ 'yearly_sales' => [],
+ 'opening_balance' => 0,
+ 'stats' => [
+ 'expired_items' => 0,
+ 'near_expiry_items' => 0,
+ 'low_stock_items_count' => 0,
+ 'total_sales' => 0,
+ 'total_received' => 0,
+ 'total_receivable' => 0,
+ 'total_purchases' => 0,
+ ],
+ 'settings' => [],
+];
+
+$permission_groups = [
+ 'General' => ['dashboard' => __('dashboard')],
+ 'Inventory' => [
+ 'items' => __('items'),
+ 'categories' => __('categories'),
+ 'units' => __('units')
+ ],
+ 'Customers' => [
+ 'customers' => __('customers')
+ ],
+ 'Suppliers' => [
+ 'suppliers' => __('suppliers')
+ ],
+ 'POS' => [
+ 'pos' => __('pos')
+ ],
+ 'Sales' => [
+ 'sales' => __('sales'),
+ 'sales_returns' => __('sales_returns'),
+ 'quotations' => __('quotations')
+ ],
+ 'Purchases' => [
+ 'purchases' => __('purchases'),
+ 'lpos' => __('lpos'),
+ 'purchase_returns' => __('purchase_returns')
+ ],
+ 'Expenses' => [
+ 'expense_categories' => __('expense_categories'),
+ 'expenses' => __('expenses')
+ ],
+ 'Accounting' => [
+ 'accounting' => __('accounting'),
+ 'trial_balance' => __('trial_balance'),
+ 'profit_loss' => __('profit_loss'),
+ 'balance_sheet' => __('balance_sheet'),
+ 'vat_report' => __('vat_report')
+ ],
+ 'HR' => [
+ 'hr_departments' => __('departments'),
+ 'hr_employees' => __('employees'),
+ 'hr_attendance' => __('attendance'),
+ 'hr_payroll' => __('payroll')
+ ],
+ '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'),
+ 'loyalty_history' => __('loyalty_history')
+ ],
+ 'Settings' => [
+ 'payment_methods' => __('payment_methods'),
+ 'devices' => __('devices'),
+ 'settings' => __('settings')
+ ],
+ 'Administration' => [
+ 'role_groups' => __('role_groups'),
+ 'users' => __('users'),
+ 'cash_registers' => __('cash_registers'),
+ 'register_sessions' => __('register_sessions'),
+ 'logs' => 'System Logs'
+ ]
+];
+
+
+if ($page === 'export') {
+ $type = $_GET['type'] ?? 'sales';
+ $format = $_GET['format'] ?? 'csv';
+ $filename = $type . "_export_" . date('Y-m-d') . ($format === 'excel' ? ".xls" : ".csv");
+
+ 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') {
+ $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);
+ 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_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 $table v LEFT JOIN $cust_table c ON v.$cust_col = c.id
+ WHERE $whereSql ORDER BY v.id DESC");
+ $stmt->execute($params);
+ $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"];
+ $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);
+ $stmt = db()->prepare("SELECT id, name, email, phone, tax_id, balance, created_at FROM $table WHERE $whereSql ORDER BY id DESC");
+ $stmt->execute($params);
+ $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 = [];
+ 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);
+ $stmt = db()->prepare("SELECT i.sku, i.name_en, i.name_ar, c.name_en as category, i.purchase_price, i.sale_price, i.stock_quantity, i.vat_rate
+ 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);
+ $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 "" . htmlspecialchars($h) . " ";
+ echo " ";
+ foreach ($rows as $row) {
+ echo "";
+ foreach ($row as $val) echo "" . htmlspecialchars((string)$val) . " ";
+ echo " ";
+ }
+ echo "
";
+ } 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): ?>
+
+
+ = $lang === 'ar' ? "نسخة تجريبية: متبقي $trial_days يوم" : "Trial Version: $trial_days days remaining" ?>.
+
= __('activate_now') ?>
+
+
+
+
+
+
+
+
+
+
+
+
= __($page) ?>
+
+
+
+
+
+
+
+ = count($purchaseAlerts) ?>
+
+
+
+
+
= $lang === 'ar' ? 'تنبيهات المدفوعات' : 'Payment Alerts' ?>
+
+
+
+
+
+
+
+ = $lang === 'ar' ? 'المظهر' : 'Theme' ?>
+
+
+
+
+ = $lang === 'ar' ? 'English' : 'العربية' ?>
+
+
+
+
= htmlspecialchars((string)($_SESSION['user_role_name'] ?? '')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 || $data['stats']['near_expiry_items'] > 0 || $data['stats']['low_stock_items_count'] > 0 || $purchaseAlertsCount > 0): ?>
+
+
+
+
+
+ Administrative Alerts:
+ 0): ?>
+ = $data['stats']['expired_items'] ?> items have expired.
+
+ 0): ?>
+ = $data['stats']['near_expiry_items'] ?> items are expiring soon.
+
+ 0): ?>
+ = $data['stats']['low_stock_items_count'] ?> items are below minimum level.
+
+ 0): ?>
+ = $purchaseAlertsCount ?> purchase invoices are due or overdue.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= __('total_sales') ?>
+
OMR = number_format((float)($data['stats']['total_sales'] ?? 0), 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
= __('total_received') ?>
+
OMR = number_format((float)($data['stats']['total_received'] ?? 0), 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
= __('customer_due') ?>
+
OMR = number_format((float)($data['stats']['total_receivable'] ?? 0), 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
= __('total_purchases') ?>
+
OMR = number_format((float)($data['stats']['total_purchases'] ?? 0), 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= __('total_paid') ?>
+
OMR = number_format((float)($data['stats']['total_paid'] ?? 0), 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
= __('supplier_due') ?>
+
OMR = number_format((float)($data['stats']['total_payable'] ?? 0), 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
= __('total_customers') ?>
+
= (int)($data['stats']['total_customers'] ?? 0) ?>
+
+
+
+
+
+
+
+
+
+
+
+
Total Items
+
= (int)($data['stats']['total_items'] ?? 0) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sales Performance
+
+ Monthly
+ Yearly
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Recent Customers
+
+ View All
+
+
+
+
+
+
+ Name
+ Phone
+ Balance
+
+
+
+
+
+ = htmlspecialchars((string)($c['name'] ?? '')) ?>
+ = htmlspecialchars((string)($c['phone'] ?? '')) ?>
+ = $lang === 'ar' ? number_format((float)$c['balance'], 3) . ' ر.ع.' : 'OMR ' . number_format((float)$c['balance'], 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($data['settings']['company_name'] ?? 'Company Name') ?>
+
+
+
+
+
+
= $currTitle['en'] ?> Management
+
+
+
+
+
+
+
+
+
+ Name
+ Tax ID
+ Email
+ Phone
+ Balance
+ Actions
+
+
+
+
+
+ = htmlspecialchars((string)($c['name'] ?? '')) ?>
+ = htmlspecialchars((string)($c['tax_id'] ?? '---')) ?>
+ = htmlspecialchars((string)($c['email'] ?? '')) ?>
+ = htmlspecialchars((string)($c['phone'] ?? '')) ?>
+ = $lang === 'ar' ? number_format((float)$c['balance'], 3) . ' ر.ع.' : 'OMR ' . number_format((float)$c['balance'], 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
Stock Categories Management
+
+
+
+ Export
+
+
+ Import Excel
+
+
+ Add Category
+
+
+
+
+
+
+
+
+ ID
+ Name (EN)
+ Name (AR)
+ Actions
+
+
+
+
+
+ = $cat['id'] ?>
+ = htmlspecialchars($cat['name_en']) ?>
+ = htmlspecialchars($cat['name_ar']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
Stock Units Management
+
+
+
+ Export
+
+
+ Import Excel
+
+
+ Add Unit
+
+
+
+
+
+
+
+
+ Name (EN)
+ Short (EN)
+ Name (AR)
+ Short (AR)
+ Actions
+
+
+
+
+
+ = htmlspecialchars($u['name_en']) ?>
+ = htmlspecialchars($u['short_name_en']) ?>
+ = htmlspecialchars($u['name_ar']) ?>
+ = htmlspecialchars($u['short_name_ar']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
Stock Items (= count($data['items'] ?? []) ?>)
+
+
+
+
+
+
+
+
+
+
+ Image
+ SKU
+ Name
+ Category
+ Supplier
+ Stock Level
+ Expiry
+ VAT
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($item['sku']) ?>
+
+
+ = htmlspecialchars((string)($item['name_en'] ?? '')) ?>
+
+ Promo
+
+
+ = htmlspecialchars((string)($item['name_ar'] ?? '')) ?>
+
+ = htmlspecialchars((string)($item['cat_en'] ?? '')) ?>
+ = htmlspecialchars((string)($item['supplier_name'] ?? '---')) ?>
+
+
+
= number_format((float)$item['stock_quantity'], 3) ?>
+
Min: = number_format((float)$item['min_stock_level'], 3) ?>
+
+
Low Stock
+
+
+
+ = !empty($item['expiry_date']) ? htmlspecialchars((string)$item['expiry_date']) : '---' ?>
+ = number_format((float)$item['vat_rate'], 2) ?>%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SKU = htmlspecialchars((string)($item['sku'] ?? '')) ?>
+ Category = htmlspecialchars($item['cat_en'] ?? '---') ?>
+ Supplier = htmlspecialchars($item['supplier_name'] ?? '---') ?>
+ Sale Price OMR = number_format((float)$item['sale_price'], 3) ?>
+ Stock Level = number_format((float)$item['stock_quantity'], 3) ?>
+ VAT Rate = number_format((float)$item['vat_rate'], 2) ?>%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
+
+ SKU
+ Item Name
+ Category
+ Stock Level
+ Expiry Date
+ Status
+
+
+
+
+
+ No items found.
+
+
+
+
+
+ = htmlspecialchars($item['sku']) ?>
+
+ = htmlspecialchars($item['name_en']) ?>
+ = htmlspecialchars($item['name_ar']) ?>
+
+ = htmlspecialchars($item['cat_en'] ?? '---') ?>
+ = number_format((float)$item['stock_quantity'], 3) ?>
+ = $expiry_date !== '' ? htmlspecialchars((string)$expiry_date) : '---' ?>
+
+
+ Expired
+
+ Near Expiry
+
+ Good
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
Low Stock Report
+
+ Print
+
+
+
+
+
+
+ SKU
+ Item Name
+ Category
+ Supplier
+ Min Level
+ Current Stock
+ Shortage
+
+
+
+
+
+ All items are above minimum levels.
+
+
+
+
+
+ = htmlspecialchars($item['sku']) ?>
+
+ = htmlspecialchars($item['name_en']) ?>
+ = htmlspecialchars($item['name_ar']) ?>
+
+ = htmlspecialchars($item['cat_en'] ?? '---') ?>
+ = htmlspecialchars($item['supplier_name'] ?? '---') ?>
+ = number_format((float)$item['min_stock_level'], 2) ?>
+
+
+ = number_format((float)$item['stock_quantity'], 3) ?>
+
+
+ = number_format($shortage, 3) ?>
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
Loyalty Transaction History
+
+ Print
+
+
+
+
+
+
+
+ Date
+ Customer
+ Tier
+ Type
+ Points
+ Description
+
+
+
+
+ No transactions found.
+
+
+
+ = date('Y-m-d H:i', strtotime($lt['created_at'])) ?>
+
+ = htmlspecialchars($lt['customer_name']) ?>
+ Current Balance: = number_format($lt['loyalty_points'], 0) ?> pts
+
+
+
+ = $tier ?>
+
+
+
+ = ucfirst($type) ?>
+
+
+ = (float)$lt['points_change'] > 0 ? '+' : '' ?>= number_format($lt['points_change'], 0) ?>
+
+ = htmlspecialchars($lt['description']) ?>
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+ prepare("SELECT * FROM register_sessions WHERE user_id = ? AND status = 'open'");
+ $stmt->execute([$_SESSION['user_id']]);
+ $active_session = $stmt->fetch(PDO::FETCH_ASSOC);
+ $_SESSION['register_session_id'] = $active_session['id'] ?? null;
+
+ $registers = db()->query("SELECT * FROM cash_registers WHERE status = 'active'")->fetchAll();
+
+ $allow_zero_stock_sell = ($data['settings']['allow_zero_stock_sell'] ?? '1') === '1';
+ $sql = "SELECT * FROM stock_items ORDER BY name_en ASC";
+ $products_raw = db()->query($sql)->fetchAll(PDO::FETCH_ASSOC);
+ $products = [];
+ foreach ($products_raw as $p) {
+ $p['original_price'] = (float)$p['sale_price'];
+ $p['sale_price'] = getPromotionalPrice($p);
+ $products[] = $p;
+ }
+ $customers = db()->query("SELECT * FROM customers ORDER BY name ASC")->fetchAll(PDO::FETCH_ASSOC);
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Held List
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($p['name_en']) ?>
+
= htmlspecialchars($p['sku']) ?>
+
+
+
+ OMR = number_format($p['original_price'], 3) ?>
+
+ OMR = number_format((float)$p['sale_price'], 3) ?>
+
+
= (float)$p['stock_quantity'] ?> left
+
+
+
+
+
+
+
+
+
Cart
+
+ Customer Screen
+
+ Close
+
+
+
+
+
+
+
+
+
+
Customer
+
+
+ Walk-in Customer
+
+
+ = htmlspecialchars($c['name']) ?>
+
+
+
+
+
+
+
+
+ Bronze
+ 0 pts
+
+
+
+ Redeem
+
+
+
+
Spend more to unlock Silver
+
+
+
+
Discount Code
+
+
+ Apply
+
+
+
+
+
+
+
+
+
+ Subtotal (Excl. VAT)
+ = __('currency') ?> 0.000
+
+
+ VAT
+ = __('currency') ?> 0.000
+
+
+ Total
+ = __('currency') ?> 0.000
+
+
+ PLACE ORDER
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Quotations
+
+ Create New Quotation
+
+
+
+
+
+
+
+
+ Search
+
+
+
+ Customer
+
+ All
+
+ >= htmlspecialchars($c['name']) ?>
+
+
+
+
+ Start Date
+
+
+
+ End Date
+
+
+
+
+ Filter
+
+
+
+ Export
+
+
+
+
+ Clear
+
+
+
+
+
+ Limit
+
+ >20
+ >40
+ >60
+ >100
+
+
+
+
+
+
+
+
+
+ Quotation #
+ Date
+ Valid Until
+ Customer
+ Status
+ Total
+ Actions
+
+
+
+ prepare("SELECT qi.*, i.name_en, i.name_ar, i.vat_rate
+ FROM quotation_items qi
+ JOIN stock_items i ON qi.item_id = i.id
+ WHERE qi.quotation_id = ?");
+ $items->execute([$q['id']]);
+ $q['items'] = $items->fetchAll(PDO::FETCH_ASSOC);
+ ?>
+
+ QUO-= str_pad((string)$q['id'], 5, '0', STR_PAD_LEFT) ?>
+ = $q['quotation_date'] ?>
+ = $q['valid_until'] ?: '---' ?>
+ = htmlspecialchars($q['customer_name'] ?? '---') ?>
+
+
+ = htmlspecialchars($q['status']) ?>
+
+ OMR = number_format((float)$q['total_with_vat'], 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No quotations found
+
+
+
+
+
+
+
+
+
+
+
Local Purchase Orders (LPO)
+
+ Create New LPO
+
+
+
+
+
+
+
+
+ Search
+
+
+
+ Supplier
+
+ All
+
+ >= htmlspecialchars($s['name']) ?>
+
+
+
+
+ Start Date
+
+
+
+ End Date
+
+
+
+
+ Filter
+
+
+
+ Export
+
+
+
+
+ Clear
+
+
+
+
+
+ Limit
+
+ >20
+ >40
+ >60
+ >100
+
+
+
+
+
+
+
+
+
+ LPO #
+ Date
+ Delivery Date
+ Supplier
+ Status
+ Total
+ Actions
+
+
+
+ prepare("SELECT li.*, i.name_en, i.name_ar, i.vat_rate
+ FROM lpo_items li
+ JOIN stock_items i ON li.item_id = i.id
+ WHERE li.lpo_id = ?");
+ $items->execute([$q['id']]);
+ $q['items'] = $items->fetchAll(PDO::FETCH_ASSOC);
+ ?>
+
+ LPO-= str_pad((string)$q['id'], 5, '0', STR_PAD_LEFT) ?>
+ = $q['lpo_date'] ?>
+ = $q['delivery_date'] ?: '---' ?>
+ = htmlspecialchars($q['supplier_name'] ?? '---') ?>
+
+
+ = htmlspecialchars($q['status']) ?>
+
+ OMR = number_format((float)$q['total_with_vat'], 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No LPOs found
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
+
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
+
VAT: = htmlspecialchars($data['settings']['vat_number'] ?? '') ?>
+
+
+
= $currTitle['en'] ?> Report
+
Date: = date('Y-m-d') ?>
+
+
+
+ = $page === 'sales' ? 'Customer' : 'Supplier' ?>:
+ |
+
+ Period: = !empty($_GET['start_date']) ? $_GET['start_date'] : 'All' ?> to = !empty($_GET['end_date']) ? $_GET['end_date'] : 'All' ?>
+
+
+
+
+
+
+
+
+
= $currTitle['en'] ?>
+
+
+
+
+
+
+
+
+ Search
+
+
+
+ = $page === 'sales' ? 'Customer' : 'Supplier' ?>
+
+ All
+
+ >= htmlspecialchars($c['name']) ?>
+
+
+
+
+ Start Date
+
+
+
+ End Date
+
+
+
+
+ Filter
+
+
+
+ Export
+
+
+
+
+ Print
+
+
+ Clear
+
+
+
+
+
+ Limit
+
+ >20
+ >40
+ >60
+ >100
+
+
+
+
+
+
+
+
+
+ Invoice #
+ Date
+ Due Date
+ = $page === 'sales' ? 'Customer' : 'Supplier' ?>
+ Status
+ Total
+ Paid
+ Balance
+ Actions
+
+
+
+ prepare("SELECT ii.*, i.name_en, i.name_ar, i.vat_rate
+ FROM $itemTable ii
+ JOIN stock_items i ON ii.item_id = i.id
+ WHERE ii.$fkCol = ?");
+ $items->execute([$inv['id']]);
+ $inv['items'] = $items->fetchAll(PDO::FETCH_ASSOC);
+ $prefix = ($page === 'purchases') ? 'PUR' : 'INV';
+ ?>
+
+ = !empty($inv['is_pos']) && !empty($inv['transaction_no']) ? htmlspecialchars($inv['transaction_no']) : $prefix . '-' . str_pad((string)$inv['id'], 5, '0', STR_PAD_LEFT) ?>
+ = $inv['invoice_date'] ?>
+
+
+
+
+ = $inv['due_date'] ?>
+
+
+
+
+
+ ---
+
+
+ = htmlspecialchars($inv['customer_name'] ?? '---') ?>
+
+
+ = htmlspecialchars(str_replace('_', ' ', $inv['status'])) ?>
+
+ OMR = number_format((float)$inv['total_with_vat'], 3) ?>
+ OMR = number_format((float)$inv['paid_amount'], 3) ?>
+ OMR = number_format((float)($inv['total_with_vat'] - $inv['paid_amount']), 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Totals
+ OMR = number_format($total_all, 3) ?>
+ OMR = number_format($total_paid, 3) ?>
+ OMR = number_format($total_balance, 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= $lang === 'ar' ? $page_title_ar : $page_title_en ?>
+
+ = $lang === 'ar' ? 'طباعة' : 'Print' ?>
+
+
+
+
+
+
+
+ = $lang === 'ar' ? ($page === 'customer_statement' ? 'اختر العميل' : 'اختر المورد') : 'Select ' . ($page === 'customer_statement' ? 'Customer' : 'Supplier') ?>
+
+ ---
+
+ >= htmlspecialchars($e['name']) ?>
+
+
+
+
+ = $lang === 'ar' ? 'من تاريخ' : 'From Date' ?>
+
+
+
+ = $lang === 'ar' ? 'إلى تاريخ' : 'To Date' ?>
+
+
+
+
+ = $lang === 'ar' ? 'عرض التقرير' : 'View Report' ?>
+
+
+
+
+
+ Limit
+
+ >20
+ >40
+ >60
+ >100
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
+
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
+
+
+
= $lang === 'ar' ? 'كشف حساب' : 'Statement of Account' ?> - = htmlspecialchars($data['selected_entity']['name']) ?>
+
= htmlspecialchars($data['selected_entity']['name']) ?>
+
= htmlspecialchars($data['selected_entity']['email']) ?> | = htmlspecialchars($data['selected_entity']['phone']) ?>= $lang === 'ar' ? 'الفترة' : 'Period' ?> : = $_GET['start_date'] ?> = $lang === 'ar' ? 'إلى' : 'to' ?> = $_GET['end_date'] ?>
+
+
+
+
+
+
+ = $lang === 'ar' ? 'التاريخ' : 'Date' ?>
+ = $lang === 'ar' ? 'المرجع' : 'Reference' ?>
+ = $lang === 'ar' ? 'الوصف' : 'Description' ?>
+ = $lang === 'ar' ? 'مدين' : 'Debit' ?>
+ = $lang === 'ar' ? 'دائن' : 'Credit' ?>
+ = $lang === 'ar' ? 'الرصيد' : 'Balance' ?>
+
+
+
+
+
+ = $t['trans_date'] ?>
+ = $t['trans_type'] === 'invoice' ? ($lang === 'ar' ? ($page === 'supplier_statement' ? 'شراء-' : 'بيع-') : ($page === 'supplier_statement' ? 'PUR-' : 'INV-')).str_pad((string)$t['ref_no'], 5, '0', STR_PAD_LEFT) : ($lang === 'ar' ? 'قبض-' : 'RCP-').str_pad((string)$t['id'], 5, '0', STR_PAD_LEFT) ?>
+
+
+ = $lang === 'ar' ? 'فاتورة ضريبية' : 'Tax Invoice' ?>
+
+ = $lang === 'ar' ? 'دفع' : 'Payment' ?> - = $lang === 'ar' ? ($t['payment_method'] === 'cash' ? 'نقد' : ($t['payment_method'] === 'card' ? 'بطاقة ائتمان' : 'آجل')) : $t['payment_method'] ?>
+
+
+ = number_format($debit, 3) ?>
+ = number_format($credit, 3) ?>
+ = number_format($running_balance, 3) ?>
+
+
+
+
+
+ = $lang === 'ar' ? 'رصيد الإقفال' : 'Closing Balance' ?>
+ = $lang === 'ar' ? number_format($running_balance, 3) . ' ر.ع.' : 'OMR ' . number_format($running_balance, 3) ?>
+
+
+
+
+
+
+
+
+
+ Printed on = date('Y-m-d H:i:s') ?>
+
+
+
+
+
Please select an entity and date range to generate the statement.
+
+
+
+
+
+
+
Cashflow Statement
+
+ Print
+
+
+
+
+
+
+
+ From Date
+
+
+
+ To Date
+
+
+
+
+ Generate
+
+
+
+
+
+ Limit
+
+ >20
+ >40
+ >60
+ >100
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
+
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
+
+
+
Cashflow Statement
+
Period: = htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?> to = htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?>
+
+
+
+
+
+
+ Description
+ Amount (OMR)
+
+
+
+
+ Opening Cash Balance
+ = number_format($data['opening_balance'], 3) ?>
+
+
+
+ Operating Activities
+
+ 0) $op_inflow += $amt; else $op_outflow += abs($amt);
+ } elseif ($t['other_type'] === 'asset' && !in_array($t['other_account'], ['Accounts Receivable', 'Inventory'])) {
+ // Fixed assets etc
+ if ($amt > 0) $inv_inflow += $amt; else $inv_outflow += abs($amt);
+ } elseif ($t['other_type'] === 'equity' || $t['other_type'] === 'liability') {
+ if ($amt > 0) $fin_inflow += $amt; else $fin_outflow += abs($amt);
+ } else {
+ // Default to operating if unsure
+ if ($amt > 0) $op_inflow += $amt; else $op_outflow += abs($amt);
+ }
+ }
+ ?>
+
+ Cash Received from Customers & Others
+ = number_format($op_inflow, 3) ?>
+
+
+ Cash Paid to Suppliers & Expenses
+ (= number_format($op_outflow, 3) ?>)
+
+
+ Net Cash from Operating Activities
+ = number_format($op_inflow - $op_outflow, 3) ?>
+
+
+
+ Investing Activities
+
+
+ Net Cash from Investing Activities
+ = number_format($inv_inflow - $inv_outflow, 3) ?>
+
+
+
+ Financing Activities
+
+
+ Net Cash from Financing Activities
+ = number_format($fin_inflow - $fin_outflow, 3) ?>
+
+
+
+
+ Net Change in Cash
+ = number_format($net_change, 3) ?>
+
+
+ Closing Cash Balance
+ = number_format($data['opening_balance'] + $net_change, 3) ?>
+
+
+
+
+
+
+
+
___________________ Prepared By
+
+
+
___________________ Approved By
+
+
+
+
+
+
+
+
+
Payment Methods
+
+ Add Payment Method
+
+
+
+
+
+
+ ID
+ Name (EN)
+ Name (AR)
+ Actions
+
+
+
+
+
+ = $pm['id'] ?>
+ = htmlspecialchars($pm['name_en'] ?? '') ?>
+ = htmlspecialchars($pm['name_ar'] ?? '') ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
Expense Categories
+
+ Add Category
+
+
+
+
+
+
+ ID
+ Name (EN)
+ Name (AR)
+ Actions
+
+
+
+
+
+ = $cat['id'] ?>
+ = htmlspecialchars($cat['name_en']) ?>
+ = htmlspecialchars($cat['name_ar']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Name (EN)
+
+
+
+
+
+
+
+
+
Name (AR)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
+
Accounting Module
+
+
+
+ Sync All
+
+
+
+ Manual Entry
+
+
+
+ Print
+
+
+
+
+
+
+
+
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
+
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
+
+
+
= isset($_GET['view']) ? ucwords(str_replace('_', ' ', $_GET['view'])) : 'Journal' ?>
+
+
+
+
+
+
+
+
+
+ Date
+ Description
+ Reference
+ Amount
+ Action
+
+
+
+
+
+ = $entry['entry_date'] ?>
+ = htmlspecialchars($entry['description']) ?>
+ = htmlspecialchars($entry['reference']) ?>
+ = number_format((float)$entry['total_debit'], 3) ?>
+
+
+ View
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+ Add Account
+
+
+
+
+
+
+ Code
+ Name
+ Type
+ Parent
+ Balance
+
+
+
+
+
+ = $acc['code'] ?>
+
+ = htmlspecialchars($acc['name_en']) ?>
+ = htmlspecialchars($acc['name_ar']) ?>
+
+ = $acc['type'] ?>
+ = htmlspecialchars($acc['parent_name'] ?? '---') ?>
+ = number_format(getAccountBalance($acc['code']), 3) ?>
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
VAT Summary Report
+
+
+ VAT Input (Purchases)
+ = number_format($data['vat_report']['input_vat'], 2) ?>
+
+
+ VAT Output (Sales)
+ = number_format($data['vat_report']['output_vat'], 2) ?>
+
+
+ Net VAT Payable / (Refundable)
+ = number_format($data['vat_report']['net_vat'], 2) ?>
+
+
+
+
+ This report calculates the difference between VAT collected on sales and VAT paid on purchases for the selected period.
+
+
+
+
+
+
+
+
+
+
+
+ Code
+ Account Name
+ Debit
+ Credit
+
+
+
+
+
+ = $row['code'] ?>
+ = htmlspecialchars($row['name_en']) ?>
+ = number_format((float)$row['total_debit'], 3) ?>
+ = number_format((float)$row['total_credit'], 3) ?>
+
+
+
+
+
+ Total
+ = number_format($total_d, 3) ?>
+ = number_format($total_c, 3) ?>
+
+
+
+
+
+
+
+
+
+
Profit & Loss Statement
+
+ Revenue
+
+
+ = htmlspecialchars($acc['name_en']) ?>
+ = number_format($bal, 3) ?>
+
+
+
+ Total Revenue
+ = number_format($total_rev, 3) ?>
+
+
+ Expenses
+
+
+ = htmlspecialchars($acc['name_en']) ?>
+ = number_format($bal, 3) ?>
+
+
+
+ Total Expenses
+ = number_format($total_exp, 3) ?>
+
+
+
+ Net Profit / Loss
+ = number_format($total_rev - $total_exp, 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
Balance Sheet
+
+
+
Assets
+
+
+
+ = htmlspecialchars($acc['name_en']) ?>
+ = number_format($bal, 3) ?>
+
+
+
+ Total Assets
+ = number_format($total_assets, 3) ?>
+
+
+
+
+
Liabilities & Equity
+
+ Liabilities
+
+
+ = htmlspecialchars($acc['name_en']) ?>
+ = number_format($bal, 3) ?>
+
+
+
+ Equity
+
+
+ = htmlspecialchars($acc['name_en']) ?>
+ = number_format($bal, 3) ?>
+
+
+
+ query("SELECT code FROM acc_accounts WHERE type='revenue' AND parent_id IS NOT NULL")->fetchAll() as $a) $rev += getAccountBalance($a['code']);
+ $exp = 0; foreach(db()->query("SELECT code FROM acc_accounts WHERE type='expense' AND parent_id IS NOT NULL")->fetchAll() as $a) $exp += getAccountBalance($a['code']);
+ $earnings = $rev - $exp;
+ $total_equity += $earnings;
+ ?>
+
+ Retained Earnings (Current)
+ = number_format($earnings, 3) ?>
+
+
+
+ Total Liab. & Equity
+ = number_format($total_liab + $total_equity, 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account
+ Debit
+ Credit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Expenses List
+
+ Add Expense
+
+
+
+
+
+
+
+ Category
+
+ All
+
+ >= htmlspecialchars($c['name_en']) ?>
+
+
+
+
+ Start Date
+
+
+
+ End Date
+
+
+
+
Filter
+
+
+ Export
+
+
+
+
+
+
+
+
+
+
+
+ Date
+ Reference
+ Category
+ Description
+ Amount
+ Actions
+
+
+
+
+
+ = $exp['expense_date'] ?>
+ = htmlspecialchars($exp['reference_no'] ?: '---') ?>
+ = htmlspecialchars($exp['cat_en'] ?? 'Unknown') ?>
+ = htmlspecialchars($exp['description']) ?>
+ OMR = number_format((float)$exp['amount'], 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Category
+
+
+ >= htmlspecialchars($c['name_en']) ?>
+
+
+
+
+ Date
+
+
+
+ Amount
+
+
+
+ Reference No
+
+
+
+ Description
+ = htmlspecialchars($exp['description']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
+
+
+ Category
+
+ Select Category
+
+ = htmlspecialchars($c['name_en']) ?>
+
+
+
+
+ Date
+
+
+
+ Amount
+
+
+
+ Reference No
+
+
+
+ Description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
+
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
+
VAT: = htmlspecialchars($data['settings']['vat_number'] ?? '') ?>
+
+
+
Expense Report
+
Date: = date('Y-m-d') ?>
+
Period: = htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?> - = htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?>
+
+
+
+
+
+
+
Expense Report
+
+ Print
+
+
+
+
+
+
+
+ From Date
+
+
+
+ To Date
+
+
+
+ Category
+
+ All Categories
+
+ >
+ = htmlspecialchars($cat['name_en']) ?> / = htmlspecialchars($cat['name_ar']) ?>
+
+
+
+
+
+ Generate Report
+
+
+
+
+
+
+
+
+
Total Expenses
+ OMR = number_format((float)$data['total_expenses'], 3) ?>
+ For the selected period
+
+
+
+
+
+
+
+
+
+ Category
+ Total Amount
+ % of Total
+
+
+
+
+ No expenses found for this period.
+
+ 0 ? ($row['total'] / $data['total_expenses'] * 100) : 0;
+ ?>
+
+
+ = htmlspecialchars($row['name_en']) ?>
+ = htmlspecialchars($row['name_ar']) ?>
+
+ OMR = number_format((float)$row['total'], 3) ?>
+
+
+ = number_format($percent, 1) ?>%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
+
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
+
VAT: = htmlspecialchars($data['settings']['vat_number'] ?? '') ?>
+
+
+
Sales Returns Report
+
Date: = date('Y-m-d') ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Filter
+
+
+
+
+
+
+
+
+ Return #
+ Date
+ Invoice #
+ Customer
+ Total Amount
+ Actions
+
+
+
+
+
+ RET-= str_pad((string)$ret['id'], 5, '0', STR_PAD_LEFT) ?>
+ = $ret['return_date'] ?>
+ INV-= str_pad((string)$ret['invoice_id'], 5, '0', STR_PAD_LEFT) ?>
+ = htmlspecialchars($ret['customer_name'] ?? 'Walk-in') ?>
+ OMR = number_format((float)$ret['total_amount'], 3) ?>
+
+
+
+
+
+
+
+
+ No returns found
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Filter
+
+
+
+
+
+
+
+
+ Return #
+ Date
+ Invoice #
+ Supplier
+ Total Amount
+ Actions
+
+
+
+
+
+ PRET-= str_pad((string)$ret['id'], 5, '0', STR_PAD_LEFT) ?>
+ = $ret['return_date'] ?>
+ PUR-= str_pad((string)$ret['purchase_id'], 5, '0', STR_PAD_LEFT) ?>
+ = htmlspecialchars($ret['supplier_name'] ?? 'Unknown') ?>
+ OMR = number_format((float)$ret['total_amount'], 3) ?>
+
+
+
+
+
+
+
+
+ No returns found
+
+
+
+
+
+
+
+
+
+
HR Departments
+
+ Add Department
+
+
+
+
+
+
+ ID
+ Department Name
+ Actions
+
+
+
+
+
+ = $d['id'] ?>
+ = htmlspecialchars($d['name']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Department Name
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
HR Employees
+
+ Add Employee
+
+
+
+
+
+
+ Name
+ Biometric ID
+ Department
+ Position
+ Salary
+ Status
+ Actions
+
+
+
+
+
+
+ = htmlspecialchars($e['name']) ?>
+ = htmlspecialchars($e['email']) ?>
+
+ = htmlspecialchars($e['biometric_id'] ?? '---') ?>
+ = htmlspecialchars($e['dept_name'] ?? '---') ?>
+ = htmlspecialchars($e['position']) ?>
+ OMR = number_format($e['salary'], 3) ?>
+
+
+ = $e['status'] ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
HR Attendance
+
+
+
+ Pull Data from Devices
+
+
+
+ Biometric Sync
+
+
+
+
+
+
+
+
+
+
+
+ Employee
+ Department
+ Status
+ Clock In
+ Clock Out
+ Action
+
+
+
+
+
+ = htmlspecialchars($e['name']) ?>
+ = htmlspecialchars($e['dept_name'] ?? '---') ?>
+
+
+
+ = $e['status'] ?>
+
+
+ Not Marked
+
+
+ = $e['clock_in'] ?? '---' ?>
+ = $e['clock_out'] ?? '---' ?>
+
+
+ Mark
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Status
+
+ >Present
+ >Absent
+ >On Leave
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
+
+ To sync attendance from your biometric device, use the following API endpoint:
+
+
+ = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" ?>/api/biometric_sync.php
+
+
Expected JSON format:
+
+[
+ {
+ "biometric_id": "101",
+ "device_id": 1,
+ "timestamp": "2026-02-17 08:30:00",
+ "type": "in"
+ },
+ {
+ "biometric_id": "101",
+ "device_id": 1,
+ "timestamp": "2026-02-17 17:30:00",
+ "type": "out"
+ }
+]
+
+
+ Note: Ensure Employee Biometric IDs match those in the device logs.
+
+
+
+
+
+
+
+
+
+
HR Payroll
+
+
+
+
+
+ >= date('F', mktime(0, 0, 0, $m, 1)) ?>
+
+
+
+ =date('Y')-2; $y--): ?>
+ >= $y ?>
+
+
+
+
+ Generate
+
+
+
+
+
+
+
+ Employee
+ Basic
+ Bonus
+ Deductions
+ Net Salary
+ Status
+ Actions
+
+
+
+
+
+ = htmlspecialchars($p['emp_name']) ?>
+ OMR = number_format($p['basic_salary'], 3) ?>
+ + OMR = number_format($p['bonus'], 3) ?>
+ - OMR = number_format($p['deductions'], 3) ?>
+ OMR = number_format($p['net_salary'], 3) ?>
+
+
+ = $p['status'] ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Employee
+
+ --- Select ---
+
+ = htmlspecialchars($e['name']) ?> (Basic: = number_format($e['salary'], 3) ?>)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Biometric Devices
+
+ Add Device
+
+
+
+
+
+
+ Device Name
+ IP / IO Address
+ Port
+ Serial
+ Last Sync
+ Status
+ Actions
+
+
+
+
+
+
+ = htmlspecialchars($d['device_name']) ?>
+
+
+ IP: = htmlspecialchars($d['ip_address']) ?>
+
+ IO: = htmlspecialchars($d['io_address']) ?>
+
+
+ = $d['port'] ?>
+ = htmlspecialchars($d['serial_number'] ?? '---') ?>
+ = $d['last_sync'] ?? 'Never' ?>
+
+
+ = $d['status'] ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Device Name
+ Type
+ Connection
+ Details
+ Status
+ Actions
+
+
+
+
+
+
+ = htmlspecialchars($d['device_name']) ?>
+
+
+ = $d['device_type'] ?>
+
+
+ = $d['connection_type'] ?>
+
+
+
+ = htmlspecialchars((string)$d['ip_address']) ?>:= $d['port'] ?>
+
+ Baud: = $d['baud_rate'] ?>
+
+ USB Interface
+
+
+
+ = $d['status'] ?>
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Device Name
+
+
+
+
+ Device Type
+
+ >Weight Scale
+ >Receipt Printer
+ >Customer Display
+
+
+
+ Connection Type
+
+ >USB
+ >Network (TCP/IP)
+ >Serial (RS232)
+
+
+
+
+
+ Baud Rate
+
+
+
+ Status
+
+ >Active
+ >Inactive
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
+
+
+ Device Name
+
+
+
+
+ Device Type
+
+ Weight Scale
+ Receipt Printer
+ Customer Display
+
+
+
+ Connection Type
+
+ USB
+ Network (TCP/IP)
+ Serial (RS232)
+
+
+
+
+
+ Baud Rate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Profile Picture
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($data['user']['username']) ?>
+
= htmlspecialchars($_SESSION['user_role_name'] ?? 'User') ?>
+
+
+
+
+
+
+
+
+
Company Profile
+
+
+
+ Company Name
+
+
+
+ Phone
+
+
+
+ Email
+
+
+
+ VAT Number
+
+
+
+ Address
+ = htmlspecialchars($data['settings']['company_address'] ?? '') ?>
+
+
+ Allow Selling Out of Stock
+
+ data-en="Disabled" data-ar="معطل">Disabled
+ data-en="Enabled" data-ar="مفعل">Enabled
+
+
+
+
Company Logo
+
+
+
+
+
+
+
+
+
Favicon
+
+
+
+
+
+
+
+
+
Manager Signature
+
+
+
+
+
+
+
+
+
Loyalty Configuration
+
+
+ Loyalty System
+
+ data-en="Disabled" data-ar="معطل">Disabled
+ data-en="Enabled" data-ar="مفعل">Enabled
+
+
+
+ Points per 1 OMR
+
+
+
+ Points for 1 OMR Discount
+
+
+
+
+
+
+ Save Changes
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Group Name
+ Created Date
+ Status
+ Actions
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars((string)$group['name']) ?>
+
+
+ = date('M d, Y', strtotime((string)$group['created_at'])) ?>
+ Active
+
+
+
+
+
+
+
+
+
+
+
+
+ Group Name
+
+
+
+
+
Permissions
+
+ Select All
+ Deselect All
+
+
+
+
+ prepare("SELECT permission FROM role_permissions WHERE role_id = ?");
+ $stmtP->execute([$group['id']]);
+ $perms = $stmtP->fetchAll(PDO::FETCH_COLUMN);
+ foreach ($permission_groups as $group_name => $modules): ?>
+
+
+
= $group_name ?>
+
+
+ Group All
+
+
+
+ $label): ?>
+
+
+
= $label ?>
+
+
+ Select All
+
+
+
+
+
+ >
+ = ucfirst($a) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
Customer Display Settings
+
+
+
+
+
Greeting Message
+
+
+ Title
+
+
+
+ Subtitle
+
+
+
+
+
Slideshow Images
+
Upload images for the customer display slideshow (1920x1080 recommended).
+
+
+
+
Slide 1
+
+
+
+
+
+
+
+
+
+
Slide 2
+
+
+
+
+
+
+
+
+
+
Slide 3
+
+
+
+
+
+
+
+
+
+
+ Save Changes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manual Backup
+
Create a database backup immediately.
+
+ Backup Now
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Filename
+ Size
+ Date
+ Actions
+
+
+
+
+
+ No backups found.
+
+
+
+
+ = htmlspecialchars($b['name']) ?>
+ = htmlspecialchars($b['size']) ?>
+ = htmlspecialchars($b['date']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User Info
+ Access Level
+ Contact
+ Status
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+ = strtoupper(substr((string)$u['username'], 0, 1)) ?>
+
+
+
+
= htmlspecialchars((string)$u['username']) ?>
+
ID: #= str_pad((string)$u['id'], 4, '0', STR_PAD_LEFT) ?>
+
+
+
+
+
+ = htmlspecialchars((string)($u['group_name'] ?? 'No Role Assigned')) ?>
+
+
+
+ = htmlspecialchars((string)($u['email'] ?? '')) ?>
+ = htmlspecialchars((string)($u['phone'] ?? '-')) ?>
+
+
+
+ Active
+
+ Suspended
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Username
+
+
+
+ Email Address
+
+
+
+ Phone Number
+
+
+
+ Assign Role Group
+
+ --- No Group ---
+
+ >= htmlspecialchars((string)$g['name']) ?>
+
+
+
+
+ Account Status
+
+ >Active
+ >Inactive
+
+
+
+ New Password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
Cash Registers Management
+
Define your shop counters and registers.
+
+
+
+
+ License Limit: = $current_regs ?> / = $allowed_acts ?>
+ Registers
+
+
+
+
+ Add Register
+
+
+
+
+
+
+ ID
+ Name
+ Status
+ Created At
+ Actions
+
+
+
+
+
+ #= $r['id'] ?>
+ = htmlspecialchars($r['name']) ?>
+
+
+ = ucfirst($r['status']) ?>
+
+
+ = $r['created_at'] ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Register Name
+
+
+
+ Status
+
+ >Active
+ >Inactive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
+
+
Register Sessions (= count($data['sessions']) ?>)
+
Manage daily opening and closing of cash registers.
+
+
+ prepare("SELECT s.*, r.name as register_name FROM register_sessions s JOIN cash_registers r ON s.register_id = r.id WHERE s.user_id = ? AND s.status = 'open'");
+ $active_session->execute([$_SESSION['user_id']]);
+ $session = $active_session->fetch();
+ ?>
+
+
+ Open Register
+
+
+
+ Close Register
+
+
+
+
+
+
+
+
+
+ Current Open Register: = htmlspecialchars($session['register_name']) ?> |
+ Opened At: = $session['opened_at'] ?> |
+ Opening Balance: OMR = number_format((float)$session['opening_balance'], 3) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ ID
+ Register
+ Cashier
+ Opened At
+ Closed At
+ Opening Bal.
+ Cash Sale
+ Credit Card
+ Credit
+ Total Sale
+ Balance
+ Status
+ Report
+
+
+
+
+
+ #= $s['id'] ?>
+ = htmlspecialchars($s['register_name'] ?? 'N/A') ?>
+ = htmlspecialchars($s['username'] ?? 'N/A') ?>
+ = $s['opened_at'] ?>
+ = $s['closed_at'] ?? '---' ?>
+ OMR = number_format((float)$s['opening_balance'], 3) ?>
+ prepare("SELECT
+ SUM(CASE WHEN LOWER(payment_method) = 'cash' THEN amount ELSE 0 END) as cash_total,
+ SUM(CASE WHEN LOWER(payment_method) IN ('card', 'credit card', 'visa', 'mastercard') THEN amount ELSE 0 END) as card_total,
+ SUM(CASE WHEN LOWER(payment_method) = 'credit' THEN amount ELSE 0 END) as credit_total,
+ SUM(CASE WHEN LOWER(payment_method) LIKE '%transfer%' OR LOWER(payment_method) LIKE '%bank%' THEN amount ELSE 0 END) as transfer_total,
+ SUM(amount) as total_sales
+ FROM (
+ SELECT p.payment_method, p.amount FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed'
+ UNION ALL
+ SELECT p.payment_method, p.amount FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1
+ ) as combined_payments");
+ $stats_stmt->execute([$s['id'], $s['id']]);
+ $st = $stats_stmt->fetch();
+ $c_total = (float)($st['cash_total'] ?? 0);
+ $cd_total = (float)($st['card_total'] ?? 0);
+ $cr_total = (float)($st['credit_total'] ?? 0);
+ $tr_total = (float)($st['transfer_total'] ?? 0);
+ $t_sales = (float)($st['total_sales'] ?? 0);
+ $row_expected_cash = (float)$s['opening_balance'] + $c_total;
+ ?>
+ OMR = number_format($c_total, 3) ?>
+ OMR = number_format($cd_total, 3) ?>
+ OMR = number_format($cr_total, 3) ?>
+ OMR = number_format($t_sales, 3) ?>
+
+ 0 ? 'text-info' : 'text-danger');
+ ?>
+ OMR = number_format($diff, 3) ?>
+ ---
+
+
+
+ = ucfirst($s['status']) ?>
+
+
+
+
+
+
+
+
+
+
+
+ = renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Register
+
= htmlspecialchars($s['register_name']) ?>
+
+
+
Cashier
+
= htmlspecialchars($s['username']) ?>
+
+
+
Opened At
+
= $s['opened_at'] ?>
+
+
+
Closed At
+
= $s['closed_at'] ?? 'Still Open' ?>
+
+
+
+ prepare("SELECT payment_method, SUM(amount) as total FROM (
+ SELECT p.payment_method, p.amount FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed'
+ UNION ALL
+ SELECT p.payment_method, p.amount FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1
+ ) as combined_payments GROUP BY payment_method");
+ $breakdown->execute([$s['id'], $s['id']]);
+ $methods = $breakdown->fetchAll();
+
+ $cash_sales = 0;
+ $card_sales = 0;
+ $credit_sales = 0;
+ $bank_transfer_sales = 0;
+
+ foreach ($methods as $m) {
+ $method = strtolower($m['payment_method']);
+ if ($method === 'cash') $cash_sales = $m['total'];
+ elseif ($method === 'card' || strpos($method, 'card') !== false) $card_sales = $m['total'];
+ elseif ($method === 'credit') $credit_sales = $m['total'];
+ elseif (strpos($method, 'transfer') !== false || strpos($method, 'bank') !== false) $bank_transfer_sales = $m['total'];
+ else $cash_sales += $m['total'];
+ }
+ $total_sales = $cash_sales + $card_sales + $credit_sales + $bank_transfer_sales;
+ $expected_cash_total = (float)$s['opening_balance'] + $cash_sales;
+ $total_all = (float)$s['opening_balance'] + $total_sales;
+ ?>
+
+
+
+
Sales Summary
+
+ Opening Balance:
+ OMR = number_format((float)$s['opening_balance'], 3) ?>
+
+ prepare("SELECT SUM(vat_amount) as total_tax, SUM(discount_amount) as total_discount, SUM(total_net) as total_net FROM (
+ SELECT tax_amount as vat_amount, discount_amount, net_amount as total_net FROM pos_transactions WHERE register_session_id = ? AND status = 'completed'
+ UNION ALL
+ SELECT vat_amount, discount_amount, total_with_vat as total_net FROM invoices WHERE register_session_id = ? AND status = 'paid' AND is_pos = 1
+ ) as combined_totals");
+ $extra_stmt->execute([$s['id'], $s['id']]);
+ $extra = $extra_stmt->fetch();
+ ?>
+
+ Tax Amount:
+ OMR = number_format((float)($extra['total_tax'] ?? 0), 3) ?>
+
+
+ Discount:
+ OMR = number_format((float)($extra['total_discount'] ?? 0), 3) ?>
+
+
+ Cash Sale:
+ OMR = number_format($cash_sales, 3) ?>
+
+
+ Credit Card:
+ OMR = number_format($card_sales, 3) ?>
+
+
+ Credit:
+ OMR = number_format($credit_sales, 3) ?>
+
+
+ Bank Transfer:
+ OMR = number_format($bank_transfer_sales, 3) ?>
+
+
+ Total Sale:
+ OMR = number_format($total_sales, 3) ?>
+
+
+
+
+
+
+
Cash Reconciliation
+
+ Opening Balance:
+ OMR = number_format((float)$s['opening_balance'], 3) ?>
+
+
+ (+) Cash Sale:
+ OMR = number_format($cash_sales, 3) ?>
+
+
+ Expected Cash:
+ OMR = number_format($expected_cash_total, 3) ?>
+
+
+
+
+ Actual Cash:
+ OMR = number_format((float)$s['cash_in_hand'], 3) ?>
+
+
+
+ Balance:
+ OMR = number_format($shortage, 3) ?>
+
+
+
+
+
+
+
+
+
Transaction Details
+
+
+
+
+ Time
+ Order #
+ Customer
+ Items
+ Method
+ Amount
+
+
+
+ prepare("SELECT i.*, c.name as customer_name, GROUP_CONCAT(p.payment_method SEPARATOR ', ') as methods FROM invoices i LEFT JOIN payments p ON i.id = p.invoice_id LEFT JOIN customers c ON i.customer_id = c.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1 GROUP BY i.id ORDER BY i.created_at DESC");
+ $txs_stmt->execute([$s['id']]);
+ $txs = $txs_stmt->fetchAll();
+ foreach ($txs as $tx):
+ ?>
+
+ = date('H:i', strtotime($tx['created_at'])) ?>
+ = htmlspecialchars($tx['transaction_no']) ?>
+ = htmlspecialchars($tx['customer_name'] ?: 'Walk-in') ?>
+
+ prepare("SELECT si.name_en, ii.quantity FROM invoice_items ii JOIN stock_items si ON ii.item_id = si.id WHERE ii.invoice_id = ?");
+ $items_stmt->execute([$tx['id']]);
+ $items = $items_stmt->fetchAll();
+ foreach ($items as $item) {
+ echo "" . htmlspecialchars($item['name_en']) . " x " . (float)$item['quantity'] . " ";
+ }
+ ?>
+
+ = htmlspecialchars($tx['methods'] ?: '---') ?>
+ = number_format($tx['total_with_vat'], 3) ?>
+
+
+ prepare("SELECT t.*, c.name as customer_name, GROUP_CONCAT(p.payment_method SEPARATOR ', ') as methods FROM pos_transactions t LEFT JOIN pos_payments p ON t.id = p.transaction_id LEFT JOIN customers c ON t.customer_id = c.id WHERE t.register_session_id = ? AND t.status = 'completed' GROUP BY t.id ORDER BY t.created_at DESC");
+ $txs_stmt->execute([$s['id']]);
+ $txs = $txs_stmt->fetchAll();
+ foreach ($txs as $tx):
+ ?>
+
+ = date('H:i', strtotime($tx['created_at'])) ?>
+ = htmlspecialchars($tx['transaction_no']) ?>
+ = htmlspecialchars($tx['customer_name'] ?: 'Walk-in') ?>
+
+ prepare("SELECT i.name_en, ti.quantity FROM pos_items ti JOIN stock_items i ON ti.item_id = i.id WHERE ti.transaction_id = ?");
+ $items_stmt->execute([$tx['id']]);
+ $items = $items_stmt->fetchAll();
+ foreach ($items as $item) {
+ echo "" . htmlspecialchars($item['name_en']) . " x " . (float)$item['quantity'] . " ";
+ }
+ ?>
+
+ = htmlspecialchars($tx['methods'] ?: '---') ?>
+ = number_format($tx['net_amount'], 3) ?>
+
+
+ No transactions
+
+
+
+
+
+
+
+
+
Expected Cash: OMR = number_format($expected_cash_total, 3) ?>
+
Actual Cash: OMR = number_format((float)($s['cash_in_hand'] ?? 0), 3) ?>
+
+
+
+
+
Notes:
+
= nl2br(htmlspecialchars($s['notes'])) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select Register / Counter
+
+
+ = htmlspecialchars($reg['name']) ?>
+
+
+
+
+
Opening Cash Balance (OMR)
+
+
Enter the amount of cash already in the drawer.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Before closing, please count all cash in your register.
+
+
+ prepare("SELECT payment_method, SUM(amount) as total FROM (
+ SELECT p.payment_method, p.amount FROM pos_payments p JOIN pos_transactions t ON p.transaction_id = t.id WHERE t.register_session_id = ? AND t.status = 'completed'
+ UNION ALL
+ SELECT p.payment_method, p.amount FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1
+ ) as combined GROUP BY payment_method");
+ $curBreakdown->execute([$session['id'], $session['id']]);
+ $curMethods = $curBreakdown->fetchAll();
+
+ $cash_sales = 0;
+ $card_sales = 0;
+ $credit_sales = 0;
+ $bank_transfer_sales = 0;
+
+ foreach ($curMethods as $m) {
+ $method = strtolower($m['payment_method']);
+ if ($method === 'cash') $cash_sales = $m['total'];
+ elseif ($method === 'card' || strpos($method, 'card') !== false) $card_sales = $m['total'];
+ elseif ($method === 'credit') $credit_sales = $m['total'];
+ elseif (strpos($method, 'transfer') !== false || strpos($method, 'bank') !== false) $bank_transfer_sales = $m['total'];
+ else $cash_sales += $m['total'];
+ }
+ $total_sales = $cash_sales + $card_sales + $credit_sales + $bank_transfer_sales;
+ $expected_cash = (float)$session['opening_balance'] + $cash_sales;
+ $total_all = (float)$session['opening_balance'] + $total_sales;
+ ?>
+
+
+
Session Summary
+
+ Opening Balance:
+ OMR = number_format((float)$session['opening_balance'], 3) ?>
+
+
+ Cash Sales:
+ OMR = number_format($cash_sales, 3) ?>
+
+
+ Credit Card Sales:
+ OMR = number_format($card_sales, 3) ?>
+
+
+ Credit:
+ OMR = number_format($credit_sales, 3) ?>
+
+
+ Bank Transfer:
+ OMR = number_format($bank_transfer_sales, 3) ?>
+
+
+ Total Sales:
+ OMR = number_format($total_sales, 3) ?>
+
+
+
+ Balance (Total):
+ OMR = number_format($total_all, 3) ?>
+
+
+ Expected Cash:
+ OMR = number_format($expected_cash, 3) ?>
+
+
+
+
+
+
Transaction Details
+
+
+
+
+ Time
+ Order #
+ Customer
+ Method
+ Amount
+
+
+
+ prepare("SELECT t.*, c.name as customer_name, GROUP_CONCAT(p.payment_method SEPARATOR ', ') as methods FROM pos_transactions t LEFT JOIN pos_payments p ON t.id = p.transaction_id LEFT JOIN customers c ON t.customer_id = c.id WHERE t.register_session_id = ? AND t.status = 'completed' GROUP BY t.id ORDER BY t.created_at DESC");
+ $txs_stmt->execute([$session['id']]);
+ $txs = $txs_stmt->fetchAll();
+ foreach ($txs as $tx):
+ ?>
+
+ = date('H:i', strtotime($tx['created_at'])) ?>
+ = htmlspecialchars($tx['transaction_no']) ?>
+ = htmlspecialchars($tx['customer_name'] ?: 'Walk-in') ?>
+ = htmlspecialchars($tx['methods'] ?: '---') ?>
+ = number_format($tx['net_amount'], 3) ?>
+
+
+ No transactions
+
+
+
+
+
+
+
+ Total Cash in Hand (Actual Counted)
+
+
+
+ Closing Notes / Comments
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
--- " . htmlspecialchars(basename($file)) . " --- ";
+ $lines = shell_exec("tail -n 50 " . escapeshellarg($path));
+ echo "
" . htmlspecialchars((string)$lines) . " ";
+ }
+ }
+ if (!$found_logs) {
+ echo "
No accessible log files found.
";
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Department Name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Group Name
+
+
+
+
+
Permissions
+
+ Select All
+ Deselect All
+
+
+
+
+ $modules): ?>
+
+
+
= $group_name ?>
+
+
+ Group All
+
+
+
+ $label): ?>
+
+
= $label ?>
+
+
+
+
+ = ucfirst($a) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Name (EN)
+
+
+
+ EN
+
+
+
+
+
Name (AR)
+
+
+
+ AR
+
+
+
+
+ Category
+
+ ---
+
+ = htmlspecialchars($c['name_en']) ?> / = htmlspecialchars($c['name_ar']) ?>
+
+
+
+
+ Unit
+
+ ---
+
+ = htmlspecialchars($u['short_name_en']) ?> / = htmlspecialchars($u['short_name_ar']) ?>
+
+
+
+
+ Supplier
+
+ ---
+
+ = htmlspecialchars($s['name']) ?>
+
+
+
+
+
SKU / Barcode
+
+
+ Suggest
+
+
+
+ Sale Price
+
+
+
+ Purchase Price
+
+
+
+ Initial Stock
+
+
+
+ Min Stock Level
+
+
+
+ Item Picture
+
+
+
+ VAT Rate (%)
+
+
+
+
+
+ Has Expiry Date?
+
+
+
+ Expiry Date
+
+
+
+
+ Promotion Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please upload a CSV file with the following columns: SKU, English Name, Arabic Name, Sale Price, Cost Price.
+
+
+
+ Download Template
+
+
+
+ Choose CSV File
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance.
+
+
+
+ Choose CSV File
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance.
+
+
+
+ Choose CSV File
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please upload a CSV file with the following columns: Name (EN), Name (AR).
+
+
+
+ Choose CSV File
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please upload a CSV file with the following columns: Name (EN), Name (AR), Short (EN), Short (AR).
+
+
+
+ Choose CSV File
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Name (EN)
+
+
+
+
+
+
+
+
+
Name (AR)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Name (EN)
+
+
+
+
+
+
+
+
+ Short (EN)
+
+
+
+
Name (AR)
+
+
+
+
+
+
+
+
+ Short (AR)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Returned Items
+
+
+
+
+ Item
+ Returned Qty
+ Unit Price
+ Total Price
+
+
+
+
+
+ Total Amount:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select Invoice
+
+ Choose Invoice...
+
+
+ PUR-= str_pad((string)$inv['id'], 5, '0', STR_PAD_LEFT) ?> (= $inv['invoice_date'] ?>) - OMR = number_format((float)$inv['total_with_vat'], 3) ?>
+
+
+
+
+
+ Return Date
+
+
+
+
+
+
Items for Return
+
+
+
+
+ Item
+ Purchased Qty
+ Return Qty
+ Price
+ Total
+
+
+
+
+
+ Total Return Amount:
+ = __('currency') ?> 0.000
+
+
+
+
+
+ Notes / Reason for Return
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Select Invoice
+
+ Choose Invoice...
+
+
+ INV-= str_pad((string)$inv['id'], 5, '0', STR_PAD_LEFT) ?> (= $inv['invoice_date'] ?>) - OMR = number_format((float)$inv['total_with_vat'], 3) ?>
+
+
+
+
+
+ Return Date
+
+
+
+
+
+
Items for Return
+
+
+
+
+ Item
+ Sold Qty
+ Return Qty
+ Price
+ Total
+
+
+
+
+
+ Total Return Amount:
+ = __('currency') ?> 0.000
+
+
+
+
+
+ Notes / Reason for Return
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Code
+
+
+
+ Name (EN)
+
+
+
+ Name (AR)
+
+
+
+ Type
+
+ Asset
+ Liability
+ Equity
+ Revenue
+ Expense
+
+
+
+ Parent Account
+
+ --- None ---
+
+ = $acc['code'] ?> - = htmlspecialchars($acc['name_en']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Journal Details
+
+
+ Journal is not balanced! Difference: 0.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = $page === 'sales' ? 'Customer' : 'Supplier' ?>
+
+ ---
+
+ = htmlspecialchars($c['name']) ?>
+
+
+
+
+ Date
+
+
+
+ Due Date
+
+
+
+ Payment Type
+
+ Cash
+ Credit Card
+ Bank Transfer
+ Credit
+
+
+
+ Status
+
+ Unpaid
+ Partially Paid
+ Paid
+
+
+
+ Paid Amount
+
+
+
+
+
+
+
+
+
+
+ Item Details
+ Qty
+ Unit Price
+ VAT
+ Total
+
+
+
+
+
+
+ Subtotal
+ = __('currency') ?> 0.000
+
+
+
+ Total VAT
+ = __('currency') ?> 0.000
+
+
+
+ Grand Total
+ = __('currency') ?> 0.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = $page === 'sales' ? 'Customer' : 'Supplier' ?>
+
+ ---
+
+ = htmlspecialchars($c['name']) ?>
+
+
+
+
+ Date
+
+
+
+ Due Date
+
+
+
+ Payment Type
+
+ Cash
+ Credit Card
+ Bank Transfer
+ Credit
+
+
+
+ Status
+
+ Unpaid
+ Partially Paid
+ Paid
+
+
+
+ Paid Amount
+
+
+
+
+
+
+
+
+
+
+ Item Details
+ Qty
+ Unit Price
+ VAT
+ Total
+
+
+
+
+
+
+ Subtotal
+ = __('currency') ?> 0.000
+
+
+
+ Total VAT
+ = __('currency') ?> 0.000
+
+
+
+ Grand Total
+ = __('currency') ?> 0.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Supplier
+
+ ---
+
+ = htmlspecialchars($s['name']) ?>
+
+
+
+
+ Date
+
+
+
+ Delivery Date
+
+
+
+ Terms & Conditions
+
+
+
+
+
+
+
+
+
+
+ Item Details
+ Qty
+ Unit Price
+ VAT
+ Total
+
+
+
+
+
+
+ Subtotal
+ = __('currency') ?> 0.000
+
+
+ Total VAT
+ = __('currency') ?> 0.000
+
+
+ Grand Total
+ = __('currency') ?> 0.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Supplier
+
+ ---
+
+ = htmlspecialchars($s['name']) ?>
+
+
+
+
+ Date
+
+
+
+ Delivery Date
+
+
+
+ Status
+
+ Pending
+ Converted
+ Cancelled
+
+
+
+ Terms
+
+
+
+
+
+
+
+
+
+
+ Item Details
+ Qty
+ Unit Price
+ VAT
+ Total
+
+
+
+
+
+
+ Subtotal
+ = __('currency') ?> 0.000
+
+
+ Total VAT
+ = __('currency') ?> 0.000
+
+
+ Grand Total
+ = __('currency') ?> 0.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Customer
+
+ ---
+
+ = htmlspecialchars($c['name']) ?>
+
+
+
+
+ Date
+
+
+
+ Valid Until
+
+
+
+
+
+
+
+
+
+
+ Item Details
+ Qty
+ Unit Price
+ VAT
+ Total
+
+
+
+
+
+
+ Subtotal
+ = __('currency') ?> 0.000
+
+
+ Total VAT
+ = __('currency') ?> 0.000
+
+
+ Grand Total
+ = __('currency') ?> 0.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Customer
+
+ ---
+
+ = htmlspecialchars($c['name']) ?>
+
+
+
+
+ Date
+
+
+
+ Valid Until
+
+
+
+ Status
+
+ Pending
+ Converted
+ Expired
+ Cancelled
+
+
+
+
+
+
+
+
+
+
+ Item Details
+ Qty
+ Unit Price
+ VAT
+ Total
+
+
+
+
+
+
+ Subtotal
+ = __('currency') ?> 0.000
+
+
+ Total VAT
+ = __('currency') ?> 0.000
+
+
+ Grand Total
+ = __('currency') ?> 0.000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Bill To / فاتورة إلى
+
+
VAT / الضريبة:
+
Phone / الهاتف:
+
+
+
+
+
Payment Details / تفاصيل الدفع
+
Method / الطريقة:
+
Currency / العملة: OMR / ريال عماني
+
+
+
+
+
+
+
+
+
+
Amount in Words / المبلغ بالحروف
+
+
+
+
Terms & Conditions / الشروط والأحكام
+
+ Goods once sold will not be taken back or exchanged.
+ Payment is due within the agreed credit period.
+
+
+
+
+
+ Subtotal (Excl. VAT) / المجموع الفرعي (دون الضريبة)
+
+
+
+ VAT Amount / مبلغ الضريبة
+
+
+
+ Grand Total / المجموع الكلي
+
+
+
+
+
Payment Tracking / تتبع الدفع
+
+
+
+ Date / التاريخ
+ Method / الطريقة
+ Amount / المبلغ
+
+
+
+
+
+
+ Paid Amount / المبلغ المدفوع
+
+
+
+ Balance Due / الرصيد المتبقي
+
+
+
+
+
+
+
+
+
+
+
+
Customer Signature / توقيع العميل
+
+
+
+
+
+
Authorized Signatory / التوقيع المعتمد
+
+
+
+
Generated by = htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?> | Visit us at = $_SERVER['HTTP_HOST'] ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Amount
+
+
+
+ Remaining Amount
+
+
+
+ Amount to Pay
+
+
+
+ Payment Date
+
+
+
+ Payment Method
+
+ Cash
+ Credit Card
+ Bank Transfer
+
+
+
+ Notes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
+
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
+
+
Payment Receipt / سند قبض
+
+
+
+
Receipt No / رقم السند
+
+
+
+
+
+
+
Received From / استلمنا من
+
+
+
+
Against Invoice / مقابل فاتورة
+
+
+
+
Payment Method / طريقة الدفع
+
+
+
+
+
Amount Paid / المبلغ المدفوع
+
+
+
+
+
+
+
Receiver's Signature / توقيع المستلم
+
+
+
Authorized Signatory / التوقيع المعتمد
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Customer
+ Walk-in Customer
+
+
+
+
+ Select Credit Customer
+
+ --- Select Customer ---
+
+ = htmlspecialchars($c['name']) ?> (= htmlspecialchars($c['phone'] ?? '') ?>)
+
+
+
+
+
+
+
+
+
+
+
+
+
Add Payment Method
+
+
+
+ Cash
+
+
+
+ Credit Card
+
+
+
+ Credit
+
+
+
+ Bank Transfer
+
+
+
+
+
+
+
+
+
+
+ Total Tendered (Cash)
+ 0.000
+
+
* Change is calculated based on cash payments only.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number of Labels
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Label Layout
+
+ 3 x 7 (21 Labels per sheet)
+ 3 x 8 (24 Labels per sheet)
+ 4 x 10 (40 Labels per sheet)
+ L7651 (5 x 13 - 65 Labels)
+ L4736 (2 x 7 - 14 Labels)
+
+
+
+ Copies (Set All)
+
+
+
+ Print A4 Sheet
+
+
+
+
+
+
Quantities per Item
+
+ Select items to adjust quantities.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/insert_lines.txt b/insert_lines.txt
new file mode 100644
index 0000000..be653b2
--- /dev/null
+++ b/insert_lines.txt
@@ -0,0 +1,65 @@
+431
+481
+486
+498
+514
+518
+781
+828
+832
+855
+916
+932
+989
+1003
+1062
+1110
+1125
+1183
+1215
+1221
+1258
+1264
+1290
+1306
+1370
+1387
+1393
+1451
+1466
+1527
+1559
+1565
+1602
+1608
+1641
+1655
+1733
+1757
+1762
+1848
+1867
+1900
+1945
+1974
+2040
+2045
+2091
+2096
+2126
+2173
+2177
+2187
+2221
+2226
+2248
+2277
+2335
+2400
+2413
+2454
+2487
+2523
+2559
+2592
+2628
diff --git a/outlets_html.php b/outlets_html.php
new file mode 100644
index 0000000..c363e09
--- /dev/null
+++ b/outlets_html.php
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+ ID
+ Name
+ Address
+ Phone
+ Status
+ Created At
+ Actions
+
+
+
+
+
+ #= $o['id'] ?>
+ = htmlspecialchars($o['name']) ?>
+ = htmlspecialchars($o['address'] ?: '-') ?>
+ = htmlspecialchars($o['phone'] ?: '-') ?>
+ = ucfirst($o['status']) ?>
+ = htmlspecialchars($o['created_at']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+ Phone
+
+
+
+ Address
+
+
+
+ Status
+
+ Active
+ Inactive
+
+
+
+
+
+
+
+
+
diff --git a/patch_all.php b/patch_all.php
new file mode 100644
index 0000000..3b880ba
--- /dev/null
+++ b/patch_all.php
@@ -0,0 +1,90 @@
+$content = file_get_contents('index.php');
+
+$search = " 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);
+ }";
+
+$replace = " default:
+ if (can('dashboard_view')) {
+ \$oW = ((\$_SESSION['outlet_id'] ?? 1) == 0) ? \"\" : \"WHERE outlet_id = \" . (int)(\$_SESSION['outlet_id'] ?? 1);
+ \$oA = ((\$_SESSION['outlet_id'] ?? 1) == 0) ? \"\" : \"AND outlet_id = \" . (int)(\$_SESSION['outlet_id'] ?? 1);
+ \$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 \$oW\")->fetchColumn() ?: 0) + (db()->query(\"SELECT SUM(net_amount) FROM pos_transactions WHERE status = 'completed' \$oA\")->fetchColumn() ?: 0),
+ 'total_received' => (db()->query(\"SELECT SUM(amount) FROM payments \$oW\")->fetchColumn() ?: 0) + (db()->query(\"SELECT SUM(amount) FROM pos_payments \$oW\")->fetchColumn() ?: 0),
+ 'total_purchases' => db()->query(\"SELECT SUM(total_with_vat) FROM purchases \$oW\")->fetchColumn() ?: 0,
+ 'total_paid' => db()->query(\"SELECT SUM(amount) FROM purchase_payments \$oW\")->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 \$oW 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 \$oW GROUP BY label ORDER BY label ASC LIMIT 5\")->fetchAll(PDO::FETCH_ASSOC);
+ }";
+
+if (strpos($content, $search) !== false) {
+ $content = str_replace($search, $replace, $content);
+ echo "Dashboard patched successfully\n";
+ file_put_contents('index.php', $content);
+} else {
+ echo "Could not find target to patch\n";
+}
+$content = file_get_contents('index.php');
+
+$replacements = [
+ [
+ "case 'sales':\n case 'purchases':\n \$type = (\$page === 'sales') ? 'sale' : 'purchase';\n \$table = (\$type === 'purchase') ? 'purchases' : 'invoices';\n \$cust_supplier_col = (\$type === 'purchase') ? 'supplier_id' : 'customer_id';\n \$cust_supplier_table = (\$type === 'purchase') ? 'suppliers' : 'customers';\n \n \$where = [\"1=1\"];",
+ "case 'sales':\n case 'purchases':\n \$type = (\$page === 'sales') ? 'sale' : 'purchase';\n \$table = (\$type === 'purchase') ? 'purchases' : 'invoices';\n \$cust_supplier_col = (\$type === 'purchase') ? 'supplier_id' : 'customer_id';\n \$cust_supplier_table = (\$type === 'purchase') ? 'suppliers' : 'customers';\n \n \$where = [];\n if ((\$_SESSION['outlet_id'] ?? 1) != 0) \$where[] = \"v.outlet_id = \" . (int)(\$_SESSION['outlet_id'] ?? 1);\n else \$where[] = \"1=1\";"
+ ],
+ [
+ "case 'quotations':\n \$where = [\"1=1\"];",
+ "case 'quotations':\n \$where = [];\n if ((\$_SESSION['outlet_id'] ?? 1) != 0) \$where[] = \"q.outlet_id = \" . (int)(\$_SESSION['outlet_id'] ?? 1);\n else \$where[] = \"1=1\";"
+ ],
+ [
+ "case 'lpos':\n \$where = [\"1=1\"];",
+ "case 'lpos':\n \$where = [];\n if ((\$_SESSION['outlet_id'] ?? 1) != 0) \$where[] = \"q.outlet_id = \" . (int)(\$_SESSION['outlet_id'] ?? 1);\n else \$where[] = \"1=1\";"
+ ],
+ [
+ "case 'expenses':\n \$where = [\"1=1\"];",
+ "case 'expenses':\n \$where = [];\n if ((\$_SESSION['outlet_id'] ?? 1) != 0) \$where[] = \"e.outlet_id = \" . (int)(\$_SESSION['outlet_id'] ?? 1);\n else \$where[] = \"1=1\";"
+ ],
+ [
+ "case 'payments':",
+ "case 'payments':\n \$where = [];\n if ((\$_SESSION['outlet_id'] ?? 1) != 0) \$where[] = \"p.outlet_id = \" . (int)(\$_SESSION['outlet_id'] ?? 1);\n else \$where[] = \"1=1\";"
+ ]
+];
+
+foreach ($replacements as $rep) {
+ if (strpos($content, $rep[0]) !== false) {
+ $content = str_replace($rep[0], $rep[1], $content);
+ echo "Patched successfully\n";
+ } else {
+ echo "Could not find target to patch\n";
+ }
+}
+
+file_put_contents('index.php', $content);
diff --git a/patch_dashboard.php b/patch_dashboard.php
new file mode 100644
index 0000000..7725b2c
--- /dev/null
+++ b/patch_dashboard.php
@@ -0,0 +1,56 @@
+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);
+ }";
+
+$replace = " default:
+ if (can('dashboard_view')) {
+ \$oW = ((\$_SESSION['outlet_id'] ?? 1) == 0) ? \"\" : \"WHERE outlet_id = \" . (int)(\$_SESSION['outlet_id'] ?? 1);
+ \$oA = ((\$_SESSION['outlet_id'] ?? 1) == 0) ? \"\" : \"AND outlet_id = \" . (int)(\$_SESSION['outlet_id'] ?? 1);
+ \$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 \$oW\")->fetchColumn() ?: 0) + (db()->query(\"SELECT SUM(net_amount) FROM pos_transactions WHERE status = 'completed' \$oA\")->fetchColumn() ?: 0),
+ 'total_received' => (db()->query(\"SELECT SUM(amount) FROM payments \$oW\")->fetchColumn() ?: 0) + (db()->query(\"SELECT SUM(amount) FROM pos_payments \$oW\")->fetchColumn() ?: 0),
+ 'total_purchases' => db()->query(\"SELECT SUM(total_with_vat) FROM purchases \$oW\")->fetchColumn() ?: 0,
+ 'total_paid' => db()->query(\"SELECT SUM(amount) FROM purchase_payments \$oW\")->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 \$oW 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 \$oW GROUP BY label ORDER BY label ASC LIMIT 5\")->fetchAll(PDO::FETCH_ASSOC);
+ }";
+
+if (strpos($content, $search) !== false) {
+ $content = str_replace($search, $replace, $content);
+ echo "Dashboard patched successfully\n";
+ file_put_contents('index.php', $content);
+} else {
+ echo "Could not find target to patch\n";
+}
diff --git a/patch_inserts.php b/patch_inserts.php
new file mode 100644
index 0000000..63b4dbb
--- /dev/null
+++ b/patch_inserts.php
@@ -0,0 +1,68 @@
+ $p) {
+ if (strpos($content, $p[0]) !== false) {
+ $content = str_replace($p[0], $p[1], $content);
+ $content = str_replace($p[2], $p[3], $content);
+ echo "Patched #$i \n";
+
+ // Let's also add the ? to the query string
+ $q0 = "VALUES (";
+ $q1 = "VALUES (?, ";
+ // We only want to add it inside the specific INSERT query
+ $insert_start = strpos($content, $p[1]);
+ if ($insert_start !== false) {
+ $values_start = strpos($content, 'VALUES (', $insert_start);
+ if ($values_start !== false) {
+ $content = substr_replace($content, 'VALUES (?, ', $values_start, 9);
+ }
+ }
+ }
+}
+
+file_put_contents('index.php', $content);
diff --git a/patch_where.php b/patch_where.php
new file mode 100644
index 0000000..e7b6ab8
--- /dev/null
+++ b/patch_where.php
@@ -0,0 +1,36 @@
+exec("DROP FUNCTION IF EXISTS current_outlet_id");
+ $db->exec("CREATE FUNCTION current_outlet_id() RETURNS INT DETERMINISTIC RETURN @session_outlet_id");
+
+ $db->exec("RENAME TABLE invoices TO _invoices");
+ $db->exec("CREATE VIEW invoices AS SELECT * FROM _invoices WHERE outlet_id = current_outlet_id() OR current_outlet_id() IS NULL");
+
+ $db->exec("SET @session_outlet_id = 1");
+ $db->exec("INSERT INTO invoices (customer_id, invoice_date, status, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (1, '2026-01-01', 'unpaid', 100, 5, 105, 0)");
+ echo "Insert OK\n";
+
+ $stmt = $db->query("SELECT * FROM invoices ORDER BY id DESC LIMIT 1");
+ print_r($stmt->fetch(PDO::FETCH_ASSOC));
+
+} catch (Exception $e) {
+ echo "Error: " . $e->getMessage() . "\n";
+}
+
+$db->exec("DROP VIEW IF EXISTS invoices");
+$db->exec("RENAME TABLE _invoices TO invoices");
+$db->exec("DELETE FROM invoices WHERE customer_id=1 AND total_amount=100");
+$db->exec("DROP FUNCTION IF EXISTS current_outlet_id");
diff --git a/test_last_insert.php b/test_last_insert.php
new file mode 100644
index 0000000..c6f9345
--- /dev/null
+++ b/test_last_insert.php
@@ -0,0 +1,21 @@
+exec("RENAME TABLE invoices TO _invoices");
+ $db->exec("CREATE FUNCTION current_outlet_id() RETURNS INT DETERMINISTIC RETURN @session_outlet_id");
+ $db->exec("CREATE VIEW invoices AS SELECT * FROM _invoices WHERE outlet_id = current_outlet_id() OR current_outlet_id() IS NULL");
+
+ $db->exec("SET @session_outlet_id = 1");
+ $db->exec("INSERT INTO invoices (customer_id, invoice_date, status, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (1, '2026-01-01', 'unpaid', 100, 5, 105, 0)");
+ $id = $db->lastInsertId();
+ echo "Last Insert ID: " . $id . "\n";
+
+} catch (Exception $e) {
+ echo "Error: " . $e->getMessage() . "\n";
+}
+
+$db->exec("DROP VIEW IF EXISTS invoices");
+$db->exec("RENAME TABLE _invoices TO invoices");
+$db->exec("DELETE FROM invoices WHERE customer_id=1 AND total_amount=100");
+$db->exec("DROP FUNCTION IF EXISTS current_outlet_id");
diff --git a/test_view.php b/test_view.php
new file mode 100644
index 0000000..be14846
--- /dev/null
+++ b/test_view.php
@@ -0,0 +1,23 @@
+exec("RENAME TABLE invoices TO _invoices");
+ $db->exec("CREATE VIEW invoices AS SELECT * FROM _invoices WHERE outlet_id = @session_outlet_id OR @session_outlet_id IS NULL");
+
+ $db->exec("SET @session_outlet_id = 1");
+ // Test INSERT
+ $db->exec("INSERT INTO invoices (customer_id, invoice_date, status, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (1, '2026-01-01', 'unpaid', 100, 5, 105, 0)");
+ echo "Insert OK\n";
+ // Check if outlet_id is populated. It won't be, because the view doesn't auto-fill!
+ // We still need the trigger for INSERT.
+ $stmt = $db->query("SELECT * FROM invoices ORDER BY id DESC LIMIT 1");
+ print_r($stmt->fetch(PDO::FETCH_ASSOC));
+
+} catch (Exception $e) {
+ echo "Error: " . $e->getMessage() . "\n";
+}
+// rollback
+$db->exec("DROP VIEW IF EXISTS invoices");
+$db->exec("RENAME TABLE _invoices TO invoices");
+$db->exec("DELETE FROM invoices WHERE customer_id=1 AND total_amount=100");
diff --git a/wheres.txt b/wheres.txt
new file mode 100644
index 0000000..bfa67af
--- /dev/null
+++ b/wheres.txt
@@ -0,0 +1,372 @@
+ if ($type === 'sales' || $type === 'purchases') {
+ $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);
+ 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_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->execute($params);
+ $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"];
+ $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);
+ $stmt = db()->prepare("SELECT id, name, email, phone, tax_id, balance, created_at FROM $table WHERE $whereSql ORDER BY id DESC");
+ $stmt->execute($params);
+ $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 = [];
+ 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);
+ $stmt = db()->prepare("SELECT i.sku, i.name_en, i.name_ar, c.name_en as category, i.purchase_price, i.sale_price, i.stock_quantity, i.vat_rate
+ 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);
+ $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'];
+--
+$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);
+--
+ $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);
+--
+ 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
+--
+ 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'])) {
+--
+ 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'])) {
+--
+ $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'];
+--
+ $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
+--
+ $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
+--
+ 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['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 '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';
+ }