Autosave: 20260223-061939

This commit is contained in:
Flatlogic Bot 2026-02-23 06:19:40 +00:00
parent e5617b6c15
commit 37dfb898e7
21 changed files with 1779 additions and 139 deletions

146
admin/ad_edit.php Normal file
View File

@ -0,0 +1,146 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
$ad = null;
$message = '';
$isEdit = false;
if ($id) {
$stmt = $pdo->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 = '<div class="alert alert-danger">Failed to upload image.</div>';
}
} else {
$message = '<div class="alert alert-danger">Invalid file type. Allowed: jpg, png, gif, webp.</div>';
}
}
if (empty($image_path) && !$isEdit) {
$message = '<div class="alert alert-danger">Image is required for new advertisements.</div>';
}
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 = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
if (!$isEdit) {
$ad = [
'title' => $_POST['title'] ?? '',
'sort_order' => $_POST['sort_order'] ?? 0,
'is_active' => 1,
'image_path' => ''
];
}
include 'includes/header.php';
?>
<div class="mb-4">
<a href="ads.php" class="text-decoration-none text-muted mb-2 d-inline-block"><i class="bi bi-arrow-left"></i> Back to Ads Management</a>
<h2 class="fw-bold mb-0"><?= $isEdit ? 'Edit Advertisement' : 'Add New Advertisement' ?></h2>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label class="form-label">Title / Caption</label>
<input type="text" name="title" class="form-control" value="<?= htmlspecialchars($ad['title'] ?? '') ?>" placeholder="e.g. Special Offer 50% Off">
<div class="form-text">This will be shown as a caption on the image.</div>
</div>
<div class="mb-3">
<label class="form-label">Sort Order</label>
<input type="number" name="sort_order" class="form-control" value="<?= htmlspecialchars($ad['sort_order'] ?? 0) ?>">
<div class="form-text">Lower numbers appear first in the slider.</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="isActiveSwitch" <?= ($ad['is_active'] ?? 1) ? 'checked' : '' ?>>
<label class="form-check-label" for="isActiveSwitch">Active (Show in Slider)</label>
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Advertisement Image <span class="text-danger">*</span></label>
<?php if (!empty($ad['image_path'])): ?>
<div class="mb-2">
<img src="../<?= htmlspecialchars($ad['image_path']) ?>"
class="img-fluid rounded border shadow-sm"
style="max-height: 250px; width: 100%; object-fit: cover;"
alt="Ad Image">
</div>
<?php else: ?>
<div class="mb-2 p-5 bg-light text-center border rounded text-muted">
<i class="bi bi-images fs-1"></i><br>No Image Selected
</div>
<?php endif; ?>
<input type="file" name="image" class="form-control" accept="image/*" <?= !$isEdit ? 'required' : '' ?>>
<div class="form-text">Recommended size: 1920x1080 (HD) or 16:9 aspect ratio.</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-end gap-2">
<a href="ads.php" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary"><?= $isEdit ? 'Save Changes' : 'Upload Image' ?></button>
</div>
</form>
</div>
</div>
<?php include 'includes/footer.php'; ?>

120
admin/ads.php Normal file
View File

@ -0,0 +1,120 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
// Ensure the table exists (idempotent)
$pdo->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';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-0">Advertisement Slider</h2>
<p class="text-muted mb-0">Manage pictures for the public ads display page.</p>
</div>
<a href="ad_edit.php" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Add Image
</a>
</div>
<div class="alert alert-info border-0 shadow-sm d-flex align-items-center">
<i class="bi bi-info-circle-fill me-3 fs-4"></i>
<div>
These images will be displayed in a slider on the <strong><a href="../ads.php" target="_blank" class="alert-link">ads.php</a></strong> page.
You can upload up to 7 images for optimal performance.
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($ads_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Order</th>
<th style="width: 150px;">Preview</th>
<th>Title / Caption</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($ads as $ad): ?>
<tr>
<td class="ps-4 fw-medium"><?= $ad['sort_order'] ?></td>
<td>
<img src="../<?= htmlspecialchars($ad['image_path']) ?>"
class="rounded object-fit-cover border shadow-sm"
width="120" height="70"
alt="<?= htmlspecialchars($ad['title'] ?? '') ?>">
</td>
<td>
<div class="fw-bold"><?= htmlspecialchars($ad['title'] ?: 'No title') ?></div>
<small class="text-muted"><?= htmlspecialchars($ad['image_path']) ?></small>
</td>
<td>
<?php if ($ad['is_active']): ?>
<span class="badge bg-success-subtle text-success px-3">Active</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary px-3">Inactive</span>
<?php endif; ?>
</td>
<td class="text-end pe-4">
<a href="ad_edit.php?id=<?= $ad['id'] ?>" class="btn btn-sm btn-outline-primary me-1"><i class="bi bi-pencil"></i></a>
<a href="?delete=<?= $ad['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to delete this image?')"><i class="bi bi-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($ads)): ?>
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<i class="bi bi-images fs-1 d-block mb-3"></i>
No advertisement images found. Click "Add Image" to get started.
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($ads_pagination); ?>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -2,8 +2,14 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
// Global SweetAlert2 replacement for confirm()
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Initialize all dropdowns manually to ensure they work
var dropdownElementList = [].slice.call(document.querySelectorAll('.dropdown-toggle'))
var dropdownList = dropdownElementList.map(function (dropdownToggleEl) {
return new bootstrap.Dropdown(dropdownToggleEl)
});
// Global SweetAlert2 replacement for confirm()
const confirmLinks = document.querySelectorAll('a[onclick^="return confirm"]'); const confirmLinks = document.querySelectorAll('a[onclick^="return confirm"]');
confirmLinks.forEach(link => { confirmLinks.forEach(link => {

View File

@ -2,13 +2,24 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
// Ensure functions are available if not already
if (!function_exists('get_company_settings')) { // Ensure functions are available
// Attempt to locate config relative to this header file if (file_exists(__DIR__ . '/../../db/config.php')) {
if (file_exists(__DIR__ . '/../../db/config.php')) { require_once __DIR__ . '/../../db/config.php';
require_once __DIR__ . '/../../db/config.php';
}
} }
if (file_exists(__DIR__ . '/../../includes/functions.php')) {
require_once __DIR__ . '/../../includes/functions.php';
}
// Require login for all admin pages
if (function_exists('require_login')) {
require_login();
}
$currentUser = function_exists('get_logged_user') ? get_logged_user() : null;
$userName = $currentUser['full_name'] ?? ($currentUser['username'] ?? 'Admin');
$userGroup = $currentUser['group_name'] ?? 'System';
$userInitial = strtoupper(substr($userName, 0, 1));
$companySettings = function_exists('get_company_settings') ? get_company_settings() : []; $companySettings = function_exists('get_company_settings') ? get_company_settings() : [];
$companyName = $companySettings['company_name'] ?? 'Foody'; $companyName = $companySettings['company_name'] ?? 'Foody';
@ -19,6 +30,19 @@ $faviconUrl = $companySettings['favicon_url'] ?? '';
function isActive($page) { function isActive($page) {
return basename($_SERVER['PHP_SELF']) === $page ? 'active' : ''; return basename($_SERVER['PHP_SELF']) === $page ? 'active' : '';
} }
// Group active helper
function isGroupActive($pages) {
return in_array(basename($_SERVER['PHP_SELF']), $pages) ? 'show' : '';
}
function isGroupExpanded($pages) {
return in_array(basename($_SERVER['PHP_SELF']), $pages) ? 'true' : 'false';
}
function getGroupToggleClass($pages) {
return in_array(basename($_SERVER['PHP_SELF']), $pages) ? '' : 'collapsed';
}
?> ?>
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
@ -86,6 +110,14 @@ function isActive($page) {
font-weight: 700; font-weight: 700;
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; /* Clickable */
width: 100%;
background: none;
border: none;
text-align: left;
}
.sidebar-heading:hover {
color: var(--accent-color);
} }
.sidebar-heading i { .sidebar-heading i {
font-size: 1rem; font-size: 1rem;
@ -189,7 +221,7 @@ function isActive($page) {
<h5 class="fw-bold" style="color: var(--text-primary);">Menu</h5> <h5 class="fw-bold" style="color: var(--text-primary);">Menu</h5>
</div> </div>
<div class="sidebar-content"> <div class="sidebar-content accordion" id="sidebarAccordion">
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= isActive('index.php') ?>" href="index.php"> <a class="nav-link <?= isActive('index.php') ?>" href="index.php">
@ -198,101 +230,169 @@ function isActive($page) {
</li> </li>
</ul> </ul>
<h6 class="sidebar-heading"><i class="bi bi-shop"></i> POS & Operations</h6> <?php $posGroup = ['orders.php', 'ads.php']; ?>
<ul class="nav flex-column"> <div class="nav-group">
<li class="nav-item"> <a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($posGroup) ?>"
<a class="nav-link" href="../pos.php" target="_blank"> data-bs-toggle="collapse" href="#collapsePos" role="button" aria-expanded="<?= isGroupExpanded($posGroup) ?>" aria-controls="collapsePos">
<i class="bi bi-display me-2"></i> POS Terminal <span><i class="bi bi-shop"></i> POS & Operations</span>
</a> <i class="bi bi-chevron-down chevron-icon"></i>
</li> </a>
<li class="nav-item"> <div class="collapse <?= isGroupActive($posGroup) ?>" id="collapsePos" data-bs-parent="#sidebarAccordion">
<a class="nav-link <?= isActive('orders.php') ?>" href="orders.php"> <ul class="nav flex-column">
<i class="bi bi-receipt me-2"></i> Orders (POS) <li class="nav-item">
</a> <a class="nav-link" href="../pos.php" target="_blank">
</li> <i class="bi bi-display me-2"></i> POS Terminal
<li class="nav-item"> </a>
<a class="nav-link" href="../kitchen.php" target="_blank"> </li>
<i class="bi bi-fire me-2"></i> Kitchen View <li class="nav-item">
</a> <a class="nav-link <?= isActive('orders.php') ?>" href="orders.php">
</li> <i class="bi bi-receipt me-2"></i> Orders (POS)
</ul> </a>
</li>
<li class="nav-item">
<a class="nav-link" href="../kitchen.php" target="_blank">
<i class="bi bi-fire me-2"></i> Kitchen View
</a>
</li>
</ul>
</div>
</div>
<h6 class="sidebar-heading"><i class="bi bi-menu-button-wide"></i> Menu Management</h6> <?php $menuGroup = ['products.php', 'product_edit.php', 'categories.php', 'category_edit.php', 'product_variants.php']; ?>
<ul class="nav flex-column"> <div class="nav-group">
<li class="nav-item"> <a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($menuGroup) ?>"
<a class="nav-link <?= isActive('products.php') ?>" href="products.php"> data-bs-toggle="collapse" href="#collapseMenu" role="button" aria-expanded="<?= isGroupExpanded($menuGroup) ?>" aria-controls="collapseMenu">
<i class="bi bi-box-seam me-2"></i> Products <span><i class="bi bi-menu-button-wide"></i> Menu Management</span>
</a> <i class="bi bi-chevron-down chevron-icon"></i>
</li> </a>
<li class="nav-item"> <div class="collapse <?= isGroupActive($menuGroup) ?>" id="collapseMenu" data-bs-parent="#sidebarAccordion">
<a class="nav-link <?= isActive('categories.php') ?>" href="categories.php"> <ul class="nav flex-column">
<i class="bi bi-tags me-2"></i> Categories <li class="nav-item">
</a> <a class="nav-link <?= isActive('products.php') ?>" href="products.php">
</li> <i class="bi bi-box-seam me-2"></i> Products
</ul> </a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('categories.php') ?>" href="categories.php">
<i class="bi bi-tags me-2"></i> Categories
</a>
</li>
</ul>
</div>
</div>
<h6 class="sidebar-heading"><i class="bi bi-gear-wide-connected"></i> Restaurant Setup</h6> <?php $setupGroup = ['outlets.php', 'outlet_edit.php', 'areas.php', 'area_edit.php', 'tables.php', 'table_edit.php']; ?>
<ul class="nav flex-column"> <div class="nav-group">
<li class="nav-item"> <a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($setupGroup) ?>"
<a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php"> data-bs-toggle="collapse" href="#collapseSetup" role="button" aria-expanded="<?= isGroupExpanded($setupGroup) ?>" aria-controls="collapseSetup">
<i class="bi bi-shop me-2"></i> Outlets <span><i class="bi bi-gear-wide-connected"></i> Restaurant Setup</span>
</a> <i class="bi bi-chevron-down chevron-icon"></i>
</li> </a>
<li class="nav-item"> <div class="collapse <?= isGroupActive($setupGroup) ?>" id="collapseSetup" data-bs-parent="#sidebarAccordion">
<a class="nav-link <?= isActive('areas.php') ?>" href="areas.php"> <ul class="nav flex-column">
<i class="bi bi-geo-alt me-2"></i> Areas <li class="nav-item">
</a> <a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php">
</li> <i class="bi bi-shop me-2"></i> Outlets
<li class="nav-item"> </a>
<a class="nav-link <?= isActive('tables.php') ?>" href="tables.php"> </li>
<i class="bi bi-ui-checks-grid me-2"></i> Tables <li class="nav-item">
</a> <a class="nav-link <?= isActive('areas.php') ?>" href="areas.php">
</li> <i class="bi bi-geo-alt me-2"></i> Areas
</ul> </a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('tables.php') ?>" href="tables.php">
<i class="bi bi-ui-checks-grid me-2"></i> Tables
</a>
</li>
</ul>
</div>
</div>
<h6 class="sidebar-heading"><i class="bi bi-people"></i> People & Partners</h6> <?php $peopleGroup = ['customers.php', 'customer_edit.php', 'suppliers.php', 'supplier_edit.php', 'loyalty.php']; ?>
<ul class="nav flex-column"> <div class="nav-group">
<li class="nav-item"> <a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($peopleGroup) ?>"
<a class="nav-link <?= isActive('customers.php') ?>" href="customers.php"> data-bs-toggle="collapse" href="#collapsePeople" role="button" aria-expanded="<?= isGroupExpanded($peopleGroup) ?>" aria-controls="collapsePeople">
<i class="bi bi-people-fill me-2"></i> Customers <span><i class="bi bi-people"></i> People & Partners</span>
</a> <i class="bi bi-chevron-down chevron-icon"></i>
</li> </a>
<li class="nav-item"> <div class="collapse <?= isGroupActive($peopleGroup) ?>" id="collapsePeople" data-bs-parent="#sidebarAccordion">
<a class="nav-link <?= isActive('suppliers.php') ?>" href="suppliers.php"> <ul class="nav flex-column">
<i class="bi bi-truck me-2"></i> Suppliers <li class="nav-item">
</a> <a class="nav-link <?= isActive('customers.php') ?>" href="customers.php">
</li> <i class="bi bi-people-fill me-2"></i> Customers
<li class="nav-item"> </a>
<a class="nav-link <?= isActive('loyalty.php') ?>" href="loyalty.php"> </li>
<i class="bi bi-award me-2"></i> Loyalty <li class="nav-item">
</a> <a class="nav-link <?= isActive('suppliers.php') ?>" href="suppliers.php">
</li> <i class="bi bi-truck me-2"></i> Suppliers
</ul> </a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('loyalty.php') ?>" href="loyalty.php">
<i class="bi bi-award me-2"></i> Loyalty
</a>
</li>
</ul>
</div>
</div>
<h6 class="sidebar-heading"><i class="bi bi-sliders"></i> Settings</h6> <?php $userGroupPages = ['users.php', 'user_edit.php', 'user_groups.php', 'user_group_edit.php']; ?>
<ul class="nav flex-column mb-5"> <div class="nav-group">
<li class="nav-item"> <a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($userGroupPages) ?>"
<a class="nav-link <?= isActive('payment_types.php') ?>" href="payment_types.php"> data-bs-toggle="collapse" href="#collapseUsers" role="button" aria-expanded="<?= isGroupExpanded($userGroupPages) ?>" aria-controls="collapseUsers">
<i class="bi bi-credit-card me-2"></i> Payment Types <span><i class="bi bi-person-badge"></i> User Management</span>
</a> <i class="bi bi-chevron-down chevron-icon"></i>
</li> </a>
<li class="nav-item"> <div class="collapse <?= isGroupActive($userGroupPages) ?>" id="collapseUsers" data-bs-parent="#sidebarAccordion">
<a class="nav-link <?= isActive('integrations.php') ?>" href="integrations.php"> <ul class="nav flex-column">
<i class="bi bi-plugin me-2"></i> Integrations <li class="nav-item">
</a> <a class="nav-link <?= isActive('users.php') ?>" href="users.php">
</li> <i class="bi bi-people me-2"></i> Users
<li class="nav-item"> </a>
<a class="nav-link <?= isActive('company.php') ?>" href="company.php"> </li>
<i class="bi bi-building me-2"></i> Company <li class="nav-item">
</a> <a class="nav-link <?= isActive('user_groups.php') ?>" href="user_groups.php">
</li> <i class="bi bi-shield-lock me-2"></i> Roles / Groups
</a>
</li>
</ul>
</div>
</div>
<li class="nav-item border-top mt-2 pt-2"> <?php $settingsGroup = ['payment_types.php', 'payment_type_edit.php', 'integrations.php', 'company.php']; ?>
<a class="nav-link text-muted" href="../index.php" target="_blank"> <div class="nav-group">
<i class="bi bi-box-arrow-up-right me-2"></i> View Site <a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($settingsGroup) ?>"
</a> data-bs-toggle="collapse" href="#collapseSettings" role="button" aria-expanded="<?= isGroupExpanded($settingsGroup) ?>" aria-controls="collapseSettings">
</li> <span><i class="bi bi-sliders"></i> Settings</span>
</ul> <i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($settingsGroup) ?>" id="collapseSettings" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column mb-5">
<li class="nav-item">
<a class="nav-link <?= isActive('payment_types.php') ?>" href="payment_types.php">
<i class="bi bi-credit-card me-2"></i> Payment Types
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('integrations.php') ?>" href="integrations.php">
<i class="bi bi-plugin me-2"></i> Integrations
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('company.php') ?>" href="company.php">
<i class="bi bi-building me-2"></i> Company
</a>
</li>
<li class="nav-item border-top mt-2 pt-2">
<a class="nav-link text-muted" href="../index.php" target="_blank">
<i class="bi bi-box-arrow-up-right me-2"></i> View Site
</a>
</li>
</ul>
</div>
</div>
</div> </div>
</div> </div>
@ -319,15 +419,15 @@ function isActive($page) {
<!-- User Profile --> <!-- User Profile -->
<div class="dropdown"> <div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false" style="color: var(--text-primary);"> <a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false" style="color: var(--text-primary);">
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center text-white me-2 shadow-sm" style="width:38px;height:38px; font-weight:600;">A</div> <div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center text-white me-2 shadow-sm" style="width:38px;height:38px; font-weight:600;"><?= $userInitial ?></div>
<span class="d-none d-sm-inline fw-medium">Admin</span> <span class="d-none d-sm-inline fw-medium"><?= htmlspecialchars($userName) ?></span>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="userDropdown"> <ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="userDropdown">
<li><span class="dropdown-item-text text-muted small">Signed in as Admin</span></li> <li><span class="dropdown-item-text text-muted small">Signed in as <?= htmlspecialchars($userGroup) ?></span></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="company.php"><i class="bi bi-building me-2"></i> Company Settings</a></li> <li><a class="dropdown-item" href="company.php"><i class="bi bi-building me-2"></i> Company Settings</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#"><i class="bi bi-box-arrow-right me-2"></i> Logout</a></li> <li><a class="dropdown-item text-danger" href="/logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
<?php <?php
require_once __DIR__ . '/../db/config.php'; require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db(); $pdo = db();
@ -33,7 +34,7 @@ include 'includes/header.php';
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div>
<h2 class="fw-bold mb-1">Dashboard</h2> <h2 class="fw-bold mb-1">Dashboard</h2>
<p class="text-muted">Welcome back, Admin!</p> <p class="text-muted">Welcome back, <?= htmlspecialchars($userName) ?>!</p>
</div> </div>
<div> <div>
<a href="orders.php" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i> New Order</a> <a href="orders.php" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i> New Order</a>
@ -164,4 +165,4 @@ include 'includes/header.php';
</div> </div>
</div> </div>
<?php include 'includes/footer.php'; ?> <?php include 'includes/footer.php'; ?>

View File

@ -28,9 +28,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Wablas // Wablas
if ($provider === 'wablas') { if ($provider === 'wablas') {
$keys = ['domain', 'token', 'secret_key', 'order_template']; $keys = ['domain', 'token', 'secret_key', 'order_template', 'is_enabled'];
foreach ($keys as $k) { foreach ($keys as $k) {
$val = $_POST[$k] ?? ''; $val = $_POST[$k] ?? '0';
if ($k === 'is_enabled' && !isset($_POST[$k])) {
$val = '0';
}
$stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)"); $stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
$stmt->execute(['wablas', $k, $val]); $stmt->execute(['wablas', $k, $val]);
} }
@ -73,6 +76,7 @@ $wablasDom = getSetting($allSettings, 'wablas', 'domain');
$wablasTok = getSetting($allSettings, 'wablas', 'token'); $wablasTok = getSetting($allSettings, 'wablas', 'token');
$wablasSecKey = getSetting($allSettings, 'wablas', 'secret_key'); $wablasSecKey = getSetting($allSettings, 'wablas', 'secret_key');
$wablasTemplate = getSetting($allSettings, 'wablas', 'order_template'); $wablasTemplate = getSetting($allSettings, 'wablas', 'order_template');
$wablasEnabled = getSetting($allSettings, 'wablas', 'is_enabled');
// Default template if empty // Default template if empty
if (empty($wablasTemplate)) { if (empty($wablasTemplate)) {
@ -147,10 +151,16 @@ require_once __DIR__ . '/includes/header.php';
<div class="card shadow h-100"> <div class="card shadow h-100">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 fw-bold text-success">Wablas WhatsApp</h6> <h6 class="m-0 fw-bold text-success">Wablas WhatsApp</h6>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_enabled" id="is_enabled_switch" form="wablas_form" value="1" <?= $wablasEnabled === '1' ? 'checked' : '' ?>>
<label class="form-check-label" for="is_enabled_switch">Enabled</label>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST"> <form method="POST" id="wablas_form">
<input type="hidden" name="provider" value="wablas"> <input type="hidden" name="provider" value="wablas">
<!-- Also keep a hidden input to send '0' if checkbox is unchecked (handled in PHP POST block too) -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Domain</label> <label class="form-label">Domain</label>
<input type="text" class="form-control" name="domain" placeholder="https://..." value="<?= htmlspecialchars($wablasDom) ?>"> <input type="text" class="form-control" name="domain" placeholder="https://..." value="<?= htmlspecialchars($wablasDom) ?>">

View File

@ -38,9 +38,6 @@ if (!empty($_GET['end_date'])) {
// Filter: Search (Order No) // Filter: Search (Order No)
if (!empty($_GET['search'])) { if (!empty($_GET['search'])) {
// Exact match for ID usually, but LIKE might be more user friendly if they type partial?
// "search by order no" usually implies ID. Let's stick to ID or simple LIKE.
// If numeric, assume ID.
if (is_numeric($_GET['search'])) { if (is_numeric($_GET['search'])) {
$where[] = "o.id = :search"; $where[] = "o.id = :search";
$params[':search'] = $_GET['search']; $params[':search'] = $_GET['search'];
@ -49,8 +46,13 @@ if (!empty($_GET['search'])) {
$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; $where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
// Changed alias 'out' to 'ot' to avoid reserved keyword conflict // Calculate Total Sum for filtered orders
// Added join to payment_types to get payment name $sum_query = "SELECT SUM(total_amount) as total_sum FROM orders o $where_clause";
$stmt_sum = $pdo->prepare($sum_query);
$stmt_sum->execute($params);
$total_sum = $stmt_sum->fetchColumn() ?: 0;
// Main Query
$query = "SELECT o.*, ot.name as outlet_name, pt.name as payment_type_name, $query = "SELECT o.*, ot.name as outlet_name, pt.name as payment_type_name,
(SELECT GROUP_CONCAT(CONCAT(p.name, ' x', oi.quantity) SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items_summary (SELECT GROUP_CONCAT(CONCAT(p.name, ' x', oi.quantity) SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items_summary
FROM orders o FROM orders o
@ -62,6 +64,9 @@ $query = "SELECT o.*, ot.name as outlet_name, pt.name as payment_type_name,
$orders_pagination = paginate_query($pdo, $query, $params); $orders_pagination = paginate_query($pdo, $query, $params);
$orders = $orders_pagination['data']; $orders = $orders_pagination['data'];
// Add total sum to pagination object for rendering
$orders_pagination['total_amount_sum'] = $total_sum;
include 'includes/header.php'; include 'includes/header.php';
?> ?>
@ -72,6 +77,59 @@ include 'includes/header.php';
</span> </span>
</div> </div>
<!-- Summary Stats -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0 bg-primary bg-opacity-10 text-primary p-3 rounded">
<i class="bi bi-currency-dollar fs-4"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0 small text-uppercase fw-bold">Total Revenue</h6>
<div class="fs-4 fw-bold text-primary"><?= format_currency($total_sum) ?></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0 bg-success bg-opacity-10 text-success p-3 rounded">
<i class="bi bi-receipt fs-4"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0 small text-uppercase fw-bold">Total Orders</h6>
<div class="fs-4 fw-bold text-success"><?= $orders_pagination['total_rows'] ?></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0 bg-info bg-opacity-10 text-info p-3 rounded">
<i class="bi bi-calendar-event fs-4"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0 small text-uppercase fw-bold">Date Range</h6>
<div class="small fw-bold">
<?= !empty($_GET['start_date']) ? date('M d, Y', strtotime($_GET['start_date'])) : 'Start' ?>
-
<?= !empty($_GET['end_date']) ? date('M d, Y', strtotime($_GET['end_date'])) : 'Today' ?>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters --> <!-- Filters -->
<div class="card border-0 shadow-sm mb-4"> <div class="card border-0 shadow-sm mb-4">
<div class="card-body bg-light"> <div class="card-body bg-light">
@ -148,9 +206,9 @@ include 'includes/header.php';
</td> </td>
<td> <td>
<?php if (!empty($order['customer_name'])): ?> <?php if (!empty($order['customer_name'])): ?>
<div><?= htmlspecialchars($order['customer_name']) ?></div> <div><?= htmlspecialchars((string)($order['customer_name'] ?? '')) ?></div>
<?php if (!empty($order['customer_phone'])): ?> <?php if (!empty($order['customer_phone'])): ?>
<small class="text-muted"><i class="bi bi-telephone me-1"></i><?= htmlspecialchars($order['customer_phone']) ?></small> <small class="text-muted"><i class="bi bi-telephone me-1"></i><?= htmlspecialchars((string)($order['customer_phone'] ?? '')) ?></small>
<?php endif; ?> <?php endif; ?>
<?php else: ?> <?php else: ?>
<span class="text-muted small">Guest</span> <span class="text-muted small">Guest</span>
@ -170,12 +228,12 @@ include 'includes/header.php';
</td> </td>
<td> <td>
<?php if ($order['order_type'] === 'dine-in' && $order['table_number']): ?> <?php if ($order['order_type'] === 'dine-in' && $order['table_number']): ?>
<span class="badge bg-secondary">Table <?= htmlspecialchars($order['table_number']) ?></span> <span class="badge bg-secondary">Table <?= htmlspecialchars((string)($order['table_number'] ?? '')) ?></span>
<?php else: ?> <?php else: ?>
<span class="badge bg-light text-dark border"><?= ucfirst($order['order_type']) ?></span> <span class="badge bg-light text-dark border"><?= ucfirst($order['order_type']) ?></span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td><small class="text-muted"><?= htmlspecialchars($order['items_summary']) ?></small></td> <td><small class="text-muted"><?= htmlspecialchars((string)($order['items_summary'] ?? '')) ?></small></td>
<td><?= format_currency($order['total_amount']) ?></td> <td><?= format_currency($order['total_amount']) ?></td>
<td> <td>
<?php <?php
@ -189,7 +247,7 @@ include 'includes/header.php';
}; };
?> ?>
<span class="badge <?= $payment_badge ?> text-dark bg-opacity-25 border border-<?= str_replace('bg-', '', $payment_badge) ?>"> <span class="badge <?= $payment_badge ?> text-dark bg-opacity-25 border border-<?= str_replace('bg-', '', $payment_badge) ?>">
<?= htmlspecialchars($payment_name) ?> <?= htmlspecialchars((string)($payment_name ?? '')) ?>
</span> </span>
</td> </td>
<td> <td>

204
admin/user_edit.php Normal file
View File

@ -0,0 +1,204 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('all');
$id = $_GET['id'] ?? null;
$user = null;
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
}
if (!$user) {
header('Location: users.php');
exit;
}
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$full_name = $_POST['full_name'];
$username = $_POST['username'];
$email = $_POST['email'];
$group_id = $_POST['group_id'];
$is_active = isset($_POST['is_active']) ? 1 : 0;
$assigned_outlets = $_POST['outlets'] ?? [];
// Check if username changed and if new one exists
if ($username !== $user['username']) {
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ? AND id != ?");
$stmt->execute([$username, $id]);
if ($stmt->fetch()) {
$message = '<div class="alert alert-danger">Username already taken.</div>';
}
}
if (!$message) {
$pdo->beginTransaction();
try {
$sql = "UPDATE users SET full_name = ?, username = ?, email = ?, group_id = ?, is_active = ? WHERE id = ?";
$params = [$full_name, $username, $email, $group_id, $is_active, $id];
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
// Update password if provided
if (!empty($_POST['password'])) {
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$pdo->prepare("UPDATE users SET password = ? WHERE id = ?")->execute([$password, $id]);
}
// Update assigned outlets
$pdo->prepare("DELETE FROM user_outlets WHERE user_id = ?")->execute([$id]);
if (!empty($assigned_outlets)) {
$stmt_outlet = $pdo->prepare("INSERT INTO user_outlets (user_id, outlet_id) VALUES (?, ?)");
foreach ($assigned_outlets as $outlet_id) {
$stmt_outlet->execute([$id, $outlet_id]);
}
}
$pdo->commit();
$message = '<div class="alert alert-success">User updated successfully!</div>';
// Refresh user data
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$pdo->rollBack();
$message = '<div class="alert alert-danger">Error updating user: ' . $e->getMessage() . '</div>';
}
}
}
$groups = $pdo->query("SELECT * FROM user_groups ORDER BY name")->fetchAll();
$all_outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll();
$user_outlets = $pdo->prepare("SELECT outlet_id FROM user_outlets WHERE user_id = ?");
$user_outlets->execute([$id]);
$assigned_outlet_ids = $user_outlets->fetchAll(PDO::FETCH_COLUMN);
include 'includes/header.php';
?>
<div class="mb-4">
<a href="users.php" class="text-decoration-none text-muted small"><i class="bi bi-arrow-left"></i> Back to Users</a>
<h2 class="fw-bold mt-2">Edit User: <?= htmlspecialchars($user['username']) ?></h2>
</div>
<?= $message ?>
<div class="row">
<div class="col-md-8">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<form method="POST">
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">FULL NAME</label>
<input type="text" name="full_name" class="form-control" value="<?= htmlspecialchars($user['full_name']) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">USERNAME</label>
<input type="text" name="username" class="form-control" value="<?= htmlspecialchars($user['username']) ?>" required>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">EMAIL</label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($user['email']) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">USER GROUP / ROLE</label>
<select name="group_id" class="form-select" required>
<?php foreach ($groups as $group): ?>
<option value="<?= $group['id'] ?>" <?= $user['group_id'] == $group['id'] ? 'selected' : '' ?>><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted">NEW PASSWORD (LEAVE BLANK TO KEEP CURRENT)</label>
<input type="password" name="password" class="form-control" placeholder="******">
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted d-block mb-2">ASSIGNED OUTLETS</label>
<div class="row">
<?php foreach ($all_outlets as $outlet): ?>
<div class="col-md-4 mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="outlets[]" value="<?= $outlet['id'] ?>" id="outlet_<?= $outlet['id'] ?>" <?= in_array($outlet['id'], $assigned_outlet_ids) ? 'checked' : '' ?>>
<label class="form-check-label small" for="outlet_<?= $outlet['id'] ?>">
<?= htmlspecialchars($outlet['name']) ?>
</label>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="form-text mt-1">Assign one or more outlets to this user.</div>
</div>
<div class="mb-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="isActiveSwitch" <?= $user['is_active'] ? 'checked' : '' ?>>
<label class="form-check-label fw-bold text-muted small" for="isActiveSwitch">ACTIVE ACCOUNT</label>
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-end gap-2">
<a href="users.php" class="btn btn-light rounded-pill px-4">Cancel</a>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold">Update User</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm rounded-4 bg-primary bg-gradient text-white shadow">
<div class="card-body p-4">
<h5 class="fw-bold mb-3">User Info</h5>
<div class="d-flex align-items-center mb-3">
<div class="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center me-3" style="width:50px;height:50px; font-weight:700; font-size:1.5rem;">
<?= strtoupper(substr($user['full_name'] ?: $user['username'], 0, 1)) ?>
</div>
<div>
<div class="fw-bold"><?= htmlspecialchars($user['full_name']) ?></div>
<div class="small opacity-75">Member since <?= date('M Y', strtotime($user['created_at'])) ?></div>
</div>
</div>
<div class="small">
<div class="mb-1"><i class="bi bi-envelope me-2"></i> <?= htmlspecialchars($user['email']) ?></div>
<div><i class="bi bi-person-badge me-2"></i> User ID: #<?= $user['id'] ?></div>
</div>
<hr class="opacity-25 my-3">
<div class="small">
<div class="fw-bold mb-2">Assigned Outlets:</div>
<?php if (empty($assigned_outlet_ids)): ?>
<div class="opacity-75">No outlets assigned.</div>
<?php else: ?>
<ul class="list-unstyled mb-0 opacity-75">
<?php foreach ($all_outlets as $outlet): ?>
<?php if (in_array($outlet['id'], $assigned_outlet_ids)): ?>
<li><i class="bi bi-geo-alt me-1"></i> <?= htmlspecialchars($outlet['name']) ?></li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

141
admin/user_groups.php Normal file
View File

@ -0,0 +1,141 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('all'); // Only super admin can manage groups
$message = '';
// Handle Add Group
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_group') {
$name = $_POST['name'];
$permissions = isset($_POST['perms']) ? implode(',', $_POST['perms']) : '';
$stmt = $pdo->prepare("INSERT INTO user_groups (name, permissions) VALUES (?, ?)");
if ($stmt->execute([$name, $permissions])) {
$message = '<div class="alert alert-success border-0 shadow-sm rounded-3"><i class="bi bi-check-circle-fill me-2"></i>Group added successfully!</div>';
} else {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3"><i class="bi bi-exclamation-triangle-fill me-2"></i>Error adding group.</div>';
}
}
// Handle Delete
if (isset($_GET['delete'])) {
$id = $_GET['delete'];
// Prevent deleting the Administrator group (id 1 usually)
if ($id != 1) {
$pdo->prepare("DELETE FROM user_groups WHERE id = ?")->execute([$id]);
}
header("Location: user_groups.php");
exit;
}
$groups = $pdo->query("SELECT * FROM user_groups ORDER BY id ASC")->fetchAll();
$available_perms = [
'manage_orders' => 'Manage Orders',
'manage_products' => 'Manage Products/Menu',
'manage_reports' => 'View Reports',
'manage_settings' => 'System Settings',
'pos' => 'Access POS Terminal',
'kitchen' => 'Access Kitchen View',
'all' => 'Full Administrator Access'
];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1 text-dark">User Groups / Roles</h2>
<p class="text-muted mb-0">Define what different users can do</p>
</div>
<button class="btn btn-primary shadow-sm rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#addGroupModal">
<i class="bi bi-plus-lg me-1"></i> Add Group
</button>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm rounded-3 overflow-hidden">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Group Name</th>
<th>Permissions</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td class="ps-4"><?= $group['id'] ?></td>
<td><span class="fw-bold"><?= htmlspecialchars($group['name']) ?></span></td>
<td>
<?php
if ($group['permissions'] === 'all') {
echo '<span class="badge bg-danger rounded-pill px-3">Full Access</span>';
} else {
$perms = explode(',', $group['permissions']);
foreach ($perms as $p) {
if (isset($available_perms[$p])) {
echo '<span class="badge bg-info bg-opacity-10 text-info border border-info border-opacity-25 rounded-pill px-2 me-1 mb-1" style="font-weight:500;">' . $available_perms[$p] . '</span>';
}
}
}
?>
</td>
<td class="text-end pe-4">
<?php if ($group['id'] != 1): ?>
<a href="?delete=<?= $group['id'] ?>" class="btn btn-sm btn-outline-danger border-0 rounded-circle" onclick="return confirm('Delete this group?')">
<i class="bi bi-trash"></i>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Add Group Modal -->
<div class="modal fade" id="addGroupModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0 pb-0 px-4 pt-4">
<h5 class="modal-title fw-bold">Create New Group</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body p-4">
<input type="hidden" name="action" value="add_group">
<div class="mb-4">
<label class="form-label small fw-bold text-muted">GROUP NAME</label>
<input type="text" name="name" class="form-control form-control-lg border-0 bg-light" placeholder="e.g. Supervisor" required style="border-radius: 12px;">
</div>
<label class="form-label small fw-bold text-muted mb-3">PERMISSIONS</label>
<div class="row g-2">
<?php foreach ($available_perms as $key => $label): ?>
<div class="col-12">
<div class="form-check form-switch p-3 bg-light rounded-3 d-flex align-items-center justify-content-between">
<label class="form-check-label ms-0 fw-medium" for="perm_<?= $key ?>"><?= $label ?></label>
<input class="form-check-input" type="checkbox" name="perms[]" value="<?= $key ?>" id="perm_<?= $key ?>">
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold">Save Group</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

222
admin/users.php Normal file
View File

@ -0,0 +1,222 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('all'); // Only super admin can manage users
$message = '';
// Handle Add User
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_user') {
$username = $_POST['username'];
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$full_name = $_POST['full_name'];
$email = $_POST['email'];
$group_id = $_POST['group_id'];
$assigned_outlets = $_POST['outlets'] ?? [];
// Check if user exists
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if ($stmt->fetch()) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3"><i class="bi bi-exclamation-triangle-fill me-2"></i>Username already exists.</div>';
} else {
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("INSERT INTO users (username, password, full_name, email, group_id) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$username, $password, $full_name, $email, $group_id]);
$user_id = $pdo->lastInsertId();
if (!empty($assigned_outlets)) {
$stmt_outlet = $pdo->prepare("INSERT INTO user_outlets (user_id, outlet_id) VALUES (?, ?)");
foreach ($assigned_outlets as $outlet_id) {
$stmt_outlet->execute([$user_id, $outlet_id]);
}
}
$pdo->commit();
$message = '<div class="alert alert-success border-0 shadow-sm rounded-3"><i class="bi bi-check-circle-fill me-2"></i>User added successfully!</div>';
} catch (Exception $e) {
$pdo->rollBack();
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3"><i class="bi bi-exclamation-triangle-fill me-2"></i>Error adding user: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
$id = $_GET['delete'];
$currentUser = get_logged_user();
// Prevent deleting yourself
if ($id != $currentUser['id']) {
$pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$id]);
}
header("Location: users.php");
exit;
}
// Fetch users with group names and assigned outlets
$users_sql = "
SELECT u.*, g.name as group_name, GROUP_CONCAT(o.name SEPARATOR ', ') as assigned_outlets
FROM users u
LEFT JOIN user_groups g ON u.group_id = g.id
LEFT JOIN user_outlets uo ON u.id = uo.user_id
LEFT JOIN outlets o ON uo.outlet_id = o.id
GROUP BY u.id
ORDER BY u.id ASC
";
$users = $pdo->query($users_sql)->fetchAll();
$groups = $pdo->query("SELECT * FROM user_groups ORDER BY name")->fetchAll();
$all_outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1 text-dark">User Management</h2>
<p class="text-muted mb-0">Manage system users and their access levels</p>
</div>
<button class="btn btn-primary shadow-sm rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#addUserModal">
<i class="bi bi-person-plus-fill me-1"></i> Add User
</button>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm rounded-3 overflow-hidden">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">User</th>
<th>Email</th>
<th>Role / Group</th>
<th>Outlets</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 text-primary rounded-circle d-flex align-items-center justify-content-center me-3 shadow-sm" style="width:42px;height:42px; font-weight:600;">
<?= strtoupper(substr($user['full_name'] ?: $user['username'], 0, 1)) ?>
</div>
<div>
<div class="fw-bold text-dark"><?= htmlspecialchars($user['full_name']) ?></div>
<div class="text-muted small">@<?= htmlspecialchars($user['username']) ?></div>
</div>
</div>
</td>
<td><?= htmlspecialchars($user['email']) ?></td>
<td>
<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25 rounded-pill px-3" style="font-weight:600;">
<?= htmlspecialchars($user['group_name'] ?? 'No Group') ?>
</span>
</td>
<td>
<div class="small text-muted" style="max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<?= htmlspecialchars($user['assigned_outlets'] ?: 'None') ?>">
<?= htmlspecialchars($user['assigned_outlets'] ?: 'None') ?>
</div>
</td>
<td>
<?php if ($user['is_active']): ?>
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25 rounded-pill px-3">Active</span>
<?php else: ?>
<span class="badge bg-danger bg-opacity-10 text-danger border border-danger border-opacity-25 rounded-pill px-3">Inactive</span>
<?php endif; ?>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<a href="user_edit.php?id=<?= $user['id'] ?>" class="btn btn-sm btn-icon-soft edit" title="Edit User">
<i class="bi bi-pencil-fill"></i>
</a>
<?php if ($user['id'] != get_logged_user()['id']): ?>
<a href="?delete=<?= $user['id'] ?>" class="btn btn-sm btn-icon-soft delete" onclick="return confirm('Delete this user?')" title="Delete">
<i class="bi bi-trash-fill"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Add User Modal -->
<div class="modal fade" id="addUserModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0 pb-0 px-4 pt-4">
<h5 class="modal-title fw-bold">Add New User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body p-4">
<input type="hidden" name="action" value="add_user">
<div class="row g-3">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">FULL NAME</label>
<input type="text" name="full_name" class="form-control border-0 bg-light" placeholder="e.g. John Doe" required style="border-radius: 12px;">
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">EMAIL</label>
<input type="email" name="email" class="form-control border-0 bg-light" placeholder="e.g. john@example.com" required style="border-radius: 12px;">
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">USERNAME</label>
<input type="text" name="username" class="form-control border-0 bg-light" placeholder="e.g. jdoe" required style="border-radius: 12px;">
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">PASSWORD</label>
<input type="password" name="password" class="form-control border-0 bg-light" placeholder="******" required style="border-radius: 12px;">
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">USER GROUP / ROLE</label>
<select name="group_id" class="form-select border-0 bg-light" required style="border-radius: 12px;">
<?php foreach ($groups as $group): ?>
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted d-block mb-2">ASSIGNED OUTLETS</label>
<div class="p-3 bg-light rounded-4">
<div class="row">
<?php foreach ($all_outlets as $outlet): ?>
<div class="col-md-6 mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="outlets[]" value="<?= $outlet['id'] ?>" id="new_outlet_<?= $outlet['id'] ?>">
<label class="form-check-label small" for="new_outlet_<?= $outlet['id'] ?>">
<?= htmlspecialchars($outlet['name']) ?>
</label>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold">Create User</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

302
ads.php Normal file
View File

@ -0,0 +1,302 @@
<?php
require_once __DIR__ . '/db/config.php';
$pdo = db();
// Fetch active advertisement images
$stmt = $pdo->prepare("SELECT * FROM ads_images WHERE is_active = 1 ORDER BY sort_order ASC");
$stmt->execute();
$ads = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get current outlet (default to 1 if not specified)
$outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
// Fetch company settings for branding
$stmt = $pdo->query("SELECT * FROM company_settings LIMIT 1");
$company = $stmt->fetch(PDO::FETCH_ASSOC);
$companyName = $company['company_name'] ?? 'Foody';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Now Serving & Ads - <?= htmlspecialchars($companyName) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800&display=swap" rel="stylesheet">
<style>
:root {
--primary-color: #0d6efd;
--ready-color: #198754;
--preparing-color: #0dcaf0;
--bg-dark: #121212;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-dark);
color: white;
overflow: hidden;
height: 100vh;
margin: 0;
}
.main-container {
display: flex;
height: 100vh;
width: 100vw;
}
.serving-board {
flex: 0 0 35%;
background-color: #1a1a1a;
border-right: 2px solid #333;
display: flex;
flex-direction: column;
padding: 2rem;
}
.ads-slider {
flex: 1;
position: relative;
background-color: black;
}
.board-header {
text-align: center;
margin-bottom: 2rem;
border-bottom: 1px solid #333;
padding-bottom: 1rem;
}
.board-title {
font-size: 2.5rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 2px;
margin: 0;
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
text-transform: uppercase;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 10px;
}
.order-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 15px;
margin-bottom: 3rem;
}
.order-number {
font-size: 2.5rem;
font-weight: 800;
padding: 1rem 0.5rem;
border-radius: 10px;
text-align: center;
line-height: 1;
}
.order-ready {
background-color: var(--ready-color);
color: white;
animation: pulse 2s infinite;
}
.order-preparing {
background-color: #333;
color: #aaa;
border: 1px solid #444;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.carousel-item img {
width: 100%;
height: 100vh;
object-fit: cover;
}
.carousel-caption {
background: rgba(0,0,0,0.6);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 1.5rem 2rem;
bottom: 5%;
left: 50%;
transform: translateX(-50%);
width: fit-content;
max-width: 80%;
}
.caption-title {
font-size: 2.5rem;
font-weight: 800;
margin: 0;
}
/* Toggle View Button */
#toggle-view {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
opacity: 0.3;
transition: opacity 0.3s;
}
#toggle-view:hover { opacity: 1; }
/* Fullscreen Ads Class */
.fullscreen-ads .serving-board { display: none; }
.fullscreen-ads .ads-slider { flex: 0 0 100%; }
.empty-msg {
font-size: 1.2rem;
color: #555;
font-style: italic;
}
</style>
</head>
<body class="split-view">
<div class="main-container" id="main-view">
<!-- Left: Serving Board -->
<div class="serving-board">
<div class="board-header">
<h1 class="board-title">Order Status</h1>
<p class="text-muted"><?= htmlspecialchars($companyName) ?> Outlet #<?= $outlet_id ?></p>
</div>
<div class="mb-5">
<h2 class="section-title text-success">
<i class="bi bi-bell-fill"></i> Now Serving
</h2>
<div id="ready-orders" class="order-grid">
<!-- Ready orders injected here -->
<div class="empty-msg">No orders ready</div>
</div>
</div>
<div>
<h2 class="section-title text-info">
<i class="bi bi-hourglass-split"></i> Preparing
</h2>
<div id="preparing-orders" class="order-grid">
<!-- Preparing orders injected here -->
<div class="empty-msg">No orders preparing</div>
</div>
</div>
<div class="mt-auto pt-4 border-top border-dark text-center text-muted">
<small id="clock"></small>
</div>
</div>
<!-- Right: Ads Slider -->
<div class="ads-slider">
<?php if (!empty($ads)): ?>
<div id="adCarousel" class="carousel slide carousel-fade" data-bs-ride="carousel" data-bs-interval="10000">
<div class="carousel-inner">
<?php foreach ($ads as $index => $ad): ?>
<div class="carousel-item <?= $index === 0 ? 'active' : '' ?>">
<img src="<?= htmlspecialchars($ad['image_path']) ?>" alt="<?= htmlspecialchars($ad['title'] ?? '') ?>">
<?php if (!empty($ad['title'])): ?>
<div class="carousel-caption d-none d-md-block">
<h5 class="caption-title"><?= htmlspecialchars($ad['title']) ?></h5>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<div class="h-100 d-flex flex-column align-items-center justify-content-center text-muted">
<i class="bi bi-images fs-1 mb-3"></i>
<p>No advertisements uploaded yet.</p>
<small>Go to Admin > Ads Management to upload pictures.</small>
</div>
<?php endif; ?>
</div>
</div>
<button id="toggle-view" class="btn btn-dark btn-sm rounded-circle shadow">
<i class="bi bi-fullscreen"></i>
</button>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const OUTLET_ID = <?= $outlet_id ?>;
let isFullscreen = false;
// Fetch Orders for the Board
async function fetchBoardOrders() {
try {
const response = await fetch('api/kitchen.php?outlet_id=' + OUTLET_ID);
if (!response.ok) throw new Error('Network response was not ok');
const orders = await response.json();
updateBoard(orders);
} catch (error) {
console.error('Error fetching orders:', error);
}
}
function updateBoard(orders) {
const readyContainer = document.getElementById('ready-orders');
const preparingContainer = document.getElementById('preparing-orders');
const readyOrders = orders.filter(o => o.status === 'ready');
const preparingOrders = orders.filter(o => o.status === 'preparing');
// Render Ready
if (readyOrders.length > 0) {
readyContainer.innerHTML = readyOrders.map(o => `
<div class="order-number order-ready">${o.id}</div>
`).join('');
} else {
readyContainer.innerHTML = '<div class="empty-msg">Waiting for orders...</div>';
}
// Render Preparing
if (preparingOrders.length > 0) {
preparingContainer.innerHTML = preparingOrders.map(o => `
<div class="order-number order-preparing">${o.id}</div>
`).join('');
} else {
preparingContainer.innerHTML = '<div class="empty-msg">No active prep</div>';
}
}
// Toggle View Logic (Alternate between Split and Fullscreen Ads)
const toggleBtn = document.getElementById('toggle-view');
toggleBtn.addEventListener('click', () => {
isFullscreen = !isFullscreen;
const body = document.body;
const icon = toggleBtn.querySelector('i');
if (isFullscreen) {
body.classList.add('fullscreen-ads');
icon.classList.replace('bi-fullscreen', 'bi-fullscreen-exit');
} else {
body.classList.remove('fullscreen-ads');
icon.classList.replace('bi-fullscreen-exit', 'bi-fullscreen');
}
});
// Auto-alternate every 30 seconds (Optional, but user mentioned "alternatively")
setInterval(() => {
// If you want it to auto-switch, uncomment below:
toggleBtn.click();
}, 15000);
// Clock
setInterval(() => {
const clock = document.getElementById('clock');
if (clock) {
const now = new Date();
clock.textContent = now.toLocaleDateString() + ' ' + now.toLocaleTimeString();
}
}, 1000);
// Initial Fetch & Poll
fetchBoardOrders();
setInterval(fetchBoardOrders, 5000); // Poll every 5s for the board
</script>
</body>
</html>

View File

@ -121,6 +121,8 @@ body {
color: var(--text-heading) !important; color: var(--text-heading) !important;
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer;
user-select: none;
} }
.sidebar-heading i { .sidebar-heading i {
@ -128,6 +130,22 @@ body {
font-size: 1.1em; 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 { .brand-logo {
font-weight: 700; font-weight: 700;
font-size: 1.5rem; font-size: 1.5rem;
@ -393,4 +411,8 @@ body {
} }
[data-theme="dark"] .table { [data-theme="dark"] .table {
color: var(--text-primary); color: var(--text-primary);
}
/* Ensure Dropdowns are always on top */
.dropdown-menu {
z-index: 1050 !important;
} }

View File

@ -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.

View File

@ -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
);

View File

@ -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
);

View File

@ -4,6 +4,7 @@ class WablasService {
private $domain; private $domain;
private $token; private $token;
private $secret_key; private $secret_key;
private $is_enabled;
public function __construct($pdo) { public function __construct($pdo) {
$this->pdo = $pdo; $this->pdo = $pdo;
@ -23,6 +24,7 @@ class WablasService {
$this->domain = $settings['domain'] ?? ''; $this->domain = $settings['domain'] ?? '';
$this->token = $settings['token'] ?? ''; $this->token = $settings['token'] ?? '';
$this->secret_key = $settings['secret_key'] ?? ''; $this->secret_key = $settings['secret_key'] ?? '';
$this->is_enabled = ($settings['is_enabled'] ?? '0') === '1';
} }
public function testConnection() { public function testConnection() {
@ -72,6 +74,10 @@ class WablasService {
} }
public function sendMessage($phone, $message) { 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)) { if (empty($this->domain) || empty($this->token)) {
return ['success' => false, 'message' => 'Wablas configuration missing']; return ['success' => false, 'message' => 'Wablas configuration missing'];
} }

View File

@ -83,9 +83,9 @@ function paginate_query($pdo, $query, $params = [], $default_limit = 20) {
// Add LIMIT and OFFSET // 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 // 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); $stmt->execute($params);
$data = $stmt->fetchAll(); $data = $stmt->fetchAll();
@ -115,16 +115,16 @@ function render_pagination_controls($pagination, $extra_params = []) {
// Limit Selector // Limit Selector
$limits = [20, 50, 100, -1]; $limits = [20, 50, 100, -1];
echo '<div class="d-flex justify-content-between align-items-center mb-3 bg-white p-2 rounded border">'; echo '<div class="d-flex justify-content-between align-items-center mb-0 bg-white p-2 rounded flex-wrap gap-2">';
echo '<div class="d-flex align-items-center">'; echo '<div class="d-flex align-items-center flex-wrap gap-3">';
echo '<form method="GET" class="d-flex align-items-center mb-0">'; echo '<form method="GET" class="d-flex align-items-center mb-0">';
// Preserve other GET params // Preserve other GET params
foreach ($params as $key => $val) { foreach ($params as $key => $val) {
if ($key !== 'limit') echo '<input type="hidden" name="'.htmlspecialchars($key).'" value="'.htmlspecialchars($val).'">'; if ($key !== 'limit') echo '<input type="hidden" name="'.htmlspecialchars((string)$key).'" value="'.htmlspecialchars((string)$val).'">';
} }
echo '<small class="me-2 text-muted">Show:</small>'; echo '<small class="me-2 text-muted fw-bold">SHOW:</small>';
echo '<select name="limit" class="form-select form-select-sm border-0 bg-light" style="width: auto; font-weight: 500;" onchange="this.form.submit()">'; echo '<select name="limit" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">';
foreach ($limits as $l) { foreach ($limits as $l) {
$label = $l == -1 ? 'All' : $l; $label = $l == -1 ? 'All' : $l;
$selected = $limit == $l ? 'selected' : ''; $selected = $limit == $l ? 'selected' : '';
@ -134,26 +134,35 @@ function render_pagination_controls($pagination, $extra_params = []) {
echo '</form>'; echo '</form>';
// Total Count // Total Count
echo '<span class="text-muted small ms-3 border-start ps-3">Total: <strong>' . $pagination['total_rows'] . '</strong></span>'; echo '<span class="text-muted small border-start ps-3">Total: <strong>' . $pagination['total_rows'] . '</strong> items</span>';
// Optional Total Amount (Sum)
if (isset($pagination['total_amount_sum'])) {
echo '<span class="text-success small border-start ps-3">Total Sum: <strong class="fs-5">' . format_currency($pagination['total_amount_sum']) . '</strong></span>';
}
if ($total_pages > 0) {
echo '<span class="text-muted small border-start ps-3">Page <strong>' . $page . '</strong> of <strong>' . $total_pages . '</strong></span>';
}
echo '</div>'; echo '</div>';
// Pagination Links // Pagination Links
if ($total_pages > 1) { if ($total_pages > 1) {
echo '<nav><ul class="pagination pagination-sm mb-0">'; echo '<nav aria-label="Page navigation">';
echo '<ul class="pagination pagination-sm mb-0">';
// Previous // Previous
$prev_disabled = $page <= 1 ? 'disabled' : ''; $prev_disabled = $page <= 1 ? 'disabled' : '';
$prev_page = max(1, $page - 1); $prev_page = max(1, $page - 1);
$url_params = array_merge($params, ['page' => $prev_page, 'limit' => $limit]); $url_params = array_merge($params, ['page' => $prev_page, 'limit' => $limit]);
$prev_url = '?' . http_build_query($url_params); $prev_url = '?' . http_build_query($url_params);
echo "<li class='page-item $prev_disabled'><a class='page-link' href='$prev_url'>&laquo;</a></li>"; echo "<li class='page-item $prev_disabled'><a class='page-link' href='$prev_url' aria-label='Previous'><span aria-hidden='true'>&laquo;</span></a></li>";
// Logic to show limited page numbers with ellipsis // Logic to show limited page numbers with ellipsis
// Always show first, last, current, and surrounding
$shown_pages = []; $shown_pages = [];
$shown_pages[] = 1; $shown_pages[] = 1;
$shown_pages[] = $total_pages; $shown_pages[] = $total_pages;
for ($i = $page - 2; $i <= $page + 2; $i++) { for ($i = $page - 3; $i <= $page + 3; $i++) {
if ($i > 1 && $i < $total_pages) { if ($i > 1 && $i < $total_pages) {
$shown_pages[] = $i; $shown_pages[] = $i;
} }
@ -179,9 +188,76 @@ function render_pagination_controls($pagination, $extra_params = []) {
$next_page = min($total_pages, $page + 1); $next_page = min($total_pages, $page + 1);
$url_params = array_merge($params, ['page' => $next_page, 'limit' => $limit]); $url_params = array_merge($params, ['page' => $next_page, 'limit' => $limit]);
$next_url = '?' . http_build_query($url_params); $next_url = '?' . http_build_query($url_params);
echo "<li class='page-item $next_disabled'><a class='page-link' href='$next_url'>&raquo;</a></li>"; echo "<li class='page-item $next_disabled'><a class='page-link' href='$next_url' aria-label='Next'><span aria-hidden='true'>&raquo;</span></a></li>";
echo '</ul></nav>'; echo '</ul></nav>';
} }
echo '</div>'; echo '</div>';
}
/**
* 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;
}
} }

View File

@ -1,8 +1,41 @@
<?php <?php
require_once __DIR__ . '/db/config.php'; require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/functions.php';
require_login();
$pdo = db(); $pdo = db();
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); $currentUser = get_logged_user();
$current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
// 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'];
}
}
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -12,6 +45,7 @@ $current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
<title>Kitchen Display System</title> <title>Kitchen Display System</title>
<!-- Re-using existing CSS if any, or adding Bootstrap/Custom --> <!-- Re-using existing CSS if any, or adding Bootstrap/Custom -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link href="assets/css/custom.css" rel="stylesheet"> <link href="assets/css/custom.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style> <style>
@ -57,16 +91,28 @@ $current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
<div class="container-fluid"> <div class="container-fluid">
<div class="d-flex justify-content-between align-items-center py-3 px-4 bg-white shadow-sm mb-4"> <div class="d-flex justify-content-between align-items-center py-3 px-4 bg-white shadow-sm mb-4">
<h2 class="h4 mb-0">Kitchen Display</h2> <h2 class="h4 mb-0">Kitchen Display</h2>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center gap-3">
<select id="outlet-selector" class="form-select form-select-sm me-3" style="width: auto;"> <select id="outlet-selector" class="form-select form-select-sm" style="width: auto;">
<?php foreach ($outlets as $outlet): ?> <?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= $current_outlet_id == $outlet['id'] ? 'selected' : '' ?>> <option value="<?= $outlet['id'] ?>" <?= $current_outlet_id == $outlet['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($outlet['name']) ?> <?= htmlspecialchars($outlet['name']) ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<span id="clock" class="text-muted me-3"></span> <span id="clock" class="text-muted d-none d-md-inline"></span>
<a href="index.php" class="btn btn-outline-secondary btn-sm">Back to Home</a>
<div class="dropdown">
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> <?= htmlspecialchars($currentUser['username']) ?>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
<li><a class="dropdown-item" href="admin/"><i class="bi bi-shield-lock me-2"></i> Admin Panel</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a></li>
</ul>
</div>
<a href="index.php" class="btn btn-outline-secondary btn-sm">Home</a>
</div> </div>
</div> </div>
@ -219,5 +265,6 @@ fetchOrders();
setInterval(fetchOrders, 10000); // Poll every 10s setInterval(fetchOrders, 10000); // Poll every 10s
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>

88
login.php Normal file
View File

@ -0,0 +1,88 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/functions.php';
init_session();
// Redirect if already logged in
if (get_logged_user()) {
header('Location: /admin/index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (login_user($username, $password)) {
header('Location: /admin/index.php');
exit;
} else {
$error = 'Invalid username or password.';
}
}
$settings = get_company_settings();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - <?= htmlspecialchars($settings['company_name']) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/assets/css/custom.css">
<style>
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
width: 100%;
max-width: 400px;
border-radius: 1.5rem;
box-shadow: 0 1rem 3rem rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="card login-card border-0">
<div class="card-body p-5">
<div class="text-center mb-4">
<h3 class="fw-bold"><?= htmlspecialchars($settings['company_name']) ?></h3>
<p class="text-muted">Sign in to your account</p>
</div>
<?php if ($error): ?>
<div class="alert alert-danger small py-2"><?= $error ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label small fw-medium">Username</label>
<div class="input-group">
<span class="input-group-text bg-light border-0"><i class="bi bi-person"></i></span>
<input type="text" name="username" class="form-control bg-light border-0" required autofocus>
</div>
</div>
<div class="mb-4">
<label class="form-label small fw-medium">Password</label>
<div class="input-group">
<span class="input-group-text bg-light border-0"><i class="bi bi-lock"></i></span>
<input type="password" name="password" class="form-control bg-light border-0" required>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 fw-bold">Login</button>
</form>
</div>
</div>
</body>
</html>

5
logout.php Normal file
View File

@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/includes/functions.php';
logout_user();
header('Location: /login.php');
exit;

49
pos.php
View File

@ -1,11 +1,45 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require_once __DIR__ . '/db/config.php'; require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/functions.php';
require_login();
$pdo = db(); $pdo = db();
$currentUser = get_logged_user();
// Fetch outlets based on user assignment
if (has_permission('all')) {
$outlets = $pdo->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(); $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(); $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(); $payment_types = $pdo->query("SELECT * FROM payment_types WHERE is_active = 1 ORDER BY id")->fetchAll();
// Fetch variants // Fetch variants
@ -16,7 +50,6 @@ foreach ($variants_raw as $v) {
} }
$table_id = $_GET['table'] ?? '1'; // Default table $table_id = $_GET['table'] ?? '1'; // Default table
$outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
$settings = get_company_settings(); $settings = get_company_settings();
$order_type = $_GET['order_type'] ?? 'takeaway'; $order_type = $_GET['order_type'] ?? 'takeaway';
@ -79,7 +112,17 @@ foreach ($outlets as $o) {
<div id="current-table-display" class="badge bg-light text-dark border px-3 py-2" style="display: none; font-size: 0.9rem;"> <div id="current-table-display" class="badge bg-light text-dark border px-3 py-2" style="display: none; font-size: 0.9rem;">
Table <?= htmlspecialchars($table_id) ?> Table <?= htmlspecialchars($table_id) ?>
</div> </div>
<a href="admin/" class="btn btn-sm btn-outline-dark"><i class="bi bi-shield-lock"></i> Admin</a>
<div class="dropdown">
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> <?= htmlspecialchars($currentUser['username']) ?>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="admin/"><i class="bi bi-shield-lock"></i> Admin Panel</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="logout.php"><i class="bi bi-box-arrow-right"></i> Logout</a></li>
</ul>
</div>
</div> </div>
</div> </div>
</nav> </nav>