diff --git a/admin/ad_edit.php b/admin/ad_edit.php new file mode 100644 index 0000000..a6ce844 --- /dev/null +++ b/admin/ad_edit.php @@ -0,0 +1,146 @@ +prepare("SELECT * FROM ads_images WHERE id = ?"); + $stmt->execute([$id]); + $ad = $stmt->fetch(); + if ($ad) { + $isEdit = true; + } else { + header("Location: ads.php"); + exit; + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $title = trim($_POST['title']); + $sort_order = (int)$_POST['sort_order']; + $is_active = isset($_POST['is_active']) ? 1 : 0; + $image_path = $isEdit ? $ad['image_path'] : null; + + if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { + $uploadDir = __DIR__ . '/../assets/images/ads/'; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + + $fileInfo = pathinfo($_FILES['image']['name']); + $fileExt = strtolower($fileInfo['extension']); + $allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + + if (in_array($fileExt, $allowedExts)) { + $fileName = uniqid('ad_') . '.' . $fileExt; + $targetFile = $uploadDir . $fileName; + + if (move_uploaded_file($_FILES['image']['tmp_name'], $targetFile)) { + $image_path = 'assets/images/ads/' . $fileName; + } else { + $message = '
Failed to upload image.
'; + } + } else { + $message = '
Invalid file type. Allowed: jpg, png, gif, webp.
'; + } + } + + if (empty($image_path) && !$isEdit) { + $message = '
Image is required for new advertisements.
'; + } + + if (empty($message)) { + try { + if ($isEdit) { + $stmt = $pdo->prepare("UPDATE ads_images SET title = ?, sort_order = ?, is_active = ?, image_path = ? WHERE id = ?"); + $stmt->execute([$title, $sort_order, $is_active, $image_path, $id]); + header("Location: ads.php?success=updated"); + exit; + } else { + $stmt = $pdo->prepare("INSERT INTO ads_images (title, sort_order, is_active, image_path) VALUES (?, ?, ?, ?)"); + $stmt->execute([$title, $sort_order, $is_active, $image_path]); + header("Location: ads.php?success=created"); + exit; + } + } catch (PDOException $e) { + $message = '
Database error: ' . $e->getMessage() . '
'; + } + } +} + +if (!$isEdit) { + $ad = [ + 'title' => $_POST['title'] ?? '', + 'sort_order' => $_POST['sort_order'] ?? 0, + 'is_active' => 1, + 'image_path' => '' + ]; +} + +include 'includes/header.php'; +?> + +
+ Back to Ads Management +

+
+ + + +
+
+
+
+
+
+ + +
This will be shown as a caption on the image.
+
+
+ + +
Lower numbers appear first in the slider.
+
+
+
+ > + +
+
+
+
+
+ + +
+ Ad Image +
+ +
+
No Image Selected +
+ + + > +
Recommended size: 1920x1080 (HD) or 16:9 aspect ratio.
+
+
+
+
+
+ Cancel + +
+
+
+
+ + \ No newline at end of file diff --git a/admin/ads.php b/admin/ads.php new file mode 100644 index 0000000..823914c --- /dev/null +++ b/admin/ads.php @@ -0,0 +1,120 @@ +exec("CREATE TABLE IF NOT EXISTS ads_images ( + id INT AUTO_INCREMENT PRIMARY KEY, + image_path VARCHAR(255) NOT NULL, + title VARCHAR(255) DEFAULT NULL, + sort_order INT DEFAULT 0, + is_active TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +)"); + +if (isset($_GET['delete'])) { + $id = $_GET['delete']; + + // Get image path to delete file + $stmt = $pdo->prepare("SELECT image_path FROM ads_images WHERE id = ?"); + $stmt->execute([$id]); + $ad = $stmt->fetch(); + + if ($ad) { + $fullPath = __DIR__ . '/../' . $ad['image_path']; + if (file_exists($fullPath) && is_file($fullPath)) { + unlink($fullPath); + } + $pdo->prepare("DELETE FROM ads_images WHERE id = ?")->execute([$id]); + } + + header("Location: ads.php"); + exit; +} + +$query = "SELECT * FROM ads_images ORDER BY sort_order ASC, created_at DESC"; +$ads_pagination = paginate_query($pdo, $query); +$ads = $ads_pagination['data']; + +include 'includes/header.php'; +?> + +
+
+

Advertisement Slider

+

Manage pictures for the public ads display page.

+
+ + Add Image + +
+ +
+ +
+ These images will be displayed in a slider on the ads.php page. + You can upload up to 7 images for optimal performance. +
+
+ +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
OrderPreviewTitle / CaptionStatusActions
+ <?= htmlspecialchars($ad['title'] ?? '') ?> + +
+ +
+ + Active + + Inactive + + + + +
+ + No advertisement images found. Click "Add Image" to get started. +
+
+
+ +
+
+
+ + \ No newline at end of file diff --git a/admin/includes/footer.php b/admin/includes/footer.php index ca10c72..f30cd78 100644 --- a/admin/includes/footer.php +++ b/admin/includes/footer.php @@ -2,8 +2,14 @@ + + + \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index d11582d..0f9113c 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -121,6 +121,8 @@ body { color: var(--text-heading) !important; display: flex; align-items: center; + cursor: pointer; + user-select: none; } .sidebar-heading i { @@ -128,6 +130,22 @@ body { font-size: 1.1em; } +/* Chevron Rotation Logic */ +.sidebar-heading .chevron-icon { + font-size: 0.85rem !important; + transition: transform 0.3s ease; + margin-right: 0 !important; /* Override generic margin */ + color: var(--text-heading) !important; /* Softer color for chevron */ +} + +.sidebar-heading[aria-expanded="true"] .chevron-icon { + transform: rotate(180deg); +} + +.sidebar-heading.collapsed .chevron-icon { + transform: rotate(0deg); +} + .brand-logo { font-weight: 700; font-size: 1.5rem; @@ -393,4 +411,8 @@ body { } [data-theme="dark"] .table { color: var(--text-primary); +} +/* Ensure Dropdowns are always on top */ +.dropdown-menu { + z-index: 1050 !important; } \ No newline at end of file diff --git a/db/migrations/008_user_system.sql b/db/migrations/008_user_system.sql new file mode 100644 index 0000000..d23baea --- /dev/null +++ b/db/migrations/008_user_system.sql @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS user_groups ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + permissions TEXT, -- JSON or comma-separated list of capabilities + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + group_id INT, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + full_name VARCHAR(255), + email VARCHAR(255) UNIQUE, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE SET NULL +); + +-- Seed default groups +INSERT INTO user_groups (name, permissions) VALUES ('Administrator', 'all'); +INSERT INTO user_groups (name, permissions) VALUES ('Manager', 'manage_orders,manage_products,manage_reports'); +INSERT INTO user_groups (name, permissions) VALUES ('Cashier', 'pos,manage_orders'); +INSERT INTO user_groups (name, permissions) VALUES ('Waiter', 'pos'); + +-- Seed default admin user (password: admin123) +-- Using PHP to hash the password properly would be better, but for initial seeding we can use a placeholder if we have a setup script. +-- Let's just create the tables for now. diff --git a/db/migrations/009_user_outlets.sql b/db/migrations/009_user_outlets.sql new file mode 100644 index 0000000..28bc090 --- /dev/null +++ b/db/migrations/009_user_outlets.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS user_outlets ( + user_id INT(11) NOT NULL, + outlet_id INT(11) NOT NULL, + PRIMARY KEY (user_id, outlet_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE CASCADE +); diff --git a/db/migrations/010_ads_images.sql b/db/migrations/010_ads_images.sql new file mode 100644 index 0000000..e7d0658 --- /dev/null +++ b/db/migrations/010_ads_images.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS ads_images ( + id INT AUTO_INCREMENT PRIMARY KEY, + image_path VARCHAR(255) NOT NULL, + title VARCHAR(255) DEFAULT NULL, + sort_order INT DEFAULT 0, + is_active TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/includes/WablasService.php b/includes/WablasService.php index de7a124..14c0789 100644 --- a/includes/WablasService.php +++ b/includes/WablasService.php @@ -4,6 +4,7 @@ class WablasService { private $domain; private $token; private $secret_key; + private $is_enabled; public function __construct($pdo) { $this->pdo = $pdo; @@ -23,6 +24,7 @@ class WablasService { $this->domain = $settings['domain'] ?? ''; $this->token = $settings['token'] ?? ''; $this->secret_key = $settings['secret_key'] ?? ''; + $this->is_enabled = ($settings['is_enabled'] ?? '0') === '1'; } public function testConnection() { @@ -72,6 +74,10 @@ class WablasService { } public function sendMessage($phone, $message) { + if (!$this->is_enabled) { + return ['success' => false, 'message' => 'WhatsApp sending is disabled in settings']; + } + if (empty($this->domain) || empty($this->token)) { return ['success' => false, 'message' => 'Wablas configuration missing']; } diff --git a/includes/functions.php b/includes/functions.php index a499bf4..0d59db2 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -83,9 +83,9 @@ function paginate_query($pdo, $query, $params = [], $default_limit = 20) { // Add LIMIT and OFFSET // Note: PDO parameters for LIMIT/OFFSET can be tricky with some drivers, sticking to direct injection for integers is safe here - $query .= " LIMIT " . (int)$limit . " OFFSET " . (int)$offset; + $query_with_limit = $query . " LIMIT " . (int)$limit . " OFFSET " . (int)$offset; - $stmt = $pdo->prepare($query); + $stmt = $pdo->prepare($query_with_limit); $stmt->execute($params); $data = $stmt->fetchAll(); @@ -115,16 +115,16 @@ function render_pagination_controls($pagination, $extra_params = []) { // Limit Selector $limits = [20, 50, 100, -1]; - echo '
'; + echo '
'; - echo '
'; + echo '
'; echo '
'; // Preserve other GET params foreach ($params as $key => $val) { - if ($key !== 'limit') echo ''; + if ($key !== 'limit') echo ''; } - echo 'Show:'; - echo ''; foreach ($limits as $l) { $label = $l == -1 ? 'All' : $l; $selected = $limit == $l ? 'selected' : ''; @@ -134,26 +134,35 @@ function render_pagination_controls($pagination, $extra_params = []) { echo '
'; // Total Count - echo 'Total: ' . $pagination['total_rows'] . ''; + 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 '
'; +} + +/** + * Auth functions + */ + +function init_session() { + if (session_status() === PHP_SESSION_NONE) { + 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: /login.php'); + exit; + } +} + +function has_permission($permission) { + $user = get_logged_user(); + if (!$user) return false; + + $userPermissions = $user['permissions'] ?? ''; + if ($userPermissions === 'all') return true; + + $perms = explode(',', $userPermissions); + return in_array($permission, $perms); +} + +function require_permission($permission) { + require_login(); + if (!has_permission($permission)) { + http_response_code(403); + echo "Access Denied: You don't have permission to access this page."; + exit; + } } \ No newline at end of file diff --git a/kitchen.php b/kitchen.php index 2ff5373..707c081 100644 --- a/kitchen.php +++ b/kitchen.php @@ -1,8 +1,41 @@ query("SELECT * FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); -$current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1; +$currentUser = get_logged_user(); + +// Fetch outlets based on user assignment +if (has_permission('all')) { + $outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); +} else { + $stmt = $pdo->prepare(" + SELECT o.* FROM outlets o + JOIN user_outlets uo ON o.id = uo.outlet_id + WHERE uo.user_id = ? + ORDER BY o.name + "); + $stmt->execute([$currentUser['id']]); + $outlets = $stmt->fetchAll(PDO::FETCH_ASSOC); +} + +$current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : (count($outlets) > 0 ? (int)$outlets[0]['id'] : 1); + +// Security check: ensure user has access to this outlet +if (!has_permission('all')) { + $has_access = false; + foreach ($outlets as $o) { + if ($o['id'] == $current_outlet_id) { + $has_access = true; + break; + } + } + if (!$has_access && count($outlets) > 0) { + $current_outlet_id = (int)$outlets[0]['id']; + } +} ?> @@ -12,6 +45,7 @@ $current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1; Kitchen Display System + + + + + + + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..c952048 --- /dev/null +++ b/logout.php @@ -0,0 +1,5 @@ +query("SELECT * FROM outlets ORDER BY name")->fetchAll(); +} else { + $stmt = $pdo->prepare(" + SELECT o.* FROM outlets o + JOIN user_outlets uo ON o.id = uo.outlet_id + WHERE uo.user_id = ? + ORDER BY o.name + "); + $stmt->execute([$currentUser['id']]); + $outlets = $stmt->fetchAll(); +} + +$outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : (count($outlets) > 0 ? (int)$outlets[0]['id'] : 1); + +// Security check: ensure user has access to this outlet +if (!has_permission('all')) { + $has_access = false; + foreach ($outlets as $o) { + if ($o['id'] == $outlet_id) { + $has_access = true; + break; + } + } + if (!$has_access && count($outlets) > 0) { + $outlet_id = (int)$outlets[0]['id']; + } +} + $categories = $pdo->query("SELECT * FROM categories ORDER BY sort_order")->fetchAll(); $all_products = $pdo->query("SELECT p.*, c.name as category_name FROM products p JOIN categories c ON p.category_id = c.id")->fetchAll(); -$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll(); $payment_types = $pdo->query("SELECT * FROM payment_types WHERE is_active = 1 ORDER BY id")->fetchAll(); // Fetch variants @@ -16,7 +50,6 @@ foreach ($variants_raw as $v) { } $table_id = $_GET['table'] ?? '1'; // Default table -$outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1; $settings = get_company_settings(); $order_type = $_GET['order_type'] ?? 'takeaway'; @@ -79,7 +112,17 @@ foreach ($outlets as $o) { - Admin + +