query("SELECT setting_key, setting_value FROM settings"); $settings = []; while ($row = $stmt->fetch()) { $settings[$row['setting_key']] = $row['setting_value']; } } catch (Exception $e) { $settings = []; } } return $settings; } function get_setting(string $key, $default = '') { $settings = get_settings(); return $settings[$key] ?? $default; } $app_tz = get_setting('timezone', 'UTC'); if (empty($app_tz)) { $app_tz = 'UTC'; } date_default_timezone_set($app_tz); try { db()->exec("SET time_zone = '" . date('P') . "'"); } catch (Throwable $e) {} function app_name(): string { return get_setting('company_name_ar', 'حلوى الريامي') . ' | ' . get_setting('company_name_en', 'Al Riyami Sweets'); } function current_lang(): string { if (isset($_GET['lang']) && in_array($_GET['lang'], ['ar', 'en'], true)) { $_SESSION['lang'] = $_GET['lang']; } return $_SESSION['lang'] ?? 'ar'; } function is_rtl(): bool { return current_lang() === 'ar'; } function tr(string $ar, string $en): string { return current_lang() === 'ar' ? $ar : $en; } function h($value): string { return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); } function qs_with_lang(array $params = []): string { if (!isset($params['lang']) || !in_array($params['lang'], ['ar', 'en'], true)) { $params['lang'] = current_lang(); } return http_build_query($params); } function url_for(string $path, array $params = []): string { $query = qs_with_lang($params); return $path . ($query ? ('?' . $query) : ''); } function redirect_to(string $path, array $params = []): void { header('Location: ' . url_for($path, $params)); exit; } function set_flash(string $type, string $message): void { $_SESSION['flash'] = ['type' => $type, 'message' => $message]; } function pull_flash(): ?array { $flash = $_SESSION['flash'] ?? null; unset($_SESSION['flash']); return $flash; } function branches(): array { try { $db = db(); $stmt = $db->query("SELECT * FROM branches"); $res = $stmt->fetchAll(PDO::FETCH_ASSOC); if ($res) { $arr = []; foreach ($res as $row) { $arr[$row['code']] = $row; } return $arr; } } catch (Exception $e) { // Table might not exist yet } return [ 'muscat' => ['code' => 'muscat', 'name_ar' => 'فرع مسقط', 'name_en' => 'Muscat Branch', 'city_ar' => 'مسقط', 'city_en' => 'Muscat'], 'sohar' => ['code' => 'sohar', 'name_ar' => 'فرع صحار', 'name_en' => 'Sohar Branch', 'city_ar' => 'صحار', 'city_en' => 'Sohar'], 'nizwa' => ['code' => 'nizwa', 'name_ar' => 'فرع نزوى', 'name_en' => 'Nizwa Branch', 'city_ar' => 'نزوى', 'city_en' => 'Nizwa'], ]; } function branch_label(string $code): string { $branch = branches()[$code] ?? null; if (!$branch) { return $code; } return current_lang() === 'ar' ? $branch['name_ar'] : $branch['name_en']; } function role_label(string $role): string { return match ($role) { 'owner' => tr('مالك / مدير عام', 'Owner / Admin'), 'manager' => tr('مدير فرع', 'Branch Manager'), 'cashier' => tr('كاشير', 'Cashier'), default => $role, }; } function current_user(): ?array { return $_SESSION['auth_user'] ?? null; } function login_attempt(string $username, string $password): bool { require_once __DIR__ . "/../db/config.php"; $stmt = db()->prepare("SELECT * FROM users WHERE username = ?"); $stmt->execute([$username]); $user = $stmt->fetch(); if (!$user) { return false; } if (password_verify($password, $user["password"])) { $_SESSION["auth_user"] = $user; return true; } return false; } function logout_user(): void { unset($_SESSION['auth_user']); } function require_auth(): array { $user = current_user(); if (!$user) { set_flash('warning', tr('يرجى تسجيل الدخول أولاً.', 'Please sign in first.')); redirect_to('login.php'); } return $user; } function get_app_modules(): array { return ["pos" => ["name_ar" => "نقاط البيع", "name_en" => "POS", "actions" => ["show", "add"]], "normal_sale" => ["name_ar" => "بيع عادي", "name_en" => "Normal Sale", "actions" => ["show", "add"]], "sales" => ["name_ar" => "المبيعات", "name_en" => "Sales", "actions" => ["show", "edit", "del"]], "purchases" => ["name_ar" => "المشتريات", "name_en" => "Purchases", "actions" => ["show", "add", "edit", "del"]], "stock" => ["name_ar" => "المخزون", "name_en" => "Stock", "actions" => ["show", "add", "edit", "del"]], "reports" => ["name_ar" => "التقارير", "name_en" => "Reports", "actions" => ["show"]], "customers" => ["name_ar" => "العملاء", "name_en" => "Customers", "actions" => ["show", "add", "edit", "del"]], "suppliers" => ["name_ar" => "الموردين", "name_en" => "Suppliers", "actions" => ["show", "add", "edit", "del"]], "categories" => ["name_ar" => "التصنيفات", "name_en" => "Categories", "actions" => ["show", "add", "edit", "del"]], "units" => ["name_ar" => "الوحدات", "name_en" => "Units", "actions" => ["show", "add", "edit", "del"]], "users" => ["name_ar" => "المستخدمين", "name_en" => "Users", "actions" => ["show", "add", "edit", "del"]], "settings" => ["name_ar" => "الإعدادات", "name_en" => "Settings", "actions" => ["show", "edit"]], "expense_categories" => ["name_ar" => "تصنيفات المصروفات", "name_en" => "Expense Categories", "actions" => ["show", "add", "edit", "del"]], "expenses" => ["name_ar" => "المصروفات", "name_en" => "Expenses", "actions" => ["show", "add", "edit", "del"]]]; } function has_permission(string $m, string $a = "show"): bool { $u = current_user(); if (!$u) return false; if ($u["role"] === "owner") return true; $p = !empty($u["permissions"]) ? (is_array($u["permissions"]) ? $u["permissions"] : json_decode($u["permissions"], true)) : []; return !empty($p[$m][$a]); } function require_permission(string $m, string $a = "show"): array { $u = require_auth(); if (!has_permission($m, $a)) { set_flash("warning", tr("ليس لديك صلاحية.", "You do not have permission.")); redirect_to("index.php"); } return $u; } function require_roles(array $roles): array { $user = require_auth(); if (!in_array($user['role'], $roles, true)) { set_flash('warning', tr('ليس لديك صلاحية للوصول إلى هذه الصفحة.', 'You do not have permission to access this page.')); redirect_to('index.php'); } return $user; } function get_user_branches($user): array { if (!$user) return []; if ($user['role'] === 'owner') return array_keys(branches()); $list = [$user['branch_code']]; if (!empty($user['allowed_branches'])) { $extra = explode(',', $user['allowed_branches']); foreach ($extra as $b) { $b = trim($b); if ($b) $list[] = $b; } } return array_unique($list); } function get_user_branches_assoc($user): array { if (!$user) return []; $all = branches(); $allowed = get_user_branches($user); $res = []; foreach ($allowed as $b) { if (isset($all[$b])) { $res[$b] = $all[$b]; } } return $res; } function can_access_branch(string $branchCode): bool { $user = current_user(); if (!$user) { return false; } if ($user['role'] === 'owner') { return true; } $allowed = get_user_branches($user); return in_array($branchCode, $allowed, true); } function catalog(): array { try { $db = db(); $stmt = $db->query("SELECT items.*, units.name_ar as u_name_ar, units.name_en as u_name_en FROM items LEFT JOIN units ON items.unit_id = units.id"); $items = $stmt->fetchAll(PDO::FETCH_ASSOC); $catalog = []; foreach ($items as $item) { $catalog[$item["sku"]] = [ "sku" => $item["sku"], "name_ar" => $item["name"], "name_en" => $item["name"], "price" => (float)$item["price"], "cost_price" => (float)($item["cost_price"] ?? 0), "base_stock" => (int)$item["base_stock"], "vat" => (float)$item["vat"], "category_id" => $item["category_id"], "in_catalog" => (int)($item["in_catalog"] ?? 0), "supplier_id" => $item["supplier_id"], "image_url" => $item["image_url"], "unit_id" => $item["unit_id"], "unit_ar" => $item["u_name_ar"] ?? "قطعة", "unit_en" => $item["u_name_en"] ?? "pcs" ]; } return $catalog; } catch (Throwable $e) { return []; } } function product_label(string $sku): string { $item = catalog()[$sku] ?? null; if (!$item) { return $sku; } return current_lang() === "ar" ? $item["name_ar"] : $item["name_en"]; } function currency(float $amount): string { return number_format($amount, 3) . ' ' . tr('ر.ع', 'OMR'); } function sale_mode_label(string $mode): string { return $mode === 'normal' ? tr('بيع عادي', 'Normal Sale') : tr('بيع نقاط البيع', 'POS Sale'); } function ensure_sales_table(): void { $sql = "CREATE TABLE IF NOT EXISTS sales_orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, receipt_no VARCHAR(50) NOT NULL UNIQUE, sale_mode VARCHAR(20) NOT NULL, branch_code VARCHAR(30) NOT NULL, cashier_username VARCHAR(60) NOT NULL, cashier_name VARCHAR(120) NOT NULL, role_name VARCHAR(40) NOT NULL, customer_name VARCHAR(120) DEFAULT NULL, payment_method VARCHAR(30) NOT NULL, items_json LONGTEXT NOT NULL, item_count INT UNSIGNED NOT NULL DEFAULT 0, subtotal DECIMAL(10,2) NOT NULL DEFAULT 0, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'completed', notes TEXT DEFAULT NULL, sale_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_sale_mode (sale_mode), INDEX idx_branch_code (branch_code), INDEX idx_sale_date (sale_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"; db()->exec($sql); } function create_sale(array $data): int { ensure_sales_table(); $stmt = db()->prepare('INSERT INTO sales_orders (receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_name, payment_method, items_json, item_count, subtotal, vat_amount, total_amount, status, notes, sale_date) VALUES (:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_name, :payment_method, :items_json, :item_count, :subtotal, :vat_amount, :total_amount, :status, :notes, NOW())'); $stmt->bindValue(':receipt_no', $data['receipt_no']); $stmt->bindValue(':sale_mode', $data['sale_mode']); $stmt->bindValue(':branch_code', $data['branch_code']); $stmt->bindValue(':cashier_username', $data['cashier_username']); $stmt->bindValue(':cashier_name', $data['cashier_name']); $stmt->bindValue(':role_name', $data['role_name']); $stmt->bindValue(':customer_name', $data['customer_name']); $stmt->bindValue(':payment_method', $data['payment_method']); $stmt->bindValue(':items_json', json_encode($data['items'], JSON_UNESCAPED_UNICODE)); $stmt->bindValue(':item_count', $data['item_count'], PDO::PARAM_INT); $stmt->bindValue(':subtotal', $data['subtotal']); $stmt->bindValue(':vat_amount', $data['vat_amount'] ?? 0.0); $stmt->bindValue(':total_amount', $data['total_amount']); $stmt->bindValue(':status', $data['status'] ?? 'completed'); $stmt->bindValue(':notes', $data['notes']); $stmt->execute(); return (int) db()->lastInsertId(); } function base_sales_query_filters(array &$params, ?string $mode = null, ?string $branch = null): string { $sql = ' WHERE 1=1 '; if ($mode) { $sql .= ' AND sale_mode = :sale_mode '; $params[':sale_mode'] = $mode; } if ($branch) { $sql .= ' AND branch_code = :branch_code '; $params[':branch_code'] = $branch; } $user = current_user(); if ($user && $user['role'] !== 'owner') { $ubranches = get_user_branches($user); if (empty($ubranches)) { $sql .= ' AND 1=0 '; // No branches allowed } else { $namedParams = []; foreach ($ubranches as $i => $ub) { $key = ':v_branch_' . $i; $namedParams[] = $key; $params[$key] = $ub; } $sql .= ' AND branch_code IN (' . implode(', ', $namedParams) . ') '; } } return $sql; } function fetch_sales(?string $mode = null, ?string $branch = null, int $limit = 50): array { ensure_sales_table(); $params = []; $sql = 'SELECT * FROM sales_orders' . base_sales_query_filters($params, $mode, $branch) . ' ORDER BY sale_date DESC LIMIT :limit'; $stmt = db()->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value); } $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); $rows = $stmt->fetchAll(); foreach ($rows as &$row) { $row['items'] = json_decode((string) $row['items_json'], true) ?: []; } return $rows; } function fetch_sale(int $id): ?array { ensure_sales_table(); $stmt = db()->prepare('SELECT * FROM sales_orders WHERE id = :id LIMIT 1'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $sale = $stmt->fetch(); if (!$sale) { return null; } if (!can_access_branch((string) $sale['branch_code'])) { return null; } $sale['items'] = json_decode((string) $sale['items_json'], true) ?: []; return $sale; } function fetch_all_sales_for_scope(): array { ensure_sales_table(); $params = []; $sql = 'SELECT * FROM sales_orders' . base_sales_query_filters($params); $stmt = db()->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue($key, $value); } $stmt->execute(); $rows = $stmt->fetchAll(); foreach ($rows as &$row) { $row['items'] = json_decode((string) $row['items_json'], true) ?: []; } return $rows; } function dashboard_metrics(): array { $sales = fetch_all_sales_for_scope(); $today = date('Y-m-d'); $todaySales = 0; $todayRevenue = 0.0; $normalCount = 0; $posCount = 0; foreach ($sales as $sale) { if (str_starts_with((string) $sale['sale_date'], $today)) { $todaySales++; $todayRevenue += (float) $sale['total_amount']; } if (($sale['sale_mode'] ?? '') === 'normal') { $normalCount++; } else { $posCount++; } } return [ 'today_sales' => $todaySales, 'today_revenue' => $todayRevenue, 'pos_count' => $posCount, 'normal_count' => $normalCount, 'recent' => array_slice(fetch_sales(null, null, 6), 0, 6), ]; } function report_metrics(): array { $sales = fetch_all_sales_for_scope(); $branchTotals = []; $paymentTotals = []; $productTotals = []; $monthlyTotals = []; $gross = 0.0; $totalVat = 0.0; foreach ($sales as $sale) { $branch = $sale['branch_code']; $branchTotals[$branch] = ($branchTotals[$branch] ?? 0.0) + (float) $sale['total_amount']; $payment = $sale['payment_method']; $paymentTotals[$payment] = ($paymentTotals[$payment] ?? 0.0) + (float) $sale['total_amount']; $gross += (float) $sale['total_amount']; $totalVat += (float) $sale['vat_amount']; $month = substr((string)$sale['sale_date'], 0, 7); $monthlyTotals[$month] = ($monthlyTotals[$month] ?? 0.0) + (float) $sale['total_amount']; foreach ($sale['items'] as $item) { $sku = (string) ($item['sku'] ?? ''); $qty = (int) ($item['qty'] ?? 0); $productTotals[$sku] = ($productTotals[$sku] ?? 0) + $qty; } } arsort($branchTotals); arsort($paymentTotals); arsort($productTotals); ksort($monthlyTotals); return [ 'gross' => $gross, 'total_vat' => $totalVat, 'branch_totals' => $branchTotals, 'payment_totals' => $paymentTotals, 'product_totals' => $productTotals, 'monthly_totals' => $monthlyTotals, 'sales_count' => count($sales), ]; } function stock_snapshot(): array { $catalog = catalog(); $sold = []; foreach (fetch_all_sales_for_scope() as $sale) { foreach ($sale['items'] as $item) { $sku = (string) ($item['sku'] ?? ''); $sold[$sku] = ($sold[$sku] ?? 0) + (int) ($item['qty'] ?? 0); } } $rows = []; foreach ($catalog as $sku => $item) { $base = (int) $item['base_stock']; $used = $sold[$sku] ?? 0; $rows[] = [ 'sku' => $sku, 'name' => current_lang() === 'ar' ? $item['name_ar'] : $item['name_en'], 'base_stock' => $base, 'sold' => $used, 'available' => max(0, $base - $used), 'price' => $item['price'], 'cost_price' => $item['cost_price'] ?? 0, 'category_id' => $item['category_id'], 'supplier_id' => $item['supplier_id'], 'unit_id' => $item['unit_id'], 'image_url' => $item['image_url'], 'vat' => $item['vat'], ]; } usort($rows, static fn(array $a, array $b): int => $a['available'] <=> $b['available']); return $rows; } function module_cards(): array { return [ ['title_ar' => 'نقاط البيع', 'title_en' => 'POS Sale', 'path' => 'pos.php', 'desc_ar' => 'إتمام البيع السريع مع تحديث السجل.', 'desc_en' => 'Fast checkout with instant sales logging.'], ['title_ar' => 'بيع عادي', 'title_en' => 'Normal Sale', 'path' => 'normal_sale.php', 'desc_ar' => 'فاتورة يدوية مع العميل والملاحظات.', 'desc_en' => 'Manual invoice flow with customer details and notes.'], ['title_ar' => 'المبيعات', 'title_en' => 'Sales Ledger', 'path' => 'sales.php', 'desc_ar' => 'قائمة الفواتير مع التفاصيل والفرز.', 'desc_en' => 'Invoice list with filters and detail views.'], ['title_ar' => 'المخزون', 'title_en' => 'Stock', 'path' => 'stock.php', 'desc_ar' => 'قراءة فورية للمخزون الحالي والتنبيهات.', 'desc_en' => 'Live stock snapshot and low-stock indicators.'], ['title_ar' => 'المشتريات', 'title_en' => 'Purchases', 'path' => 'purchases.php', 'desc_ar' => 'واجهة مبدئية لاستلام الموردين بين الفروع.', 'desc_en' => 'Starter receiving board for suppliers and branches.'], ['title_ar' => 'التقارير', 'title_en' => 'Reports', 'path' => 'reports.php', 'desc_ar' => 'مبيعات اليوم، الفروع، وأفضل الأصناف.', 'desc_en' => 'Daily sales, branch totals, and best sellers.'], ['title_ar' => 'المستخدمون والأدوار', 'title_en' => 'Users & Roles', 'path' => 'users.php', 'desc_ar' => 'صلاحيات منفصلة للمالك والمدير والكاشير.', 'desc_en' => 'Separate access for owner, manager, and cashier.'], ]; } function purchase_pipeline(): array { return [ ['supplier' => 'Oman Dates Co.', 'reference' => 'PO-24019', 'branch' => 'muscat', 'status' => tr('بانتظار الاستلام', 'Pending Receiving'), 'eta' => '2026-04-20'], ['supplier' => 'Golden Nuts', 'reference' => 'PO-24023', 'branch' => 'sohar', 'status' => tr('في الطريق', 'In Transit'), 'eta' => '2026-04-21'], ['supplier' => 'Saffron House', 'reference' => 'PO-24026', 'branch' => 'nizwa', 'status' => tr('جاهز للفحص', 'Ready for QC'), 'eta' => '2026-04-22'], ]; } function receipt_code(): string { return (string) random_int(100000, 999999); } function create_purchase(array $data): int { db()->beginTransaction(); try { $stmt = db()->prepare("INSERT INTO purchase_orders (reference_no, branch_code, user_username, user_name, role_name, supplier_name, items_json, item_count, subtotal, vat_amount, total_amount, status, notes, purchase_date) VALUES (:reference_no, :branch_code, :user_username, :user_name, :role_name, :supplier_name, :items_json, :item_count, :subtotal, :vat_amount, :total_amount, :status, :notes, NOW())"); $stmt->bindValue(":reference_no", $data["reference_no"]); $stmt->bindValue(":branch_code", $data["branch_code"]); $stmt->bindValue(":user_username", $data["user_username"]); $stmt->bindValue(":user_name", $data["user_name"]); $stmt->bindValue(":role_name", $data["role_name"]); $stmt->bindValue(":supplier_name", $data["supplier_name"]); $stmt->bindValue(":items_json", json_encode($data["items"], JSON_UNESCAPED_UNICODE)); $stmt->bindValue(":item_count", $data["item_count"], PDO::PARAM_INT); $stmt->bindValue(":subtotal", $data["subtotal"]); $stmt->bindValue(":vat_amount", $data["vat_amount"] ?? 0.0); $stmt->bindValue(":total_amount", $data["total_amount"]); $stmt->bindValue(":status", $data["status"] ?? "completed"); $stmt->bindValue(":notes", $data["notes"]); $stmt->execute(); $purchaseId = (int) db()->lastInsertId(); // Update stock foreach ($data["items"] as $item) { $qty = (int) $item["qty"]; $sku = $item["sku"]; $updateStmt = db()->prepare("UPDATE items SET base_stock = base_stock + :qty, cost_price = :price WHERE sku = :sku"); $updateStmt->bindValue(":qty", $qty, PDO::PARAM_INT); $updateStmt->bindValue(":price", $item["price"]); $updateStmt->bindValue(":sku", $sku); $updateStmt->execute(); } db()->commit(); return $purchaseId; } catch (Throwable $e) { db()->rollBack(); throw $e; } }