0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'None',
]);
}
session_start();
if (isset($_GET['action']) && $_GET['action'] === 'download_items_template') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=items_import_template.csv');
$output = fopen('php://output', 'w');
// Add BOM for Excel UTF-8 compatibility
fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
fputcsv($output, ['SKU', 'English Name', 'Arabic Name', 'Sale Price', 'Cost Price']);
fclose($output);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
file_put_contents('post_debug.log', date('Y-m-d H:i:s') . " - POST: " . json_encode($_POST) . "\n", FILE_APPEND);
}
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/SimpleXLSX.php';
require_once __DIR__ . '/includes/stock_helper.php';
require_once __DIR__ . '/db/BackupService.php';
// Helper for current outlet
if (!function_exists('current_outlet_id')) {
function current_outlet_id() {
if (session_status() === PHP_SESSION_NONE) session_start();
return (int)($_SESSION['outlet_id'] ?? 1);
}
}
// Handle Outlet Switch
if (isset($_GET['action']) && $_GET['action'] === 'switch_outlet' && isset($_GET['id'])) {
$target_id = (int)$_GET['id'];
$allowed_outlets = $_SESSION['user_outlets'] ?? [1];
$is_admin = ($_SESSION['user_role_name'] ?? '') === 'Administrator';
if ($target_id === -1) { $_SESSION['outlet_id'] = -1; } elseif ($is_admin || in_array($target_id, $allowed_outlets)) {
$stmt = db()->prepare("SELECT id FROM outlets WHERE id = ? AND status = 'active'");
$stmt->execute([$target_id]);
if ($stmt->fetchColumn()) {
$_SESSION['outlet_id'] = $target_id;
}
}
header("Location: index.php");
exit;
}
// Timezone Setup
try {
$tz_stmt = db()->prepare("SELECT value FROM settings WHERE `key` = 'timezone'");
$tz_stmt->execute();
$app_tz = $tz_stmt->fetchColumn();
if ($app_tz && in_array($app_tz, DateTimeZone::listIdentifiers())) {
date_default_timezone_set($app_tz);
}
} catch (Exception $e) {
// Ignore if DB not ready
}
require_once 'includes/DatabaseInstaller.php';
// Auto-install database if not installed
if (!DatabaseInstaller::isInstalled()) {
try {
DatabaseInstaller::install();
} catch (Exception $e) {
die("Installation Error: " . $e->getMessage());
}
}
require_once 'lib/LicenseService.php';
require_once 'includes/lang.php';
// Language Setup
if (isset($_GET['lang'])) {
$_SESSION['lang'] = in_array($_GET['lang'], ['en', 'ar']) ? $_GET['lang'] : 'ar';
}
if (!isset($_SESSION['lang'])) {
$_SESSION['lang'] = 'ar'; // Default to Arabic as requested
}
$lang = $_SESSION['lang'];
$dir = ($lang === 'ar') ? 'rtl' : 'ltr';
// Licensing Middleware
try {
$is_activated = LicenseService::isActivated();
$trial_days = LicenseService::getTrialRemainingDays();
$can_access = LicenseService::canAccess();
} catch (PDOException $e) {
die("Database Connection Error: " . $e->getMessage() . " Please check your db/config.php settings.");
} catch (Exception $e) {
die("Application Error: " . $e->getMessage());
}
$page = $_GET['page'] ?? 'dashboard';
if (!$can_access && $page !== 'activate') {
header("Location: index.php?page=activate");
exit;
}
// Activation Page UI (accessible without login)
if ($page === 'activate') {
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['activate'])) {
$res = LicenseService::activate($_POST['license_key'] ?? '');
if ($res['success']) {
$success = "System activated successfully! Redirecting...";
header("refresh:2;url=index.php");
} else {
$error = $res['error'];
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['start_trial'])) {
// We need a way to call startTrial() which is private in LicenseService
// I'll make it public or use a public wrapper
$res = LicenseService::initTrial();
if ($res['success']) {
$success = "Trial period started! Redirecting...";
header("refresh:2;url=index.php");
} else {
$error = $res['error'];
}
}
?>
= __('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");
}
}
// Removed conflicting close_register handler
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'] ?? '';
$redirect_to = $_POST['redirect_to'] ?? 'dashboard';
// Calculate expected closing balance
$session = db()->prepare("SELECT opening_balance FROM register_sessions WHERE id = ?");
$session->execute([$session_id]);
$opening = (float)$session->fetchColumn();
// Unified calculation logic (POS Transactions + Invoices)
// Note: POS transactions are saved in invoices table with is_pos=1
$sales_sql = "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 LOWER(p.payment_method) = 'cash'";
$sales_stmt = db()->prepare($sales_sql);
$sales_stmt->execute([$session_id]);
$cash_sales = (float)$sales_stmt->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!";
if ($redirect_to === 'dashboard') {
// For POS users, we want dashboard
header("Location: index.php?page=dashboard");
} else {
// For Admin, we stay on sessions page
redirectWithMessage($message, "index.php?page=" . urlencode($redirect_to));
}
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'),
'register_sessions' => __('register_sessions')
],
'Settings' => [
'payment_methods' => __('payment_methods'),
'devices' => __('devices'),
'settings' => __('settings')
],
'Administration' => [
'role_groups' => __('role_groups'),
'users' => __('users'),
'cash_registers' => __('cash_registers'),
'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) ?>
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'] ?? []) ?>)
= 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((float)$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';
$oid = current_outlet_id();
$sql = "SELECT * FROM stock_items WHERE outlet_id = $oid ORDER BY name_en ASC LIMIT 100";
$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);
?>
= htmlspecialchars($p['name_en'] ?: $p['name_ar']) ?>
= 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']) ?>
Spend more to unlock Silver
Subtotal (Excl. VAT)
= __('currency') ?> 0.000
VAT
= __('currency') ?> 0.000
Total
= __('currency') ?> 0.000
PLACE ORDER
Quotations
Create New Quotation
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
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Local Purchase Orders (LPO)
Create New LPO
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
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
= 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' ?>
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((float)$total_all, 3) ?>
OMR = number_format((float)$total_paid, 3) ?>
OMR = number_format((float)$total_balance, 3) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
= $lang === 'ar' ? $page_title_ar : $page_title_en ?>
= $lang === 'ar' ? 'طباعة' : 'Print' ?>
= 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
= 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']) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Expenses List
Add Expense
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) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
= 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
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') ?>
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
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']) ?>
= 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) ?>
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
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Profile Picture
= htmlspecialchars($data['user']['username']) ?>
= htmlspecialchars($_SESSION['user_role_name'] ?? 'User') ?>
Contact Information
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
Loyalty Program
Loyalty Status
data-en="Disabled" data-ar="معطل">Disabled
data-en="Active" data-ar="نشط">Active
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): ?>
= 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'] ?>
= 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 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']]);
$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((float)$c_total, 3) ?>
OMR = number_format((float)$cd_total, 3) ?>
OMR = number_format((float)$cr_total, 3) ?>
OMR = number_format((float)$t_sales, 3) ?>
0 ? 'text-info' : 'text-danger');
?>
OMR = number_format((float)$diff, 3) ?>
---
= ucfirst($s['status']) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Select Register / Counter
= htmlspecialchars($reg['name']) ?>
Before closing, please count all cash in your register.
prepare("SELECT payment_method, SUM(amount) as total 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 GROUP BY payment_method");
$curBreakdown->execute([$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((float)$cash_sales, 3) ?>
Credit Card Sales:
OMR = number_format((float)$card_sales, 3) ?>
Credit:
OMR = number_format((float)$credit_sales, 3) ?>
Bank Transfer:
OMR = number_format((float)$bank_transfer_sales, 3) ?>
Total Sales:
OMR = number_format((float)$total_sales, 3) ?>
Balance (Total):
OMR = number_format((float)$total_all, 3) ?>
Expected Cash:
OMR = number_format((float)$expected_cash, 3) ?>
Transaction Details
Time
Order #
Customer
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([$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['total_with_vat'], 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.
";
}
?>
Returned Items
Item
Returned Qty
Unit Price
Total Price
Total Amount:
Items for Return
Item
Purchased Qty
Return Qty
Price
Total
Total Return Amount:
= __('currency') ?> 0.000
Notes / Reason for Return
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
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
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'] ?>
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
Payment Receipt / سند قبض
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.
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.