query("SELECT * FROM company_settings LIMIT 1"); $settings = $stmt->fetch(PDO::FETCH_ASSOC); } } catch (Exception $e) { // Log error or ignore if table doesn't exist yet } // Default values if no settings found if (!$settings) { $settings = [ 'company_name' => 'My Restaurant', 'address' => '123 Food Street', 'phone' => '555-0199', 'email' => 'info@restaurant.com', 'vat_rate' => 0.00, 'currency_symbol' => '$', 'currency_decimals' => 2 ]; } } return $settings; } // Function to format currency using settings function format_currency($amount) { $settings = get_company_settings(); return ($settings['currency_symbol'] ?? '$') . number_format((float)$amount, (int)($settings['currency_decimals'] ?? 2)); } /** * Calculate the current price of a product considering promotions. * * @param array|object $product The product data from DB. * @return float The effective price. */ function get_product_price($product) { $product = (array)$product; $price = (float)$product['price']; $today = date('Y-m-d'); $promo_active = !empty($product['promo_discount_percent']) && !empty($product['promo_date_from']) && !empty($product['promo_date_to']) && $today >= $product['promo_date_from'] && $today <= $product['promo_date_to']; if ($promo_active) { $discount = $price * ((float)$product['promo_discount_percent'] / 100); $price -= $discount; } return $price; } /** * Paginate a query result. * * @param PDO $pdo The PDO connection object. * @param string $query The base SQL query (without LIMIT/OFFSET). * @param array $params Query parameters. * @param int $default_limit Default items per page. * @return array Pagination result with keys: data, total_rows, total_pages, current_page, limit. */ function paginate_query($pdo, $query, $params = [], $default_limit = 20) { // Get current page $page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1; if ($page < 1) $page = 1; // Get limit (allow 20, 50, 100, or -1 for all) $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : $default_limit; // Validate limit if ($limit != -1 && !in_array($limit, [20, 50, 100])) { $limit = $default_limit; } // If limit is -1, fetch all if ($limit == -1) { $stmt = $pdo->prepare($query); $stmt->execute($params); $data = $stmt->fetchAll(); return [ 'data' => $data, 'total_rows' => count($data), 'total_pages' => 1, 'current_page' => 1, 'limit' => -1 ]; } // Count total rows using a subquery to handle complex queries safely $count_sql = "SELECT COUNT(*) FROM ($query) as count_table"; $stmt = $pdo->prepare($count_sql); $stmt->execute($params); $total_rows = $stmt->fetchColumn(); $total_pages = ceil($total_rows / $limit); if ($page > $total_pages && $total_pages > 0) $page = $total_pages; // Calculate offset $offset = ($page - 1) * $limit; if ($offset < 0) $offset = 0; // Add LIMIT and OFFSET $query_with_limit = $query . " LIMIT " . (int)$limit . " OFFSET " . (int)$offset; $stmt = $pdo->prepare($query_with_limit); $stmt->execute($params); $data = $stmt->fetchAll(); return [ 'data' => $data, 'total_rows' => $total_rows, 'total_pages' => $total_pages, 'current_page' => $page, 'limit' => $limit ]; } /** * Render pagination controls and limit selector. * * @param array $pagination The result array from paginate_query. * @param array $extra_params Additional GET parameters to preserve. */ function render_pagination_controls($pagination, $extra_params = []) { $page = $pagination['current_page']; $total_pages = $pagination['total_pages']; $limit = $pagination['limit']; // Build query string for limit change $params = array_merge($_GET, $extra_params); unset($params['page']); // Reset page when limit changes // Limit Selector $limits = [20, 50, 100, -1]; echo '
'; echo '
'; echo '
'; // Preserve other GET params foreach ($params as $key => $val) { if ($key !== 'limit') echo ''; } echo 'SHOW:'; echo ''; echo '
'; // Total Count echo 'Total: ' . $pagination['total_rows'] . ' items'; // Optional Total Amount (Sum) if (isset($pagination['total_amount_sum'])) { echo 'Total Sum: ' . format_currency($pagination['total_amount_sum']) . ''; } if ($total_pages > 0) { echo 'Page ' . $page . ' of ' . $total_pages . ''; } echo '
'; // Pagination Links if ($total_pages > 1) { echo ''; } echo '
'; } /** * Get the project root URL */ function get_base_url() { $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? 80) == 443 ? "https://" : "http://"; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $script = $_SERVER['SCRIPT_NAME'] ?? '/index.php'; $path = dirname($script); $path = str_replace('\\', '/', $path); // Corrected escaping for backslash $subdirs = ['/admin', '/api', '/includes', '/db', '/mail', '/ai', '/assets']; foreach ($subdirs as $dir) { if ($path === $dir || str_ends_with($path, $dir)) { $path = substr($path, 0, -strlen($dir)); break; } } if ($path === '' || $path === '.') $path = '/'; return $protocol . $host . rtrim($path, '/') . '/'; } if (!function_exists('str_ends_with')) { function str_ends_with($haystack, $needle) { $length = strlen($needle); if (!$length) return true; return substr($haystack, -$length) === $needle; } } /** * Initialize session with security and persistence improvements */ function init_session() { if (session_status() === PHP_SESSION_NONE) { // Set session lifetime to 1 week (604800 seconds) $lifetime = 604800; // Ensure gc_maxlifetime is at least as long as cookie lifetime ini_set('session.gc_maxlifetime', (string)$lifetime); // Set cookie parameters before session_start $isSecure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? 80) == 443; session_set_cookie_params([ 'lifetime' => $lifetime, 'path' => '/', 'domain' => '', 'secure' => $isSecure, 'httponly' => true, 'samesite' => 'Lax' ]); session_start(); } } function login_user($username, $password) { $pdo = db(); $stmt = $pdo->prepare("SELECT u.*, g.name as group_name, g.permissions FROM users u LEFT JOIN user_groups g ON u.group_id = g.id WHERE u.username = ? AND u.is_active = 1 LIMIT 1"); $stmt->execute([$username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if ($user && password_verify($password, $user['password'])) { init_session(); unset($user['password']); // Don't store hash in session $_SESSION['user'] = $user; return true; } return false; } function logout_user() { init_session(); unset($_SESSION['user']); session_destroy(); } function get_logged_user() { init_session(); return $_SESSION['user'] ?? null; } function require_login() { if (!get_logged_user()) { header('Location: ' . get_base_url() . 'login.php'); exit; } } function has_permission($permission) { $user = get_logged_user(); if (!$user) return false; // If permissions are missing from session (stale session), fetch from DB if (!isset($user['permissions'])) { $pdo = db(); $stmt = $pdo->prepare("SELECT g.permissions FROM users u JOIN user_groups g ON u.group_id = g.id WHERE u.id = ?"); $stmt->execute([$user['id']]); $perms = $stmt->fetchColumn(); $_SESSION['user']['permissions'] = $perms; $user['permissions'] = $perms; } $userPerms = $user['permissions'] ?: ''; // Admin has all permissions if ($userPerms === 'all') return true; $permissions = explode(',', $userPerms); $permissions = array_map('trim', $permissions); return in_array('all', $permissions) || in_array($permission, $permissions); } function require_permission($permission) { require_login(); if (!has_permission($permission)) { die("Access Denied: You do not have permission to view this page ($permission)."); } } function get_user_outlets($userId) { $pdo = db(); $stmt = $pdo->prepare("SELECT outlet_id FROM user_outlets WHERE user_id = ?"); $stmt->execute([$userId]); return $stmt->fetchAll(PDO::FETCH_COLUMN); } function can_access_outlet($userId, $outletId) { if (has_permission('all')) return true; $outlets = get_user_outlets($userId); return in_array($outletId, $outlets); } function create_backup() { $backupDir = __DIR__ . '/../storage/backups/'; if (!is_dir($backupDir)) { mkdir($backupDir, 0777, true); } $filename = 'backup_' . date('Y-m-d_H-i-s') . '.sql'; $path = $backupDir . $filename; // We'll use the environment variables from db/config.php $command = sprintf( 'mysqldump -h %s -u %s -p%s %s > %s', escapeshellarg(DB_HOST), escapeshellarg(DB_USER), escapeshellarg(DB_PASS), escapeshellarg(DB_NAME), escapeshellarg($path) ); exec($command, $output, $returnVar); if ($returnVar === 0) { // Enforce retention: keep 5 latest $files = glob($backupDir . '/*.sql'); if (count($files) > 5) { usort($files, function($a, $b) { return filemtime($a) - filemtime($b); }); while (count($files) > 5) { $oldest = array_shift($files); unlink($oldest); } } return $filename; } return false; } /** * Trigger auto backup if enabled and 24h passed since last backup. * Optimized to prevent session locking and hang. */ function trigger_auto_backup() { $settings = get_company_settings(); if (empty($settings['auto_backup_enabled'])) return; // Only Admin (with 'all' permission) should trigger backups to avoid overhead if (!has_permission('all')) return; $lastBackup = !empty($settings['last_auto_backup']) ? strtotime($settings['last_auto_backup']) : 0; $now = time(); // Run once every 24 hours if ($now - $lastBackup > 86400) { // Release session lock before starting a potentially long-running backup if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } if (create_backup()) { $pdo = db(); $stmt = $pdo->prepare("UPDATE company_settings SET last_auto_backup = NOW(), updated_at = NOW() LIMIT 1"); $stmt->execute(); } else { error_log("Auto backup failed at " . date('Y-m-d H:i:s')); $pdo = db(); $stmt = $pdo->prepare("UPDATE company_settings SET last_auto_backup = NOW(), updated_at = NOW() LIMIT 1"); $stmt->execute(); } } }