+
+
+
+
+
+
= __('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;
+ }
+ $current_oid = current_outlet_id();
+ $stmt = db()->prepare("INSERT INTO stock_items (outlet_id, 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([$current_oid, $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]);
+ $new_item_id = db()->lastInsertId();
+ 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);
+ // Update stock_items
+ $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, outlet_id) VALUES (?, ?, current_outlet_id())")->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, outlet_id) VALUES (?, ?, ?, ?, current_outlet_id())")->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" : ", outlet_id") . ") VALUES (?, ?, ?, ?, ?" . ($table === 'customers' ? ", 0" : ", " . current_outlet_id()) . ")";
+ 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;
+ update_stock($item_id, $change);
+ $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, outlet_id) 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, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
+ $stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat'], current_outlet_id()]);
+ $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
+ update_stock($item['item_id'], -$item['quantity']);
+ $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, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
+ $stmtPur->execute([$lpo['supplier_id'], $pur_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat'], current_outlet_id()]);
+ $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
+ update_stock($item['item_id'], $item['quantity']);
+ $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");
+ }
+
+
+ # --- Unified Import Logic (Excel & CSV) ---
+ # --- Unified Import Logic (Excel & CSV) ---
+ if (isset($_POST['import_items'])) {
+ error_log("Import items triggered.");
+ if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) {
+ $tmpPath = $_FILES['excel_file']['tmp_name'];
+ $rows = [];
+ if ( $xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath) ) {
+ $rows = $xlsx->rows();
+ } else {
+ $handle = fopen($tmpPath, "r");
+ $firstLine = fgets($handle); rewind($handle);
+ $sep = (substr_count($firstLine, ';') > substr_count($firstLine, ',')) ? ';' : ',';
+ $bom = fread($handle, 3); if ($bom !== "") rewind($handle);
+ while (($data = fgetcsv($handle, 0, $sep)) !== FALSE) $rows[] = $data;
+ fclose($handle);
+ }
+ if (isset($rows[0][0]) && stripos($rows[0][0], 'sku') !== false) array_shift($rows);
+
+ $current_oid = current_outlet_id();
+ foreach ($rows as $row) {
+ if (empty($row[0])) continue;
+ $sku = trim((string)$row[0]);
+ $name_en = trim((string)($row[1] ?? ''));
+ $name_ar = trim((string)($row[2] ?? ''));
+ $sale_price = (float)($row[3] ?? 0);
+ $purchase_price = (float)($row[4] ?? 0);
+ $qty = (float)($row[5] ?? 0);
+ $vat_rate = (float)($row[6] ?? 0);
+
+ $check = db()->prepare("SELECT id FROM stock_items WHERE sku = ? AND outlet_id = ?");
+ $check->execute([$sku, $current_oid]);
+ $exists = $check->fetch(PDO::FETCH_ASSOC);
+
+ if ($exists) {
+ $item_id = $exists['id'];
+ // Update Item (including stock_quantity)
+ db()->prepare("UPDATE stock_items SET name_en = ?, name_ar = ?, sale_price = ?, purchase_price = ?, vat_rate = ?, stock_quantity = ? WHERE id = ?")
+ ->execute([$name_en, $name_ar, $sale_price, $purchase_price, $vat_rate, $qty, $item_id]);
+ } else {
+ // Insert Item
+ db()->prepare("INSERT INTO stock_items (outlet_id, sku, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
+ ->execute([$current_oid, $sku, $name_en, $name_ar, $sale_price, $purchase_price, $qty, $vat_rate]);
+ }
+ $count++;
+ }
+ redirectWithMessage("Import items completed! $count processed.", "index.php?page=items");
+ }
+ }
+
+ if (isset($_POST['import_customers']) || isset($_POST['import_suppliers'])) {
+ $type = isset($_POST['import_customers']) ? 'customers' : 'suppliers';
+ $table = $type;
+ error_log("Import $type triggered.");
+ $count = 0;
+ if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) {
+ $tmpPath = $_FILES['excel_file']['tmp_name'];
+ $rows = [];
+ if ( $xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath) ) {
+ $rows = $xlsx->rows();
+ } else {
+ $handle = fopen($tmpPath, "r");
+ $firstLine = fgets($handle); rewind($handle);
+ $sep = (substr_count($firstLine, ';') > substr_count($firstLine, ',')) ? ';' : ',';
+ $bom = fread($handle, 3); if ($bom !== "") rewind($handle);
+ while (($data = fgetcsv($handle, 0, $sep)) !== FALSE) $rows[] = $data;
+ fclose($handle);
+ }
+ if (isset($rows[0][0]) && (stripos($rows[0][0], 'name') !== false || stripos($rows[0][0], 'id') !== false)) array_shift($rows);
+
+ foreach ($rows as $row) {
+ if (empty($row[0])) continue;
+ $name = trim((string)$row[0]);
+ if (!$name) continue;
+ $email = trim((string)($row[1] ?? ''));
+ $phone = trim((string)($row[2] ?? ''));
+ $tax_id = trim((string)($row[3] ?? ''));
+
+ db()->prepare("INSERT INTO $table (name, email, phone, tax_id, created_at" . ($table === 'suppliers' ? ", outlet_id" : "") . ") VALUES (?, ?, ?, ?, NOW()" . ($table === 'suppliers' ? ", ".current_outlet_id() : "") . ")")
+ ->execute([$name, $email, $phone, $tax_id]);
+ $count++;
+ }
+ redirectWithMessage("Import $type completed! $count processed.", "index.php?page=$type");
+ }
+ }
+
+ if (isset($_POST['import_categories'])) {
+ $count = 0;
+ if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) {
+ $tmpPath = $_FILES['excel_file']['tmp_name'];
+ $rows = [];
+ if ( $xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath) ) { $rows = $xlsx->rows(); }
+ else {
+ $handle = fopen($tmpPath, "r");
+ while (($data = fgetcsv($handle)) !== FALSE) $rows[] = $data;
+ fclose($handle);
+ }
+ if (isset($rows[0][0]) && stripos($rows[0][0], 'name') !== false) array_shift($rows);
+
+ foreach ($rows as $row) {
+ if (empty($row[0])) continue;
+ $name_en = trim((string)$row[0]);
+ $name_ar = trim((string)($row[1] ?? $name_en));
+
+ db()->prepare("INSERT INTO stock_categories (name_en, name_ar, outlet_id) VALUES (?, ?, current_outlet_id())")
+ ->execute([$name_en, $name_ar]);
+ $count++;
+ }
+ redirectWithMessage("Import categories completed! $count processed.", "index.php?page=categories");
+ }
+ }
+
+ if (isset($_POST['import_units'])) {
+ $count = 0;
+ if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) {
+ $tmpPath = $_FILES['excel_file']['tmp_name'];
+ $rows = [];
+ if ( $xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath) ) { $rows = $xlsx->rows(); }
+ else {
+ $handle = fopen($tmpPath, "r");
+ $firstLine = fgets($handle); rewind($handle);
+ $sep = (substr_count($firstLine, ';') > substr_count($firstLine, ',')) ? ';' : ',';
+ $bom = fread($handle, 3); if ($bom !== "") rewind($handle);
+ while (($data = fgetcsv($handle, 0, $sep)) !== FALSE) $rows[] = $data;
+ fclose($handle);
+ }
+ if (isset($rows[0][0]) && (stripos($rows[0][0], 'name') !== false || stripos($rows[0][0], 'id') !== false)) array_shift($rows);
+
+ foreach ($rows as $row) {
+ if (empty($row[0])) continue;
+ $name_en = trim((string)$row[0]);
+ $name_ar = trim((string)($row[1] ?? $name_en));
+ $short_en = trim((string)($row[2] ?? ''));
+ $short_ar = trim((string)($row[3] ?? ''));
+
+ db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_en, short_ar) VALUES (?, ?, ?, ?)")
+ ->execute([$name_en, $name_ar, $short_en, $short_ar]);
+ $count++;
+ }
+ redirectWithMessage("Import units completed! $count processed.", "index.php?page=units");
+ }
+ }
+
+ 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, outlet_id) 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, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
+ $stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat'], current_outlet_id()]);
+ $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
+ update_stock($item['item_id'], -$item['quantity']);
+ $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, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
+ $stmtPur->execute([$lpo['supplier_id'], $inv_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat'], current_outlet_id()]);
+ $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
+ update_stock($item['item_id'], $item['quantity']);
+ $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['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'];
+ update_stock($old['item_id'], $change);
+ }
+
+ // 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;
+ update_stock($item_id, $change);
+ }
+
+ $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]);
+ update_stock($item_id, $qty);
+ }
+ }
+
+ $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]);
+ update_stock($item_id, -$qty);
+ }
+ }
+
+ $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['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!";
+ }
+ }
+
+ // --- 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 "
+
+ = $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' : 'العربية' ?>
+
+
+
+ 1 || $is_admin):
+ $current_oid = current_outlet_id();
+ $current_oname = $current_oid === -1 ? (__('All Outlets') ?: 'All Outlets') : (db()->query("SELECT name FROM outlets WHERE id = $current_oid")->fetchColumn() ?: 'Outlet ' . $current_oid);
+ ?>
+
+
+ = htmlspecialchars($current_oname) ?>
+
+
+
+
+
+
+
= 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) ?>
+
+
+
+
+
+
+
+
+
+
+
+
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 Details
+
+
+
+
+
CTR No (Commercial Registration)
+
+
+
+
+
+
+
+
+
+
+
+
+ Contact Information
+
+
+
+
+
Email Address
+
+
+
+
+
+
+
Physical Address
+
+
+ = htmlspecialchars($data['settings']['company_address'] ?? '') ?>
+
+
+
+
+
+
+
+
+ System Configuration
+
+
+
+ System Timezone
+
+ $tz";
+ }
+ ?>
+
+
+
+ Stock Policy
+
+ data-en="Prevent selling out of stock" data-ar="منع البيع عند نفاذ المخزون">Prevent selling out of stock
+ data-en="Allow selling out of stock" data-ar="السماح بالبيع عند نفاذ المخزون">Allow selling out of stock
+
+
+
+
+
+
+
+
+ Visual Identity
+
+
+
+
+
Company Logo
+
+
+
+
+
+
+
+
+
+
+
+
+
Website Favicon
+
+
+
+
+
+
+
+
+
+
+
+
+
Manager Signature
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loyalty Program
+
+
+
+ Loyalty Status
+
+ data-en="Disabled" data-ar="معطل">Disabled
+ data-en="Active" data-ar="نشط">Active
+
+
+
+
Earning Rule (Points/1 OMR)
+
+
+
+
+
+
+
Redemption Rule (Points/1 OMR)
+
+
+
+
+
+
+
+
+
+
+ Save All 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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)
+ L7431 (6 x 8 - 48 Labels)
+ L4716 (6 x 8 - 48 Labels - Round)
+
+
+
+ Copies (Set All)
+
+
+
+ Print A4 Sheet
+
+
+
+
+
+
Quantities per Item
+
+ Select items to adjust quantities.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/post_debug.log b/post_debug.log
index 13194a0..c918099 100644
--- a/post_debug.log
+++ b/post_debug.log
@@ -100,3 +100,19 @@
2026-03-19 01:58:33 - POST: {"id":"117","name_en":"LAMING RED KIDNEY BEANS 425","name_ar":"\u0641\u0627\u0635\u0648\u0644\u064a\u0627 \u062d\u0645\u0631\u0627\u0621 \u0643\u064a\u062f\u0646\u064a \u0644\u0627\u0645\u064a\u0646\u062c 425","sku":"000023071605","category_id":"","unit_id":"","supplier_id":"","sale_price":"0.25","purchase_price":"0.2","stock_quantity":"0","min_stock_level":"0","vat_rate":"5","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
2026-03-19 01:58:51 - POST: {"action":"translate","text":"LAMING PINEAPPLE 454G","target":"ar"}
2026-03-19 01:59:00 - POST: {"id":"116","name_en":"LAMING PINEAPPLE 454G","name_ar":"\u0623\u0646\u0627\u0646\u0627\u0633 \u0644\u0627\u0645\u064a\u0646\u063a 454 \u062c\u0631\u0627\u0645","sku":"000023071599","category_id":"","unit_id":"","supplier_id":"","sale_price":"0.4","purchase_price":"0.346","stock_quantity":"55","min_stock_level":"0","vat_rate":"5","promotion_start":"","promotion_end":"","promotion_percent":"0","edit_item":""}
+2026-03-19 06:12:40 - POST: {"close_register":"1","session_id":"11","cash_in_hand":"10","notes":""}
+2026-03-19 06:27:02 - POST: {"open_register":"1","register_id":"3","opening_balance":"0"}
+2026-03-19 06:27:40 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":25.4}]","total_amount":"25.4","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":18,\"qty\":1,\"price\":0.6,\"vat_rate\":0,\"vat_amount\":0},{\"id\":17,\"qty\":2,\"price\":0.4,\"vat_rate\":0,\"vat_amount\":0},{\"id\":16,\"qty\":1,\"price\":1.5,\"vat_rate\":0,\"vat_amount\":0},{\"id\":22,\"qty\":5,\"price\":1,\"vat_rate\":0,\"vat_amount\":0},{\"id\":23,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":21,\"qty\":3,\"price\":1.6,\"vat_rate\":0,\"vat_amount\":0},{\"id\":20,\"qty\":1,\"price\":0.9,\"vat_rate\":0,\"vat_amount\":0},{\"id\":14,\"qty\":1,\"price\":0.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":8,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":9,\"qty\":1,\"price\":0.15,\"vat_rate\":0,\"vat_amount\":0},{\"id\":15,\"qty\":1,\"price\":2.55,\"vat_rate\":0,\"vat_amount\":0},{\"id\":29,\"qty\":1,\"price\":2.5,\"vat_rate\":0,\"vat_amount\":0},{\"id\":53,\"qty\":1,\"price\":0.15,\"vat_rate\":0,\"vat_amount\":0},{\"id\":33,\"qty\":1,\"price\":1.3,\"vat_rate\":0,\"vat_amount\":0},{\"id\":70,\"qty\":1,\"price\":2.55,\"vat_rate\":0,\"vat_amount\":0}]"}
+2026-03-19 06:30:20 - POST: {"close_register":"1","redirect_to":"dashboard","session_id":"12","cash_in_hand":"33","notes":""}
+2026-03-19 06:38:24 - POST: {"close_register":"1","redirect_to":"dashboard","session_id":"12","cash_in_hand":"33","notes":""}
+2026-03-19 06:40:40 - POST: {"open_register":"1","register_id":"3","opening_balance":"3"}
+2026-03-19 06:40:49 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":0.2}]","total_amount":"0.2","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":13,\"qty\":1,\"price\":0.2,\"vat_rate\":0,\"vat_amount\":0}]"}
+2026-03-19 06:41:01 - POST: {"close_register":"1","redirect_to":"dashboard","session_id":"13","cash_in_hand":"3","notes":""}
+2026-03-19 07:58:11 - POST: {"open_register":"1","register_id":"3","opening_balance":"32"}
+2026-03-19 07:58:27 - POST: {"action":"save_pos_transaction","customer_id":"","payments":"[{\"method\":\"cash\",\"amount\":11.25}]","total_amount":"11.249999999999998","tax_amount":"0","discount_code_id":"","discount_amount":"0","loyalty_redeemed":"0","items":"[{\"id\":8,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0},{\"id\":15,\"qty\":1,\"price\":2.55,\"vat_rate\":0,\"vat_amount\":0},{\"id\":21,\"qty\":2,\"price\":1.6,\"vat_rate\":0,\"vat_amount\":0},{\"id\":22,\"qty\":2,\"price\":1,\"vat_rate\":0,\"vat_amount\":0},{\"id\":30,\"qty\":1,\"price\":1.1,\"vat_rate\":0,\"vat_amount\":0},{\"id\":23,\"qty\":1,\"price\":1.2,\"vat_rate\":0,\"vat_amount\":0}]"}
+2026-03-19 07:59:22 - POST: {"close_register":"1","redirect_to":"dashboard","session_id":"14","cash_in_hand":"20","notes":""}
+2026-03-19 09:22:52 - POST: {"action":"save_theme","theme":"ocean"}
+2026-03-19 09:22:56 - POST: {"action":"save_theme","theme":"dark"}
+2026-03-19 09:22:58 - POST: {"action":"save_theme","theme":"default"}
+2026-03-19 09:23:00 - POST: {"action":"save_theme","theme":"dracula"}
+2026-03-19 09:23:02 - POST: {"action":"save_theme","theme":"sunset"}