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,
'currency_position' => 'before'
];
}
}
return $settings;
}
// Function to format currency using settings
function format_currency($amount) {
$settings = get_company_settings();
$symbol = $settings['currency_symbol'] ?? '$';
$decimals = (int)($settings['currency_decimals'] ?? 2);
$position = $settings['currency_position'] ?? 'before';
$formatted_number = number_format((float)$amount, $decimals);
if ($position === 'after') {
return $formatted_number . ' ' . $symbol;
} else {
return $symbol . $formatted_number;
}
}
/**
* 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) {
try {
$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
];
} catch (PDOException $e) {
die("Pagination Query Error (All): " . $e->getMessage() . "\nQuery: " . $query);
}
}
// Count total rows using a subquery to handle complex queries safely
// Strip ORDER BY from the query for the count to avoid SQL errors and improve performance
// Use a more robust regex that handles potential trailing semicolons or whitespace
$count_query = preg_replace('/ORDER\s+BY.*?(?=;|$)/is', '', $query);
$count_sql = "SELECT COUNT(*) FROM ($count_query) as count_table";
try {
$stmt = $pdo->prepare($count_sql);
$stmt->execute($params);
$total_rows = $stmt->fetchColumn();
} catch (PDOException $e) {
// If stripping ORDER BY failed or caused issues, try with the original query in subquery
try {
$count_sql_fallback = "SELECT COUNT(*) FROM ($query) as count_table";
$stmt = $pdo->prepare($count_sql_fallback);
$stmt->execute($params);
$total_rows = $stmt->fetchColumn();
} catch (PDOException $e2) {
die("Pagination Count Error: " . $e2->getMessage() . "\nSQL: " . $count_sql);
}
}
$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;
try {
$stmt = $pdo->prepare($query_with_limit);
$stmt->execute($params);
$data = $stmt->fetchAll();
} catch (PDOException $e) {
die("Pagination Data Error: " . $e->getMessage() . "\nSQL: " . $query_with_limit);
}
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 '';
// 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 || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? "") === "https" ? "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 || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? "") === "https";
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();
}
}
}