modifying loyalty

This commit is contained in:
Flatlogic Bot 2026-02-23 12:56:09 +00:00
parent 3595e1b23e
commit 370ceb510e
34 changed files with 1595 additions and 474 deletions

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("ads_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("areas_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("categories_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("settings_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("customers_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();

View File

@ -0,0 +1,84 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("expense_categories_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if (isset($_GET['delete'])) {
$id = $_GET['delete'];
// Check if there are expenses linked to this category
$stmt = $pdo->prepare("SELECT COUNT(*) FROM expenses WHERE category_id = ?");
$stmt->execute([$id]);
if ($stmt->fetchColumn() > 0) {
$_SESSION['error'] = "Cannot delete category as it has linked expenses.";
} else {
$pdo->prepare("DELETE FROM expense_categories WHERE id = ?")->execute([$id]);
$_SESSION['success'] = "Category deleted successfully.";
}
header("Location: expense_categories.php");
exit;
}
$query = "SELECT * FROM expense_categories ORDER BY name ASC";
$categories_pagination = paginate_query($pdo, $query);
$categories = $categories_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Expense Categories</h2>
<a href="expense_category_edit.php" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Add Category
</a>
</div>
<?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-danger"><?= $_SESSION['error']; unset($_SESSION['error']); ?></div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success"><?= $_SESSION['success']; unset($_SESSION['success']); ?></div>
<?php endif; ?>
<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($categories_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Description</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $cat): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $cat['id'] ?></td>
<td><?= htmlspecialchars($cat['name']) ?></td>
<td><?= htmlspecialchars($cat['description'] ?? '') ?></td>
<td class="text-end pe-4">
<a href="expense_category_edit.php?id=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-primary me-1"><i class="bi bi-pencil"></i></a>
<a href="?delete=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')"><i class="bi bi-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($categories)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted">No categories found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($categories_pagination); ?>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -0,0 +1,88 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("expense_categories_edit");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
$category = null;
$message = '';
$isEdit = false;
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM expense_categories WHERE id = ?");
$stmt->execute([$id]);
$category = $stmt->fetch();
if ($category) {
$isEdit = true;
} else {
header("Location: expense_categories.php");
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name']);
$description = trim($_POST['description']);
if (empty($name)) {
$message = '<div class="alert alert-danger">Category name is required.</div>';
} else {
try {
if ($isEdit) {
$stmt = $pdo->prepare("UPDATE expense_categories SET name = ?, description = ? WHERE id = ?");
$stmt->execute([$name, $description, $id]);
$message = '<div class="alert alert-success">Category updated successfully!</div>';
$stmt = $pdo->prepare("SELECT * FROM expense_categories WHERE id = ?");
$stmt->execute([$id]);
$category = $stmt->fetch();
} else {
$stmt = $pdo->prepare("INSERT INTO expense_categories (name, description) VALUES (?, ?)");
$stmt->execute([$name, $description]);
header("Location: expense_categories.php?success=created");
exit;
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
if (!$isEdit) {
$category = [
'name' => $_POST['name'] ?? '',
'description' => $_POST['description'] ?? ''
];
}
include 'includes/header.php';
?>
<div class="mb-4">
<a href="expense_categories.php" class="text-decoration-none text-muted mb-2 d-inline-block"><i class="bi bi-arrow-left"></i> Back to Categories</a>
<h2 class="fw-bold mb-0"><?= $isEdit ? 'Edit Category' : 'Add New Category' ?></h2>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label class="form-label">Category Name <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($category['name']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="3"><?= htmlspecialchars($category['description'] ?? '') ?></textarea>
</div>
<hr>
<div class="d-flex justify-content-end gap-2">
<a href="expense_categories.php" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary"><?= $isEdit ? 'Save Changes' : 'Create Category' ?></button>
</div>
</form>
</div>
</div>
<?php include 'includes/footer.php'; ?>

133
admin/expense_edit.php Normal file
View File

@ -0,0 +1,133 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("expenses_edit");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
$expense = null;
$message = '';
$isEdit = false;
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM expenses WHERE id = ?");
$stmt->execute([$id]);
$expense = $stmt->fetch();
if ($expense) {
$isEdit = true;
} else {
header("Location: expenses.php");
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$category_id = $_POST['category_id'];
$outlet_id = $_POST['outlet_id'];
$amount = $_POST['amount'];
$description = trim($_POST['description']);
$expense_date = $_POST['expense_date'];
if (empty($category_id) || empty($amount) || empty($expense_date)) {
$message = '<div class="alert alert-danger">Category, amount, and date are required.</div>';
} else {
try {
if ($isEdit) {
$stmt = $pdo->prepare("UPDATE expenses SET category_id = ?, outlet_id = ?, amount = ?, description = ?, expense_date = ? WHERE id = ?");
$stmt->execute([$category_id, $outlet_id, $amount, $description, $expense_date, $id]);
$message = '<div class="alert alert-success">Expense updated successfully!</div>';
$stmt = $pdo->prepare("SELECT * FROM expenses WHERE id = ?");
$stmt->execute([$id]);
$expense = $stmt->fetch();
} else {
$stmt = $pdo->prepare("INSERT INTO expenses (category_id, outlet_id, amount, description, expense_date) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$category_id, $outlet_id, $amount, $description, $expense_date]);
header("Location: expenses.php?success=created");
exit;
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
if (!$isEdit) {
$expense = [
'category_id' => $_POST['category_id'] ?? '',
'outlet_id' => $_POST['outlet_id'] ?? '',
'amount' => $_POST['amount'] ?? '',
'description' => $_POST['description'] ?? '',
'expense_date' => $_POST['expense_date'] ?? date('Y-m-d')
];
}
$expense_categories = $pdo->query("SELECT * FROM expense_categories ORDER BY name ASC")->fetchAll();
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name ASC")->fetchAll();
include 'includes/header.php';
?>
<div class="mb-4">
<a href="expenses.php" class="text-decoration-none text-muted mb-2 d-inline-block"><i class="bi bi-arrow-left"></i> Back to Expenses</a>
<h2 class="fw-bold mb-0"><?= $isEdit ? 'Edit Expense' : 'Add New Expense' ?></h2>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="POST">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Category <span class="text-danger">*</span></label>
<select name="category_id" class="form-select" required>
<option value="">Select Category</option>
<?php foreach ($expense_categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $expense['category_id'] == $cat['id'] ? 'selected' : '' ?>><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Outlet <span class="text-danger">*</span></label>
<select name="outlet_id" class="form-select" required>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= $expense['outlet_id'] == $outlet['id'] ? 'selected' : '' ?>><?= htmlspecialchars($outlet['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Amount <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" step="0.01" name="amount" class="form-control" value="<?= htmlspecialchars($expense['amount']) ?>" required>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Date <span class="text-danger">*</span></label>
<input type="date" name="expense_date" class="form-control" value="<?= htmlspecialchars($expense['expense_date']) ?>" required>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="3" placeholder="What was this expense for?"><?= htmlspecialchars($expense['description']) ?></textarea>
</div>
<hr>
<div class="d-flex justify-content-end gap-2">
<a href="expenses.php" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary"><?= $isEdit ? 'Save Changes' : 'Record Expense' ?></button>
</div>
</form>
</div>
</div>
<?php include 'includes/footer.php'; ?>

157
admin/expenses.php Normal file
View File

@ -0,0 +1,157 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("expenses_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
if (isset($_GET['delete'])) {
if (!has_permission('expenses_delete')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete expenses.</div>';
} else {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM expenses WHERE id = ?")->execute([$id]);
header("Location: expenses.php");
exit;
}
}
$expense_categories = $pdo->query("SELECT * FROM expense_categories ORDER BY name")->fetchAll();
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll();
$search = $_GET['search'] ?? '';
$category_filter = $_GET['category_filter'] ?? '';
$outlet_filter = $_GET['outlet_filter'] ?? '';
$params = [];
$where = [];
$query = "SELECT e.*, ec.name as category_name, o.name as outlet_name
FROM expenses e
LEFT JOIN expense_categories ec ON e.category_id = ec.id
LEFT JOIN outlets o ON e.outlet_id = o.id";
if ($search) {
$where[] = "e.description LIKE ?";
$params[] = "%$search%";
}
if ($category_filter) {
$where[] = "e.category_id = ?";
$params[] = $category_filter;
}
if ($outlet_filter) {
$where[] = "e.outlet_id = ?";
$params[] = $outlet_filter;
}
if (!empty($where)) {
$query .= " WHERE " . implode(" AND ", $where);
}
$query .= " ORDER BY e.expense_date DESC, e.id DESC";
$expenses_pagination = paginate_query($pdo, $query, $params);
$expenses = $expenses_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">Expenses</h2>
<p class="text-muted mb-0">Track and manage business expenditures</p>
</div>
<?php if (has_permission('expenses_add')): ?>
<a href="expense_edit.php" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Add Expense
</a>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<input type="text" name="search" class="form-control" placeholder="Search description..." value="<?= htmlspecialchars($search) ?>">
</div>
<div class="col-md-3">
<select name="category_filter" class="form-select">
<option value="">All Categories</option>
<?php foreach ($expense_categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $category_filter == $cat['id'] ? 'selected' : '' ?>><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<select name="outlet_filter" class="form-select">
<option value="">All Outlets</option>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= $outlet_filter == $outlet['id'] ? 'selected' : '' ?>><?= htmlspecialchars($outlet['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 d-flex gap-2">
<button type="submit" class="btn btn-outline-primary w-100">Filter</button>
<?php if ($search || $category_filter || $outlet_filter): ?>
<a href="expenses.php" class="btn btn-outline-secondary"><i class="bi bi-x"></i></a>
<?php endif; ?>
</div>
</form>
</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($expenses_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Date</th>
<th>Category</th>
<th>Outlet</th>
<th>Description</th>
<th>Amount</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($expenses as $exp): ?>
<tr>
<td class="ps-4 fw-medium"><?= date('M d, Y', strtotime($exp['expense_date'])) ?></td>
<td><span class="badge bg-info bg-opacity-10 text-info"><?= htmlspecialchars($exp['category_name']) ?></span></td>
<td><?= htmlspecialchars($exp['outlet_name']) ?></td>
<td><?= htmlspecialchars($exp['description']) ?></td>
<td class="fw-bold"><?= format_currency($exp['amount']) ?></td>
<td class="text-end pe-4">
<?php if (has_permission('expenses_edit')): ?>
<a href="expense_edit.php?id=<?= $exp['id'] ?>" class="btn btn-sm btn-outline-primary me-1"><i class="bi bi-pencil"></i></a>
<?php endif; ?>
<?php if (has_permission('expenses_delete')): ?>
<a href="?delete=<?= $exp['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($expenses)): ?>
<tr>
<td colspan="6" class="text-center py-4 text-muted">No expenses found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($expenses_pagination); ?>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -43,6 +43,12 @@ function isGroupExpanded($pages) {
function getGroupToggleClass($pages) {
return in_array(basename($_SERVER['PHP_SELF']), $pages) ? '' : 'collapsed';
}
// Permission helper for sidebar
function can_view($module) {
if (!function_exists('has_permission')) return true;
return has_permission($module . '_view');
}
?>
<!doctype html>
<html lang="en">
@ -223,14 +229,20 @@ function getGroupToggleClass($pages) {
<div class="sidebar-content accordion" id="sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('dashboard')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('index.php') ?>" href="index.php">
<i class="bi bi-grid me-2"></i> Dashboard
</a>
</li>
<?php endif; ?>
</ul>
<?php $posGroup = ['orders.php', 'ads.php', 'ad_edit.php']; ?>
<?php
$posGroup = ['orders.php', 'ads.php', 'ad_edit.php'];
$canViewPosGroup = can_view('pos') || can_view('orders') || can_view('kitchen') || can_view('ads');
if ($canViewPosGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($posGroup) ?>"
data-bs-toggle="collapse" href="#collapsePos" role="button" aria-expanded="<?= isGroupExpanded($posGroup) ?>" aria-controls="collapsePos">
@ -239,31 +251,44 @@ function getGroupToggleClass($pages) {
</a>
<div class="collapse <?= isGroupActive($posGroup) ?>" id="collapsePos" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('pos')): ?>
<li class="nav-item">
<a class="nav-link" href="../pos.php" target="_blank">
<i class="bi bi-display me-2"></i> POS Terminal
</a>
</li>
<?php endif; ?>
<?php if (can_view('orders')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('orders.php') ?>" href="orders.php">
<i class="bi bi-receipt me-2"></i> Orders (POS)
</a>
</li>
<?php endif; ?>
<?php if (can_view('kitchen')): ?>
<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>
<?php endif; ?>
<?php if (can_view('ads')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('ads.php') || isActive('ad_edit.php') ? 'active' : '' ?>" href="ads.php">
<i class="bi bi-megaphone me-2"></i> Ads Management
</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php $menuGroup = ['products.php', 'product_edit.php', 'categories.php', 'category_edit.php', 'product_variants.php']; ?>
<?php
$menuGroup = ['products.php', 'product_edit.php', 'categories.php', 'category_edit.php', 'product_variants.php'];
$canViewMenuGroup = can_view('products') || can_view('categories');
if ($canViewMenuGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($menuGroup) ?>"
data-bs-toggle="collapse" href="#collapseMenu" role="button" aria-expanded="<?= isGroupExpanded($menuGroup) ?>" aria-controls="collapseMenu">
@ -272,21 +297,30 @@ function getGroupToggleClass($pages) {
</a>
<div class="collapse <?= isGroupActive($menuGroup) ?>" id="collapseMenu" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('products')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('products.php') ?>" href="products.php">
<i class="bi bi-box-seam me-2"></i> Products
</a>
</li>
<?php endif; ?>
<?php if (can_view('categories')): ?>
<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>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php $setupGroup = ['outlets.php', 'outlet_edit.php', 'areas.php', 'area_edit.php', 'tables.php', 'table_edit.php']; ?>
<?php
$setupGroup = ['outlets.php', 'outlet_edit.php', 'areas.php', 'area_edit.php', 'tables.php', 'table_edit.php'];
$canViewSetupGroup = can_view('outlets') || can_view('areas') || can_view('tables');
if ($canViewSetupGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($setupGroup) ?>"
data-bs-toggle="collapse" href="#collapseSetup" role="button" aria-expanded="<?= isGroupExpanded($setupGroup) ?>" aria-controls="collapseSetup">
@ -295,26 +329,37 @@ function getGroupToggleClass($pages) {
</a>
<div class="collapse <?= isGroupActive($setupGroup) ?>" id="collapseSetup" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('outlets')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php">
<i class="bi bi-shop me-2"></i> Outlets
</a>
</li>
<?php endif; ?>
<?php if (can_view('areas')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('areas.php') ?>" href="areas.php">
<i class="bi bi-geo-alt me-2"></i> Areas
</a>
</li>
<?php endif; ?>
<?php if (can_view('tables')): ?>
<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>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php $peopleGroup = ['customers.php', 'customer_edit.php', 'suppliers.php', 'supplier_edit.php', 'loyalty.php']; ?>
<?php
$peopleGroup = ['customers.php', 'customer_edit.php', 'suppliers.php', 'supplier_edit.php', 'loyalty.php'];
$canViewPeopleGroup = can_view('customers') || can_view('suppliers') || can_view('loyalty');
if ($canViewPeopleGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($peopleGroup) ?>"
data-bs-toggle="collapse" href="#collapsePeople" role="button" aria-expanded="<?= isGroupExpanded($peopleGroup) ?>" aria-controls="collapsePeople">
@ -323,27 +368,91 @@ function getGroupToggleClass($pages) {
</a>
<div class="collapse <?= isGroupActive($peopleGroup) ?>" id="collapsePeople" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('customers')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('customers.php') ?>" href="customers.php">
<i class="bi bi-people-fill me-2"></i> Customers
</a>
</li>
<?php endif; ?>
<?php if (can_view('suppliers')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('suppliers.php') ?>" href="suppliers.php">
<i class="bi bi-truck me-2"></i> Suppliers
</a>
</li>
<?php endif; ?>
<?php if (can_view('loyalty')): ?>
<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>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php $reportsGroup = ['reports.php']; ?> <div class="nav-group"> <a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($reportsGroup) ?>" data-bs-toggle="collapse" href="#collapseReports" role="button" aria-expanded="<?= isGroupExpanded($reportsGroup) ?>" aria-controls="collapseReports"> <span><i class="bi bi-bar-chart-line"></i> Reports & Analytics</span> <i class="bi bi-chevron-down chevron-icon"></i> </a> <div class="collapse <?= isGroupActive($reportsGroup) ?>" id="collapseReports" data-bs-parent="#sidebarAccordion"> <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link <?= isActive('reports.php') ?>" href="reports.php"> <i class="bi bi-graph-up me-2"></i> Daily Reports </a> </li> </ul> </div> </div>
<?php $userGroupPages = ['users.php', 'user_edit.php', 'user_groups.php', 'user_group_edit.php']; ?>
<?php
$financialsGroup = ['expenses.php', 'expense_edit.php', 'expense_categories.php', 'expense_category_edit.php'];
$canViewFinancialsGroup = can_view('expenses') || can_view('expense_categories');
if ($canViewFinancialsGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($financialsGroup) ?>"
data-bs-toggle="collapse" href="#collapseFinancials" role="button" aria-expanded="<?= isGroupExpanded($financialsGroup) ?>" aria-controls="collapseFinancials">
<span><i class="bi bi-wallet2"></i> Financials</span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($financialsGroup) ?>" id="collapseFinancials" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('expenses')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('expenses.php') || isActive('expense_edit.php') ? 'active' : '' ?>" href="expenses.php">
<i class="bi bi-cash-stack me-2"></i> Expenses
</a>
</li>
<?php endif; ?>
<?php if (can_view('expense_categories')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('expense_categories.php') || isActive('expense_category_edit.php') ? 'active' : '' ?>" href="expense_categories.php">
<i class="bi bi-tags me-2"></i> Expense Categories
</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$reportsGroup = ['reports.php'];
if (can_view('reports')):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($reportsGroup) ?>"
data-bs-toggle="collapse" href="#collapseReports" role="button" aria-expanded="<?= isGroupExpanded($reportsGroup) ?>" aria-controls="collapseReports">
<span><i class="bi bi-bar-chart-line"></i> Reports & Analytics</span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($reportsGroup) ?>" id="collapseReports" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link <?= isActive('reports.php') ?>" href="reports.php">
<i class="bi bi-graph-up me-2"></i> Daily Reports
</a>
</li>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$userGroupPages = ['users.php', 'user_edit.php', 'user_groups.php', 'user_group_edit.php'];
$canViewUserGroup = can_view('users') || can_view('user_groups');
if ($canViewUserGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($userGroupPages) ?>"
data-bs-toggle="collapse" href="#collapseUsers" role="button" aria-expanded="<?= isGroupExpanded($userGroupPages) ?>" aria-controls="collapseUsers">
@ -352,21 +461,30 @@ function getGroupToggleClass($pages) {
</a>
<div class="collapse <?= isGroupActive($userGroupPages) ?>" id="collapseUsers" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('users')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('users.php') ?>" href="users.php">
<i class="bi bi-people me-2"></i> Users
</a>
</li>
<?php endif; ?>
<?php if (can_view('user_groups')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('user_groups.php') ?>" href="user_groups.php">
<i class="bi bi-shield-lock me-2"></i> Roles / Groups
</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php $settingsGroup = ['payment_types.php', 'payment_type_edit.php', 'integrations.php', 'company.php']; ?>
<?php
$settingsGroup = ['payment_types.php', 'payment_type_edit.php', 'integrations.php', 'company.php'];
$canViewSettingsGroup = can_view('payment_types') || can_view('settings');
if ($canViewSettingsGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($settingsGroup) ?>"
data-bs-toggle="collapse" href="#collapseSettings" role="button" aria-expanded="<?= isGroupExpanded($settingsGroup) ?>" aria-controls="collapseSettings">
@ -375,11 +493,14 @@ function getGroupToggleClass($pages) {
</a>
<div class="collapse <?= isGroupActive($settingsGroup) ?>" id="collapseSettings" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column mb-5">
<?php if (can_view('payment_types')): ?>
<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>
<?php endif; ?>
<?php if (can_view('settings')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('integrations.php') ?>" href="integrations.php">
<i class="bi bi-plugin me-2"></i> Integrations
@ -390,6 +511,7 @@ function getGroupToggleClass($pages) {
<i class="bi bi-building me-2"></i> Company
</a>
</li>
<?php endif; ?>
<li class="nav-item border-top mt-2 pt-2">
<a class="nav-link text-muted" href="../index.php" target="_blank">
@ -399,6 +521,7 @@ function getGroupToggleClass($pages) {
</ul>
</div>
</div>
<?php endif; ?>
</div>
</div>

View File

@ -3,6 +3,7 @@ require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('dashboard_view');
// Fetch Dashboard Stats
$today = date('Y-m-d');
@ -36,9 +37,11 @@ include 'includes/header.php';
<h2 class="fw-bold mb-1">Dashboard</h2>
<p class="text-muted">Welcome back, <?= htmlspecialchars($userName) ?>!</p>
</div>
<?php if (has_permission('orders_add')): ?>
<div>
<a href="orders.php" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i> New Order</a>
<a href="../pos.php" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i> New Order</a>
</div>
<?php endif; ?>
</div>
<div class="row g-4 mb-4">
@ -51,7 +54,7 @@ include 'includes/header.php';
</div>
<div>
<h6 class="text-muted mb-0">Today's Revenue</h6>
<h3 class="fw-bold mb-0">$<?= number_format($revenueToday, 2) ?></h3>
<h3 class="fw-bold mb-0"><?= format_currency($revenueToday) ?></h3>
</div>
</div>
</div>
@ -144,7 +147,7 @@ include 'includes/header.php';
<?= htmlspecialchars($order['customer_name'] ?? 'Guest') ?>
<?php endif; ?>
</td>
<td class="fw-bold">$<?= number_format((float)$order['total_amount'], 2) ?></td>
<td class="fw-bold"><?= format_currency($order['total_amount']) ?></td>
<td>
<span class="status-badge status-<?= $order['status'] ?> badge rounded-pill">
<?= ucfirst($order['status']) ?>
@ -160,9 +163,11 @@ include 'includes/header.php';
</table>
</div>
</div>
<?php if (has_permission('orders_view')): ?>
<div class="card-footer bg-white text-center py-3">
<a href="orders.php" class="text-decoration-none fw-medium">View All Orders</a>
</div>
<?php endif; ?>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("settings_view");
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/WablasService.php';

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("loyalty_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
@ -6,9 +8,10 @@ $pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_settings'])) {
$points_per_order = intval($_POST['points_per_order']);
$points_for_free_meal = intval($_POST['points_for_free_meal']);
$is_enabled = isset($_POST['is_enabled']) ? 1 : 0;
$stmt = $pdo->prepare("UPDATE loyalty_settings SET points_per_order = ?, points_for_free_meal = ? WHERE id = 1");
$stmt->execute([$points_per_order, $points_for_free_meal]);
$stmt = $pdo->prepare("UPDATE loyalty_settings SET points_per_order = ?, points_for_free_meal = ?, is_enabled = ? WHERE id = 1");
$stmt->execute([$points_per_order, $points_for_free_meal, $is_enabled]);
$success_msg = "Loyalty settings updated successfully!";
}
@ -19,7 +22,7 @@ $settings = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$settings) {
// Default fallback if migration failed or empty
$settings = ['points_per_order' => 10, 'points_for_free_meal' => 70];
$settings = ['points_per_order' => 10, 'points_for_free_meal' => 70, 'is_enabled' => 1];
}
// Fetch Customers with Points
@ -39,7 +42,14 @@ include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Loyalty Program</h2>
<div class="d-flex align-items-center gap-3">
<h2 class="fw-bold mb-0">Loyalty Program</h2>
<?php if ($settings['is_enabled']): ?>
<span class="badge bg-success-subtle text-success border border-success-subtle px-3">Active</span>
<?php else: ?>
<span class="badge bg-danger-subtle text-danger border border-danger-subtle px-3">Disabled</span>
<?php endif; ?>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#settingsModal">
<i class="bi bi-gear-fill me-2"></i> Configure Settings
</button>
@ -166,6 +176,15 @@ include 'includes/header.php';
</div>
<div class="modal-body">
<input type="hidden" name="update_settings" value="1">
<div class="mb-4">
<div class="form-check form-switch p-0 d-flex justify-content-between align-items-center">
<label class="form-check-label fw-bold" for="is_enabled">Enable Loyalty Program</label>
<input class="form-check-input ms-0" type="checkbox" id="is_enabled" name="is_enabled" <?= $settings['is_enabled'] ? 'checked' : '' ?> style="width: 3em; height: 1.5em; cursor: pointer;">
</div>
<div class="form-text mt-1">When disabled, loyalty features will be hidden from the POS.</div>
</div>
<div class="mb-3">
<label class="form-label">Points per Order</label>
<input type="number" name="points_per_order" class="form-control" value="<?= $settings['points_per_order'] ?>" min="0" required>

View File

@ -1,10 +1,16 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('orders_view');
if (isset($_POST['action']) && $_POST['action'] === 'update_status') {
if (!has_permission('orders_add')) {
header("Location: orders.php?error=permission_denied");
exit;
}
$order_id = $_POST['order_id'];
$new_status = $_POST['status'];
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
@ -77,6 +83,10 @@ include 'includes/header.php';
</span>
</div>
<?php if (isset($_GET['error']) && $_GET['error'] === 'permission_denied'): ?>
<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to perform this action.</div>
<?php endif; ?>
<!-- Summary Stats -->
<div class="row mb-4">
<div class="col-md-4">
@ -260,6 +270,7 @@ include 'includes/header.php';
<div><?= date('H:i', strtotime($order['created_at'])) ?></div>
</td>
<td>
<?php if (has_permission('orders_add')): ?>
<form method="POST" class="d-flex gap-2">
<input type="hidden" name="order_id" value="<?= $order['id'] ?>">
<input type="hidden" name="action" value="update_status">
@ -283,6 +294,9 @@ include 'includes/header.php';
<span class="text-muted small">-</span>
<?php endif; ?>
</form>
<?php else: ?>
<span class="text-muted small">View Only</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("outlets_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("payment_types_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();

View File

@ -28,6 +28,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$description = $_POST['description'];
$image_url = $product['image_url']; // Default to existing
// Promo fields
$promo_discount_percent = $_POST['promo_discount_percent'] !== '' ? $_POST['promo_discount_percent'] : null;
$promo_date_from = $_POST['promo_date_from'] !== '' ? $_POST['promo_date_from'] : null;
$promo_date_to = $_POST['promo_date_to'] !== '' ? $_POST['promo_date_to'] : null;
// Image handling
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/products/';
@ -54,8 +59,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
if (empty($message)) {
$stmt = $pdo->prepare("UPDATE products SET name = ?, category_id = ?, price = ?, description = ?, image_url = ? WHERE id = ?");
if ($stmt->execute([$name, $category_id, $price, $description, $image_url, $id])) {
$stmt = $pdo->prepare("UPDATE products SET name = ?, category_id = ?, price = ?, description = ?, image_url = ?, promo_discount_percent = ?, promo_date_from = ?, promo_date_to = ? WHERE id = ?");
if ($stmt->execute([$name, $category_id, $price, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to, $id])) {
$message = '<div class="alert alert-success">Product updated successfully!</div>';
// Refresh product data
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
@ -105,6 +110,28 @@ include 'includes/header.php';
<input type="number" step="0.01" name="price" class="form-control" value="<?= htmlspecialchars($product['price']) ?>" required>
</div>
</div>
<div class="card bg-light border-0 mb-3">
<div class="card-body">
<h5 class="fw-bold mb-3"><i class="bi bi-tag-fill me-2 text-primary"></i>Promotion Settings</h5>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold text-muted">DISCOUNT PERCENT (%)</label>
<input type="number" step="0.01" name="promo_discount_percent" class="form-control" value="<?= htmlspecialchars($product['promo_discount_percent'] ?? '') ?>" placeholder="e.g. 10.00">
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold text-muted">START DATE</label>
<input type="date" name="promo_date_from" class="form-control" value="<?= htmlspecialchars($product['promo_date_from'] ?? '') ?>">
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold text-muted">END DATE</label>
<input type="date" name="promo_date_to" class="form-control" value="<?= htmlspecialchars($product['promo_date_to'] ?? '') ?>">
</div>
</div>
<div class="form-text mt-0">If active, this discount will be automatically applied to the regular price.</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="5"><?= htmlspecialchars($product['description']) ?></textarea>

View File

@ -1,5 +1,6 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
if (!isset($_GET['product_id'])) {
@ -54,6 +55,8 @@ $variants_pagination = paginate_query($pdo, $query, [$product_id]);
$variants = $variants_pagination['data'];
include 'includes/header.php';
$effective_base_price = get_product_price($product);
?>
<div class="mb-4">
@ -98,8 +101,11 @@ include 'includes/header.php';
<span class="text-muted">No change</span>
<?php endif; ?>
</td>
<td class="text-muted">
<?= format_currency($product['price'] + $variant['price_adjustment']) ?>
<td>
<div class="fw-bold"><?= format_currency($effective_base_price + $variant['price_adjustment']) ?></div>
<?php if ($effective_base_price < $product['price']): ?>
<small class="text-muted text-decoration-line-through"><?= format_currency($product['price'] + $variant['price_adjustment']) ?></small>
<?php endif; ?>
</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1"

View File

@ -1,53 +1,68 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('products_view');
$message = '';
// Handle Add Product
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_product') {
$name = $_POST['name'];
$category_id = $_POST['category_id'];
$price = $_POST['price'];
$description = $_POST['description'];
if (!has_permission('products_add')) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to add products.</div>';
} else {
$name = $_POST['name'];
$category_id = $_POST['category_id'];
$price = $_POST['price'];
$description = $_POST['description'];
// Image handling
$image_url = 'https://placehold.co/400x300?text=' . urlencode($name);
$promo_discount_percent = $_POST['promo_discount_percent'] !== '' ? $_POST['promo_discount_percent'] : null;
$promo_date_from = $_POST['promo_date_from'] !== '' ? $_POST['promo_date_from'] : null;
$promo_date_to = $_POST['promo_date_to'] !== '' ? $_POST['promo_date_to'] : null;
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/products/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Image handling
$image_url = 'https://placehold.co/400x300?text=' . urlencode($name);
$fileInfo = pathinfo($_FILES['image']['name']);
$fileExt = strtolower($fileInfo['extension']);
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/products/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (in_array($fileExt, $allowedExts)) {
$fileName = uniqid('prod_') . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
$fileInfo = pathinfo($_FILES['image']['name']);
$fileExt = strtolower($fileInfo['extension']);
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (move_uploaded_file($_FILES['image']['tmp_name'], $targetFile)) {
$image_url = 'assets/images/products/' . $fileName;
if (in_array($fileExt, $allowedExts)) {
$fileName = uniqid('prod_') . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
if (move_uploaded_file($_FILES['image']['tmp_name'], $targetFile)) {
$image_url = 'assets/images/products/' . $fileName;
}
}
}
}
$stmt = $pdo->prepare("INSERT INTO products (name, category_id, price, description, image_url) VALUES (?, ?, ?, ?, ?)");
if ($stmt->execute([$name, $category_id, $price, $description, $image_url])) {
$message = '<div class="alert alert-success border-0 shadow-sm rounded-3"><i class="bi bi-check-circle-fill me-2"></i>Product 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 product.</div>';
$stmt = $pdo->prepare("INSERT INTO products (name, category_id, price, description, image_url, promo_discount_percent, promo_date_from, promo_date_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
if ($stmt->execute([$name, $category_id, $price, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to])) {
$message = '<div class="alert alert-success border-0 shadow-sm rounded-3"><i class="bi bi-check-circle-fill me-2"></i>Product 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 product.</div>';
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM products WHERE id = ?")->execute([$id]);
header("Location: products.php");
exit;
if (!has_permission('products_del')) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to delete products.</div>';
} else {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM products WHERE id = ?")->execute([$id]);
header("Location: products.php");
exit;
}
}
// Fetch Categories for Dropdown (moved up for filter usage)
@ -92,9 +107,11 @@ include 'includes/header.php';
<h2 class="fw-bold mb-1 text-dark">Products</h2>
<p class="text-muted mb-0">Manage your catalog</p>
</div>
<?php if (has_permission('products_add')): ?>
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#addProductModal" style="border-radius: 10px; padding: 0.6rem 1.2rem;">
<i class="bi bi-plus-lg me-1"></i> Add Product
</button>
<?php endif; ?>
</div>
<?= $message ?>
@ -142,11 +159,20 @@ include 'includes/header.php';
<th>Product</th>
<th>Category</th>
<th>Price</th>
<th>Promotion</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $product): ?>
<?php
$today = date('Y-m-d');
foreach ($products as $product):
$is_promo_active = !empty($product['promo_discount_percent']) &&
!empty($product['promo_date_from']) &&
!empty($product['promo_date_to']) &&
$today >= $product['promo_date_from'] &&
$today <= $product['promo_date_to'];
?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
@ -161,19 +187,47 @@ include 'includes/header.php';
<span class="badge-soft"><?= htmlspecialchars($product['category_name'] ?? 'Uncategorized') ?></span>
</td>
<td>
<span class="text-price fs-5"><?= format_currency($product['price']) ?></span>
<?php if ($is_promo_active): ?>
<?php
$discounted_price = $product['price'] * (1 - ($product['promo_discount_percent'] / 100));
?>
<div class="text-price fs-5"><?= format_currency($discounted_price) ?></div>
<div class="text-muted small text-decoration-line-through"><?= format_currency($product['price']) ?></div>
<?php else: ?>
<span class="text-price fs-5"><?= format_currency($product['price']) ?></span>
<?php endif; ?>
</td>
<td>
<?php if ($is_promo_active): ?>
<span class="badge bg-success rounded-pill">Active (-<?= floatval($product['promo_discount_percent']) ?>%)</span>
<div class="small text-muted mt-1">Ends <?= $product['promo_date_to'] ?></div>
<?php elseif (!empty($product['promo_discount_percent'])): ?>
<?php if ($today < $product['promo_date_from']): ?>
<span class="badge bg-info rounded-pill">Upcoming</span>
<div class="small text-muted mt-1">Starts <?= $product['promo_date_from'] ?></div>
<?php else: ?>
<span class="badge bg-secondary rounded-pill">Expired</span>
<?php endif; ?>
<?php else: ?>
<span class="text-muted small">-</span>
<?php endif; ?>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<?php if (has_permission('products_add')): ?>
<a href="product_edit.php?id=<?= $product['id'] ?>" class="btn-icon-soft edit" title="Edit Product">
<i class="bi bi-pencil-fill" style="font-size: 0.9rem;"></i>
</a>
<a href="product_variants.php?product_id=<?= $product['id'] ?>" class="btn-icon-soft" title="Manage Variants">
<i class="bi bi-sliders" style="font-size: 0.9rem;"></i>
</a>
<?php endif; ?>
<?php if (has_permission('products_del')): ?>
<a href="?delete=<?= $product['id'] ?>" class="btn-icon-soft delete" onclick="return confirm('Are you sure you want to delete this product?')" title="Delete">
<i class="bi bi-trash-fill" style="font-size: 0.9rem;"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
@ -189,8 +243,9 @@ include 'includes/header.php';
<?php endif; ?>
<!-- Add Product Modal -->
<?php if (has_permission('products_add')): ?>
<div class="modal fade" id="addProductModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="border-radius: 16px;">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title fw-bold">Add New Product</h5>
@ -199,32 +254,55 @@ include 'includes/header.php';
<form method="POST" enctype="multipart/form-data">
<div class="modal-body pt-4">
<input type="hidden" name="action" value="add_product">
<div class="mb-3">
<label class="form-label text-muted small fw-bold">PRODUCT NAME</label>
<input type="text" name="name" class="form-control form-control-lg" placeholder="e.g. Cheese Burger" required style="border-radius: 10px;">
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">CATEGORY</label>
<select name="category_id" class="form-select" required style="border-radius: 10px;">
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
<div class="row">
<div class="col-md-7">
<div class="mb-3">
<label class="form-label text-muted small fw-bold">PRODUCT NAME</label>
<input type="text" name="name" class="form-control form-control-lg" placeholder="e.g. Cheese Burger" required style="border-radius: 10px;">
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">CATEGORY</label>
<select name="category_id" class="form-select" required style="border-radius: 10px;">
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">PRICE ($)</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0" style="border-radius: 10px 0 0 10px;">$</span>
<input type="number" step="0.01" name="price" class="form-control border-start-0" placeholder="0.00" required style="border-radius: 0 10px 10px 0;">
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">DESCRIPTION</label>
<textarea name="description" class="form-control" rows="3" placeholder="Brief description..." style="border-radius: 10px;"></textarea>
</div>
</div>
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">PRICE ($)</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0" style="border-radius: 10px 0 0 10px;">$</span>
<input type="number" step="0.01" name="price" class="form-control border-start-0" placeholder="0.00" required style="border-radius: 0 10px 10px 0;">
<div class="col-md-5">
<div class="card bg-light border-0 h-100">
<div class="card-body">
<h6 class="fw-bold mb-3"><i class="bi bi-tag-fill me-2 text-primary"></i>Promotion</h6>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">DISCOUNT (%)</label>
<input type="number" step="0.01" name="promo_discount_percent" class="form-control" placeholder="0.00" style="border-radius: 10px;">
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">START DATE</label>
<input type="date" name="promo_date_from" class="form-control" style="border-radius: 10px;">
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">END DATE</label>
<input type="date" name="promo_date_to" class="form-control" style="border-radius: 10px;">
</div>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">DESCRIPTION</label>
<textarea name="description" class="form-control" rows="3" placeholder="Brief description..." style="border-radius: 10px;"></textarea>
</div>
<div class="mb-3">
<div class="mb-3 mt-3">
<label class="form-label text-muted small fw-bold">IMAGE</label>
<input type="file" name="image" class="form-control" accept="image/*" style="border-radius: 10px;">
<div class="form-text">Optional. Placeholder used if empty.</div>
@ -238,5 +316,6 @@ include 'includes/header.php';
</div>
</div>
</div>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -3,14 +3,29 @@ require_once __DIR__ . '/includes/header.php';
// Check permission
if (function_exists('require_permission')) {
require_permission('manage_reports');
require_permission('reports_view');
}
$pdo = db();
// Date Filter
// Date and Outlet Filter
$startDate = $_GET['start_date'] ?? date('Y-m-d');
$endDate = $_GET['end_date'] ?? date('Y-m-d');
$outletId = $_GET['outlet_id'] ?? '';
// Fetch Outlets for filter
$outletsStmt = $pdo->query("SELECT id, name FROM outlets ORDER BY name ASC");
$allOutlets = $outletsStmt->fetchAll();
// Base query additions
$outletCondition = "";
$expenseOutletCondition = "";
$queryParams = [$startDate, $endDate];
if (!empty($outletId)) {
$outletCondition = " AND o.outlet_id = ? ";
$expenseOutletCondition = " AND e.outlet_id = ? ";
$queryParams[] = $outletId;
}
// 1. Sales by Staff
$staffStmt = $pdo->prepare("
@ -22,10 +37,11 @@ $staffStmt = $pdo->prepare("
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE DATE(o.created_at) BETWEEN ? AND ?
$outletCondition
GROUP BY o.user_id
ORDER BY total_sales DESC
");
$staffStmt->execute([$startDate, $endDate]);
$staffStmt->execute($queryParams);
$staffSales = $staffStmt->fetchAll();
// 2. Sales by Outlet
@ -37,10 +53,11 @@ $outletStmt = $pdo->prepare("
FROM orders o
JOIN outlets ot ON o.outlet_id = ot.id
WHERE DATE(o.created_at) BETWEEN ? AND ?
$outletCondition
GROUP BY o.outlet_id
ORDER BY total_sales DESC
");
$outletStmt->execute([$startDate, $endDate]);
$outletStmt->execute($queryParams);
$outletSales = $outletStmt->fetchAll();
// 3. Sales by Category
@ -54,35 +71,87 @@ $categoryStmt = $pdo->prepare("
JOIN products p ON oi.product_id = p.id
JOIN categories c ON p.category_id = c.id
WHERE DATE(o.created_at) BETWEEN ? AND ?
$outletCondition
GROUP BY c.id
ORDER BY total_sales DESC
");
$categoryStmt->execute([$startDate, $endDate]);
$categoryStmt->execute($queryParams);
$categorySales = $categoryStmt->fetchAll();
// 4. Expenses by Category
$expenseCatStmt = $pdo->prepare("
SELECT
ec.name as category_name,
SUM(e.amount) as total_amount
FROM expenses e
JOIN expense_categories ec ON e.category_id = ec.id
WHERE e.expense_date BETWEEN ? AND ?
$expenseOutletCondition
GROUP BY ec.id
ORDER BY total_amount DESC
");
$expenseCatStmt->execute($queryParams);
$expenseSales = $expenseCatStmt->fetchAll();
// Total Summary
$totalStmt = $pdo->prepare("
SELECT
COUNT(id) as total_orders,
SUM(total_amount) as total_revenue
FROM orders
WHERE DATE(created_at) BETWEEN ? AND ?
FROM orders o
WHERE DATE(o.created_at) BETWEEN ? AND ?
$outletCondition
");
$totalStmt->execute([$startDate, $endDate]);
$totalStmt->execute($queryParams);
$summary = $totalStmt->fetch();
$totalExpStmt = $pdo->prepare("
SELECT SUM(amount) as total_expenses
FROM expenses e
WHERE expense_date BETWEEN ? AND ?
$expenseOutletCondition
");
$totalExpStmt->execute($queryParams);
$totalExpenses = $totalExpStmt->fetchColumn() ?? 0;
$netProfit = ($summary['total_revenue'] ?? 0) - $totalExpenses;
?>
<div class="container-fluid p-0">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
<div>
<h2 class="fw-bold mb-1">Daily Reports</h2>
<p class="text-muted mb-0">Overview of your business performance</p>
</div>
<form class="d-flex gap-2" method="GET">
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($startDate) ?>">
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($endDate) ?>">
<button type="submit" class="btn btn-primary">Filter</button>
<form class="row g-2 align-items-center" method="GET">
<div class="col-auto">
<select name="outlet_id" class="form-select" style="min-width: 180px;">
<option value="">All Outlets</option>
<?php foreach ($allOutlets as $o): ?>
<option value="<?= $o['id'] ?>" <?= $outletId == $o['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-calendar"></i></span>
<input type="date" name="start_date" class="form-control border-start-0" value="<?= htmlspecialchars($startDate) ?>">
</div>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-calendar"></i></span>
<input type="date" name="end_date" class="form-control border-start-0" value="<?= htmlspecialchars($endDate) ?>">
</div>
</div>
<div class="col-auto">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 text-nowrap">Filter</button>
<a href="reports.php" class="btn btn-light text-nowrap">Reset</a>
</div>
</div>
</form>
</div>
@ -114,6 +183,32 @@ $summary = $totalStmt->fetch();
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card border-0 shadow-sm stat-card p-3">
<div class="d-flex align-items-center gap-3">
<div class="icon-box bg-danger-subtle text-danger">
<i class="bi bi-cash-stack"></i>
</div>
<div>
<small class="text-muted d-block">Total Expenses</small>
<h4 class="fw-bold mb-0"><?= format_currency($totalExpenses) ?></h4>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card border-0 shadow-sm stat-card p-3">
<div class="d-flex align-items-center gap-3">
<div class="icon-box bg-info-subtle text-info">
<i class="bi bi-piggy-bank"></i>
</div>
<div>
<small class="text-muted d-block">Net Profit</small>
<h4 class="fw-bold mb-0 <?= $netProfit < 0 ? 'text-danger' : 'text-info' ?>"><?= format_currency($netProfit) ?></h4>
</div>
</div>
</div>
</div>
</div>
<div class="row g-4">
@ -192,8 +287,8 @@ $summary = $totalStmt->fetch();
</div>
<!-- Sales by Category -->
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3 border-0">
<h5 class="fw-bold mb-0"><i class="bi bi-tags me-2 text-warning"></i> Sales by Category</h5>
</div>
@ -227,6 +322,41 @@ $summary = $totalStmt->fetch();
</div>
</div>
</div>
<!-- Expenses by Category -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3 border-0">
<h5 class="fw-bold mb-0"><i class="bi bi-cash-stack me-2 text-danger"></i> Expenses by Category</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-3">Category</th>
<th class="text-end pe-3">Total Amount</th>
</tr>
</thead>
<tbody>
<?php if (empty($expenseSales)): ?>
<tr><td colspan="2" class="text-center py-4 text-muted">No data found for this period</td></tr>
<?php else: ?>
<?php foreach ($expenseSales as $exp): ?>
<tr>
<td class="ps-3">
<div class="fw-bold"><?= htmlspecialchars($exp['category_name']) ?></div>
</td>
<td class="text-end pe-3 fw-bold text-danger"><?= format_currency($exp['total_amount']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("suppliers_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();

View File

@ -1,4 +1,6 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("tables_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();

208
admin/user_group_edit.php Normal file
View File

@ -0,0 +1,208 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('user_groups_view');
$id = $_GET['id'] ?? null;
$group = null;
// Handle New Group Creation via POST from user_groups.php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$id && isset($_POST['name'])) {
if (!has_permission('user_groups_add')) {
header('Location: user_groups.php?error=permission_denied');
exit;
}
$stmt = $pdo->prepare("INSERT INTO user_groups (name, permissions) VALUES (?, '')");
$stmt->execute([$_POST['name']]);
$id = $pdo->lastInsertId();
header("Location: user_group_edit.php?id=" . $id);
exit;
}
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM user_groups WHERE id = ?");
$stmt->execute([$id]);
$group = $stmt->fetch(PDO::FETCH_ASSOC);
}
if (!$group) {
header('Location: user_groups.php');
exit;
}
$message = '';
$modules = [
'dashboard' => 'Dashboard',
'pos' => 'POS Terminal',
'kitchen' => 'Kitchen View',
'orders' => 'Orders',
'products' => 'Products',
'categories' => 'Categories',
'customers' => 'Customers',
'outlets' => 'Outlets',
'areas' => 'Areas',
'tables' => 'Tables',
'suppliers' => 'Suppliers',
'payment_types' => 'Payment Types',
'loyalty' => 'Loyalty',
'ads' => 'Ads',
'reports' => 'Reports',
'users' => 'Users',
'user_groups' => 'User Groups',
'settings' => 'Settings'
];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_group') {
if (!has_permission('user_groups_add')) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to edit groups.</div>';
} else {
$name = $_POST['name'];
$permissions = isset($_POST['perms']) ? implode(',', $_POST['perms']) : '';
// Check if name is not empty
if (empty($name)) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3"><i class="bi bi-exclamation-triangle-fill me-2"></i>Group name cannot be empty.</div>';
} else {
$stmt = $pdo->prepare("UPDATE user_groups SET name = ?, permissions = ? WHERE id = ?");
if ($stmt->execute([$name, $permissions, $id])) {
$message = '<div class="alert alert-success border-0 shadow-sm rounded-3"><i class="bi bi-check-circle-fill me-2"></i>Group updated successfully!</div>';
// Refresh group data
$stmt = $pdo->prepare("SELECT * FROM user_groups WHERE id = ?");
$stmt->execute([$id]);
$group = $stmt->fetch(PDO::FETCH_ASSOC);
} else {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3"><i class="bi bi-exclamation-triangle-fill me-2"></i>Error updating group.</div>';
}
}
}
}
$current_perms = explode(',', $group['permissions']);
include 'includes/header.php';
?>
<div class="mb-4">
<a href="user_groups.php" class="text-decoration-none text-muted small"><i class="bi bi-arrow-left"></i> Back to Groups</a>
<h2 class="fw-bold mt-2">Edit Group: <?= htmlspecialchars($group['name']) ?></h2>
</div>
<?= $message ?>
<form method="POST">
<input type="hidden" name="action" value="update_group">
<div class="row">
<div class="col-md-12">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<div class="mb-4 col-md-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" value="<?= htmlspecialchars($group['name']) ?>" required style="border-radius: 12px;" <?= !has_permission('user_groups_add') ? 'readonly' : '' ?>>
</div>
<label class="form-label small fw-bold text-muted mb-3">MODULE PERMISSIONS</label>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr>
<th class="ps-4 py-3">Module</th>
<th class="text-center py-3">View</th>
<th class="text-center py-3">Add / Edit</th>
<th class="text-center py-3">Delete</th>
<th class="text-center py-3">Full Access</th>
</tr>
</thead>
<tbody>
<?php foreach ($modules as $key => $label): ?>
<tr>
<td class="ps-4 fw-medium text-dark"><?= $label ?></td>
<td class="text-center">
<div class="form-check form-check-inline m-0">
<input class="form-check-input perm-checkbox" type="checkbox" name="perms[]" value="<?= $key ?>_view" id="perm_<?= $key ?>_view" <?= (in_array($key . '_view', $current_perms) || in_array('all', $current_perms)) ? 'checked' : '' ?> <?= !has_permission('user_groups_add') ? 'disabled' : '' ?>>
</div>
</td>
<td class="text-center">
<div class="form-check form-check-inline m-0">
<input class="form-check-input perm-checkbox" type="checkbox" name="perms[]" value="<?= $key ?>_add" id="perm_<?= $key ?>_add" <?= (in_array($key . '_add', $current_perms) || in_array('all', $current_perms)) ? 'checked' : '' ?> <?= !has_permission('user_groups_add') ? 'disabled' : '' ?>>
</div>
</td>
<td class="text-center">
<div class="form-check form-check-inline m-0">
<input class="form-check-input perm-checkbox" type="checkbox" name="perms[]" value="<?= $key ?>_del" id="perm_<?= $key ?>_del" <?= (in_array($key . '_del', $current_perms) || in_array('all', $current_perms)) ? 'checked' : '' ?> <?= !has_permission('user_groups_add') ? 'disabled' : '' ?>>
</div>
</td>
<td class="text-center">
<?php if (has_permission('user_groups_add')): ?>
<button type="button" class="btn btn-sm btn-outline-secondary rounded-pill px-3" onclick="toggleRow('<?= $key ?>', true)">All</button>
<button type="button" class="btn btn-sm btn-link text-muted text-decoration-none" onclick="toggleRow('<?= $key ?>', false)">None</button>
<?php else: ?>
<span class="text-muted small">-</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<tr class="table-info bg-opacity-10">
<td class="ps-4 fw-bold">ADMINISTRATIVE</td>
<td colspan="3" class="text-center small text-muted">Grants full access to everything in the system.</td>
<td class="text-center">
<div class="form-check form-switch d-inline-block">
<input class="form-check-input" type="checkbox" name="perms[]" value="all" id="perm_all" <?= in_array('all', $current_perms) ? 'checked' : '' ?> <?= !has_permission('user_groups_add') ? 'disabled' : '' ?>>
<label class="form-check-label fw-bold" for="perm_all">Super Admin</label>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-4 pt-3 border-top d-flex justify-content-end gap-2">
<a href="user_groups.php" class="btn btn-light rounded-pill px-4">Cancel</a>
<?php if (has_permission('user_groups_add')): ?>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Save Permissions</button>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</form>
<script>
function toggleRow(key, state) {
document.getElementById('perm_' + key + '_view').checked = state;
document.getElementById('perm_' + key + '_add').checked = state;
document.getElementById('perm_' + key + '_del').checked = state;
}
// If Super Admin is checked, maybe disable others? Or just let them be.
const permAll = document.getElementById('perm_all');
if (permAll) {
permAll.addEventListener('change', function() {
if (this.checked) {
document.querySelectorAll('.perm-checkbox').forEach(cb => {
cb.checked = true;
cb.disabled = true;
});
} else {
document.querySelectorAll('.perm-checkbox').forEach(cb => {
cb.disabled = false;
});
}
});
// Initial state for Super Admin
if (permAll.checked) {
document.querySelectorAll('.perm-checkbox').forEach(cb => {
cb.disabled = true;
});
}
}
</script>
<?php include 'includes/footer.php'; ?>

View File

@ -3,139 +3,126 @@ require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('all'); // Only super admin can manage groups
require_permission('user_groups_view');
$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]);
if (!has_permission('user_groups_del')) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to delete groups.</div>';
} else {
$id = $_GET['delete'];
// Don't delete admin group
if ($id == 1) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Cannot delete the Administrator group.</div>';
} else {
$pdo->prepare("DELETE FROM user_groups WHERE id = ?")->execute([$id]);
header("Location: user_groups.php");
exit;
}
}
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'
];
// Fetch Groups
$groups = $pdo->query("SELECT g.*, (SELECT COUNT(*) FROM users u WHERE u.group_id = g.id) as user_count
FROM user_groups g
ORDER BY g.id ASC")->fetchAll(PDO::FETCH_ASSOC);
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>
<h2 class="fw-bold mb-1">User Groups / Roles</h2>
<p class="text-muted mb-0">Define permissions and access levels</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
<?php if (has_permission('user_groups_add')): ?>
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#addGroupModal" style="border-radius: 10px;">
<i class="bi bi-shield-plus me-1"></i> Add Group
</button>
<?php endif; ?>
</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 class="row g-4">
<?php foreach ($groups as $group): ?>
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm rounded-4 h-100 position-relative overflow-hidden">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start mb-3">
<div class="bg-primary bg-opacity-10 text-primary p-3 rounded-3 shadow-sm">
<i class="bi bi-shield-lock-fill fs-4"></i>
</div>
<?php if (has_permission('user_groups_add')): ?>
<a href="user_group_edit.php?id=<?= $group['id'] ?>" class="btn-icon-soft edit" title="Edit Permissions">
<i class="bi bi-pencil-fill"></i>
</a>
<?php endif; ?>
</div>
<h5 class="fw-bold mb-1"><?= htmlspecialchars($group['name']) ?></h5>
<p class="text-muted small mb-3"><?= $group['user_count'] ?> users assigned</p>
<div class="d-flex flex-wrap gap-1 mb-4">
<?php
$perms = explode(',', $group['permissions']);
$display_perms = array_slice($perms, 0, 3);
foreach ($display_perms as $p): if (empty($p)) continue; ?>
<span class="badge bg-light text-muted border px-2 py-1" style="font-size: 0.65rem;"><?= htmlspecialchars($p) ?></span>
<?php endforeach; ?>
<?php if (count($perms) > 3): ?>
<span class="badge bg-light text-muted border px-2 py-1" style="font-size: 0.65rem;">+<?= count($perms) - 3 ?> more</span>
<?php endif; ?>
</div>
<div class="d-flex gap-2">
<?php if (has_permission('user_groups_add')): ?>
<a href="user_group_edit.php?id=<?= $group['id'] ?>" class="btn btn-primary w-100 rounded-pill">Manage Permissions</a>
<?php endif; ?>
<?php if (has_permission('user_groups_del') && $group['id'] != 1): ?>
<a href="?delete=<?= $group['id'] ?>" class="btn btn-light text-danger w-100 rounded-pill" onclick="return confirm('Are you sure?')">Delete</a>
<?php endif; ?>
</div>
</div>
<?php if ($group['name'] === 'Administrator' || $group['permissions'] === 'all'): ?>
<div class="position-absolute top-0 end-0 m-3">
<span class="badge bg-warning text-dark shadow-sm">Super Admin</span>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Add Group Modal -->
<?php if (has_permission('user_groups_add')): ?>
<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>
<div class="modal-header border-0 pb-0 ps-4 pt-4">
<h5 class="modal-title fw-bold">Create New User Group</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<form action="user_group_edit.php" method="POST">
<div class="modal-body p-4">
<input type="hidden" name="action" value="add_group">
<div class="mb-4">
<div class="mb-3">
<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; ?>
<input type="text" name="name" class="form-control form-control-lg border-0 bg-light rounded-3" placeholder="e.g. Supervisor" required>
<p class="form-text small text-muted mt-2">After creating the group, you will be redirected to define its specific permissions.</p>
</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>
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-toggle="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold">Create & Configure</button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -3,219 +3,131 @@ require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('all'); // Only super admin can manage users
require_permission('users_view');
$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']) {
if (!has_permission('users_del')) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to delete users.</div>';
} else {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$id]);
header("Location: users.php");
exit;
}
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();
// Handle Search
$search = $_GET['search'] ?? '';
$params = [];
$query = "SELECT u.*, g.name as group_name
FROM users u
LEFT JOIN user_groups g ON u.group_id = g.id";
if ($search) {
$query .= " WHERE u.username LIKE ? OR u.full_name LIKE ? OR u.email LIKE ?";
$params = ["%$search%", "%search%", "%search%"];
}
$query .= " ORDER BY u.id DESC";
$users_pagination = paginate_query($pdo, $query, $params);
$users = $users_pagination['data'];
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>
<h2 class="fw-bold mb-1">Users</h2>
<p class="text-muted mb-0">Manage system staff and access</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>
<?php if (has_permission('users_add')): ?>
<a href="user_edit.php" class="btn btn-primary btn-lg shadow-sm" style="border-radius: 10px;">
<i class="bi bi-person-plus me-1"></i> Add User
</a>
<?php endif; ?>
</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 class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-0">
<div class="p-3 border-bottom bg-light bg-opacity-50">
<form method="GET" class="row g-2">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span>
<input type="text" name="search" class="form-control border-start-0" placeholder="Search users..." value="<?= htmlspecialchars($search) ?>">
</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 class="col-auto">
<button type="submit" class="btn btn-light border">Filter</button>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">User</th>
<th>Role / Group</th>
<th>Email</th>
<th>Status</th>
<th>Joined</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="rounded-circle bg-primary bg-opacity-10 text-primary d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px; font-weight: 600;">
<?= strtoupper(substr($user['username'], 0, 1)) ?>
</div>
<div>
<div class="fw-bold text-dark"><?= htmlspecialchars($user['full_name'] ?: $user['username']) ?></div>
<div class="text-muted small">@<?= htmlspecialchars($user['username']) ?></div>
</div>
</div>
</td>
<td>
<span class="badge bg-info bg-opacity-10 text-info border border-info border-opacity-25 px-3 rounded-pill fw-medium">
<?= htmlspecialchars($user['group_name'] ?: 'No Group') ?>
</span>
</td>
<td><?= htmlspecialchars($user['email'] ?: '-') ?></td>
<td>
<?php if ($user['is_active']): ?>
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25 px-3 rounded-pill">Active</span>
<?php else: ?>
<span class="badge bg-danger bg-opacity-10 text-danger border border-danger border-opacity-25 px-3 rounded-pill">Inactive</span>
<?php endif; ?>
</td>
<td class="text-muted small"><?= date('M d, Y', strtotime($user['created_at'])) ?></td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<?php if (has_permission('users_add')): ?>
<a href="user_edit.php?id=<?= $user['id'] ?>" class="btn btn-sm btn-outline-primary rounded-pill px-3">Edit</a>
<?php endif; ?>
<?php if (has_permission('users_del')): ?>
<a href="?delete=<?= $user['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Delete this user?')">Delete</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="p-3 border-top">
<?php render_pagination_controls($users_pagination); ?>
</div>
</div>
</div>

View File

@ -1,6 +1,7 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/WablasService.php';
$input = file_get_contents('php://input');
@ -70,8 +71,9 @@ try {
$customer_phone = $data['customer_phone'] ?? null;
// Fetch Loyalty Settings
$settingsStmt = $pdo->query("SELECT points_per_order, points_for_free_meal FROM loyalty_settings WHERE id = 1");
$settingsStmt = $pdo->query("SELECT is_enabled, points_per_order, points_for_free_meal FROM loyalty_settings WHERE id = 1");
$settings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$loyalty_enabled = $settings ? (bool)$settings['is_enabled'] : true;
$points_per_order = $settings ? intval($settings['points_per_order']) : 10;
$points_threshold = $settings ? intval($settings['points_for_free_meal']) : 70;
@ -93,7 +95,7 @@ try {
}
// Loyalty Redemption Logic
$redeem_loyalty = !empty($data['redeem_loyalty']);
$redeem_loyalty = !empty($data['redeem_loyalty']) && $loyalty_enabled;
if ($redeem_loyalty && $customer_id) {
if ($current_points < $points_threshold) {
throw new Exception("Insufficient loyalty points for redemption.");
@ -113,37 +115,75 @@ try {
}
// Award Points
if ($customer_id) {
if ($customer_id && $loyalty_enabled) {
$awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?");
$awardStmt->execute([$points_per_order, $customer_id]);
$points_awarded = $points_per_order;
}
// Payment Type
// User/Payment info
$user = get_logged_user();
$user_id = $user ? $user['id'] : null;
$payment_type_id = isset($data['payment_type_id']) ? intval($data['payment_type_id']) : null;
$discount = isset($data['discount']) ? floatval($data['discount']) : 0.00;
$total_amount = isset($data['total_amount']) ? floatval($data['total_amount']) : 0.00;
// Total amount will be recalculated on server to be safe
$calculated_total = 0;
// First, process items to calculate real total
$processed_items = [];
if (!empty($data['items']) && is_array($data['items'])) {
foreach ($data['items'] as $item) {
$pid = $item['product_id'] ?? ($item['id'] ?? null);
if (!$pid) continue;
$qty = intval($item['quantity'] ?? 1);
$vid = $item['variant_id'] ?? null;
// Fetch Product Price (Promo aware)
$pStmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$pStmt->execute([$pid]);
$product = $pStmt->fetch(PDO::FETCH_ASSOC);
if (!$product) continue;
$unit_price = get_product_price($product);
// Add variant adjustment
if ($vid) {
$vStmt = $pdo->prepare("SELECT price_adjustment FROM product_variants WHERE id = ? AND product_id = ?");
$vStmt->execute([$vid, $pid]);
$vAdjustment = $vStmt->fetchColumn();
if ($vAdjustment !== false) {
$unit_price += floatval($vAdjustment);
}
}
$item_total = $unit_price * $qty;
$calculated_total += $item_total;
$processed_items[] = [
'product_id' => $pid,
'variant_id' => $vid,
'quantity' => $qty,
'unit_price' => $unit_price,
'name' => $product['name']
];
}
}
$final_total = max(0, $calculated_total - $discount);
// Check for Existing Order ID (Update Mode)
$order_id = isset($data['order_id']) ? intval($data['order_id']) : null;
$is_update = false;
if ($order_id) {
// Verify existence and status (optional: only update pending orders?)
$checkStmt = $pdo->prepare("SELECT id FROM orders WHERE id = ?");
$checkStmt->execute([$order_id]);
if ($checkStmt->fetch()) {
$is_update = true;
} else {
// If ID sent but not found, create new? Or error?
// Let's treat as new if not found to avoid errors, or maybe user meant to update.
// Safe bet: error if explicit update requested but failed.
// But for now, let's just create new if ID is invalid, or better, stick to the plan: Update if ID present.
$order_id = null; // Reset to create new
$order_id = null;
}
}
@ -157,81 +197,52 @@ try {
$stmt->execute([
$outlet_id, $table_id, $table_number, $order_type,
$customer_id, $customer_name, $customer_phone,
$payment_type_id, $total_amount, $discount, $user_id,
$payment_type_id, $final_total, $discount, $user_id,
$order_id
]);
// Clear existing items to replace them
// Clear existing items
$delStmt = $pdo->prepare("DELETE FROM order_items WHERE order_id = ?");
$delStmt->execute([$order_id]);
} else {
// INSERT New Order
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, payment_type_id, total_amount, discount, user_id, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $payment_type_id, $total_amount, $discount, $user_id]);
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $payment_type_id, $final_total, $discount, $user_id]);
$order_id = $pdo->lastInsertId();
}
// Insert Items (Common for both Insert and Update)
// Insert Items
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)");
// Pre-prepare statements for name fetching
$prodNameStmt = $pdo->prepare("SELECT name FROM products WHERE id = ?");
$varNameStmt = $pdo->prepare("SELECT name FROM product_variants WHERE id = ?");
$order_items_list = []; // To store item details for notification
$order_items_list = [];
if (!empty($data['items']) && is_array($data['items'])) {
foreach ($data['items'] as $item) {
$pid = $item['product_id'] ?? ($item['id'] ?? null);
$qty = $item['quantity'] ?? 1;
$price = $item['unit_price'] ?? ($item['price'] ?? 0);
$vid = $item['variant_id'] ?? null;
foreach ($processed_items as $pi) {
$item_stmt->execute([$order_id, $pi['product_id'], $pi['variant_id'], $pi['quantity'], $pi['unit_price']]);
if ($pid) {
$item_stmt->execute([$order_id, $pid, $vid, $qty, $price]);
// Fetch names for notification
$prodNameStmt->execute([$pid]);
$pRow = $prodNameStmt->fetch(PDO::FETCH_ASSOC);
$pName = $pRow ? $pRow['name'] : 'Item';
if ($vid) {
$varNameStmt->execute([$vid]);
$vRow = $varNameStmt->fetch(PDO::FETCH_ASSOC);
if ($vRow) {
$pName .= " (" . $vRow['name'] . ")";
}
}
$order_items_list[] = "$qty x $pName";
}
$pName = $pi['name'];
if ($pi['variant_id']) {
$varNameStmt->execute([$pi['variant_id']]);
$vName = $varNameStmt->fetchColumn();
if ($vName) $pName .= " ($vName)";
}
$order_items_list[] = "{$pi['quantity']} x $pName";
}
$pdo->commit();
// --- Post-Transaction Actions ---
// Send WhatsApp Notification if Customer exists
// --- Post-Transaction Actions (WhatsApp) ---
if ($customer_id && $customer_phone) {
try {
// Calculate final points
$final_points = $current_points - $points_deducted + $points_awarded;
// Determine message content
$final_points = $current_points - $points_deducted + ($loyalty_enabled ? $points_awarded : 0);
$wablas = new WablasService($pdo);
// Fetch Company Name
require_once __DIR__ . '/../includes/functions.php';
$companySettings = get_company_settings();
$company_name = $companySettings['company_name'] ?? 'Flatlogic POS';
// Fetch Template
$stmt = $pdo->prepare("SELECT setting_value FROM integration_settings WHERE provider = 'wablas' AND setting_key = 'order_template'");
$stmt->execute();
$template = $stmt->fetchColumn();
// Default Template if not set
if (!$template) {
$template = "Dear *{customer_name}*,
@ -248,35 +259,27 @@ You've earned *{points_earned} points* with this order.
{loyalty_status}";
}
// Calculate Loyalty Status String
$loyalty_status = "";
if ($final_points >= $points_threshold) {
$loyalty_status = "🎉 Congratulations! You have enough points for a *FREE MEAL* on your next visit!";
} else {
$needed = $points_threshold - $final_points;
$loyalty_status = "You need *$needed more points* to unlock a free meal.";
if ($loyalty_enabled) {
$loyalty_status = ($final_points >= $points_threshold)
? "🎉 Congratulations! You have enough points for a *FREE MEAL* on your next visit!"
: "You need *" . ($points_threshold - $final_points) . " more points* to unlock a free meal.";
}
// Prepare replacements
$replacements = [
'{customer_name}' => $customer_name,
'{company_name}' => $company_name,
'{order_id}' => $order_id,
'{order_details}' => !empty($order_items_list) ? implode("\n", $order_items_list) : 'N/A',
'{total_amount}' => number_format($total_amount, 3),
'{points_earned}' => $points_awarded,
'{order_details}' => implode("\n", $order_items_list),
'{total_amount}' => number_format($final_total, 3),
'{points_earned}' => $loyalty_enabled ? $points_awarded : 0,
'{points_redeemed}' => $points_deducted,
'{new_balance}' => $final_points,
'{loyalty_status}' => $loyalty_status
];
$msg = str_replace(array_keys($replacements), array_values($replacements), $template);
// Send (fire and forget, don't block response on failure, just log)
$res = $wablas->sendMessage($customer_phone, $msg);
if (!$res['success']) {
error_log("Wablas Notification Failed: " . $res['message']);
}
$wablas->sendMessage($customer_phone, $msg);
} catch (Exception $w) {
error_log("Wablas Exception: " . $w->getMessage());
@ -286,9 +289,7 @@ You've earned *{points_earned} points* with this order.
echo json_encode(['success' => true, 'order_id' => $order_id]);
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
if ($pdo->inTransaction()) $pdo->rollBack();
error_log("Order Error: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -256,7 +256,7 @@ document.addEventListener('DOMContentLoaded', () => {
// customerInfo.classList.remove('d-none');
// Loyalty Logic
if (loyaltySection) {
if (loyaltySection && typeof LOYALTY_SETTINGS !== 'undefined' && LOYALTY_SETTINGS.is_enabled) {
loyaltySection.classList.remove('d-none');
loyaltyPointsDisplay.textContent = cust.points + ' pts';
@ -296,6 +296,8 @@ document.addEventListener('DOMContentLoaded', () => {
// Loyalty Redeem Click
if (redeemLoyaltyBtn) {
redeemLoyaltyBtn.addEventListener('click', () => {
if (typeof LOYALTY_SETTINGS === 'undefined' || !LOYALTY_SETTINGS.is_enabled) return;
if (cart.length === 0) {
showToast("Cart is empty!", "warning");
return;

View File

@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS expense_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS expenses (
id INT AUTO_INCREMENT PRIMARY KEY,
category_id INT NOT NULL,
outlet_id INT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
description TEXT,
expense_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES expense_categories(id),
FOREIGN KEY (outlet_id) REFERENCES outlets(id)
);

View File

@ -0,0 +1,4 @@
ALTER TABLE products
ADD COLUMN promo_discount_percent DECIMAL(5, 2) DEFAULT NULL,
ADD COLUMN promo_date_from DATE DEFAULT NULL,
ADD COLUMN promo_date_to DATE DEFAULT NULL;

View File

@ -0,0 +1,18 @@
-- Add is_enabled column to loyalty_settings table
SET @dbname = DATABASE();
SET @tablename = "loyalty_settings";
SET @columnname = "is_enabled";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
"SELECT 1",
"ALTER TABLE loyalty_settings ADD COLUMN is_enabled TINYINT(1) DEFAULT 1;"
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@ -3,10 +3,12 @@
function get_company_settings() {
static $settings = null;
if ($settings === null) {
$pdo = db();
try {
$stmt = $pdo->query("SELECT * FROM company_settings LIMIT 1");
$settings = $stmt->fetch(PDO::FETCH_ASSOC);
if (function_exists('db')) {
$pdo = db();
$stmt = $pdo->query("SELECT * FROM company_settings LIMIT 1");
$settings = $stmt->fetch(PDO::FETCH_ASSOC);
}
} catch (Exception $e) {
// Log error or ignore if table doesn't exist yet
}
@ -33,6 +35,31 @@ function format_currency($amount) {
return $settings['currency_symbol'] . number_format((float)$amount, (int)$settings['currency_decimals']);
}
/**
* Calculate the current price of a product considering promotions.
*
* @param array|object $product The product data from DB.
* @return float The effective price.
*/
function get_product_price($product) {
$product = (array)$product;
$price = (float)$product['price'];
$today = date('Y-m-d');
$promo_active = !empty($product['promo_discount_percent']) &&
!empty($product['promo_date_from']) &&
!empty($product['promo_date_to']) &&
$today >= $product['promo_date_from'] &&
$today <= $product['promo_date_to'];
if ($promo_active) {
$discount = (float)$product['promo_discount_percent'];
$price = $price * (1 - ($discount / 100));
}
return $price;
}
/**
* Paginate a query result.
*
@ -247,6 +274,7 @@ function has_permission($permission) {
if (!$user) return false;
$userPermissions = $user['permissions'] ?? '';
// Global bypass for super admins
if ($userPermissions === 'all') return true;
$perms = explode(',', $userPermissions);

View File

@ -2,7 +2,7 @@
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/functions.php';
require_login();
require_permission('kitchen_view');
$pdo = db();
$currentUser = get_logged_user();
@ -101,17 +101,21 @@ if (!has_permission('all')) {
</select>
<span id="clock" class="text-muted d-none d-md-inline"></span>
<?php if (has_permission('kitchen_add')): ?>
<button class="btn btn-danger btn-sm" onclick="serveAll()">
<i class="bi bi-check2-all"></i> Serve All
</button>
<?php endif; ?>
<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">
<?php if (has_permission('dashboard_view')): ?>
<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>
<?php endif; ?>
<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>
@ -130,6 +134,7 @@ if (!has_permission('all')) {
<script>
const OUTLET_ID = <?= $current_outlet_id ?>;
const CAN_EDIT = <?= has_permission('kitchen_add') ? 'true' : 'false' ?>;
async function fetchOrders() {
try {
@ -150,8 +155,6 @@ function renderOrders(orders) {
return;
}
// Build the HTML using a variable, so we don't clear/flash the grid if it's identical
// But for now, simple innerHTML replacement is fine for this MVP.
const newHtml = orders.map(order => {
let actionBtn = '';
let statusBadge = '';
@ -160,15 +163,15 @@ function renderOrders(orders) {
if (order.status === 'pending') {
statusBadge = '<span class="badge badge-pending">Pending</span>';
cardClass += 'status-pending';
actionBtn = `<button class="btn btn-info btn-sm w-100 text-white" onclick="updateStatus(${order.id}, 'preparing')">Start Preparing</button>`;
if (CAN_EDIT) actionBtn = `<button class="btn btn-info btn-sm w-100 text-white" onclick="updateStatus(${order.id}, 'preparing')">Start Preparing</button>`;
} else if (order.status === 'preparing') {
statusBadge = '<span class="badge badge-preparing">Preparing</span>';
cardClass += 'status-preparing';
actionBtn = `<button class="btn btn-success btn-sm w-100" onclick="updateStatus(${order.id}, 'ready')">Mark Ready</button>`;
if (CAN_EDIT) actionBtn = `<button class="btn btn-success btn-sm w-100" onclick="updateStatus(${order.id}, 'ready')">Mark Ready</button>`;
} else if (order.status === 'ready') {
statusBadge = '<span class="badge badge-ready">Ready</span>';
cardClass += 'status-ready';
actionBtn = `<button class="btn btn-secondary btn-sm w-100" onclick="updateStatus(${order.id}, 'completed')">Serve / Complete</button>`;
if (CAN_EDIT) actionBtn = `<button class="btn btn-secondary btn-sm w-100" onclick="updateStatus(${order.id}, 'completed')">Serve / Complete</button>`;
}
const itemsHtml = order.items.map(item => `
@ -198,7 +201,7 @@ function renderOrders(orders) {
</ul>
</div>
<div class="card-footer bg-white border-top-0 pb-3">
${actionBtn}
${actionBtn || '<div class="text-center small text-muted">View Only</div>'}
</div>
</div>
</div>
@ -209,6 +212,7 @@ function renderOrders(orders) {
}
function updateStatus(orderId, newStatus) {
if (!CAN_EDIT) return;
Swal.fire({
title: 'Update Status?',
text: `Move order #${orderId} to ${newStatus}?`,
@ -251,6 +255,7 @@ async function performUpdate(orderId, newStatus) {
}
async function serveAll() {
if (!CAN_EDIT) return;
const result = await Swal.fire({
title: 'Serve All Orders?',
text: "This will mark all active orders as completed and clear the screen.",

46
pos.php
View File

@ -3,7 +3,7 @@ declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/functions.php';
require_login();
require_permission('pos_view');
$pdo = db();
$currentUser = get_logged_user();
@ -60,6 +60,13 @@ foreach ($outlets as $o) {
break;
}
}
// Fetch Loyalty Settings
$loyalty_stmt = $pdo->query("SELECT * FROM loyalty_settings WHERE id = 1");
$loyalty_settings = $loyalty_stmt->fetch(PDO::FETCH_ASSOC);
if (!$loyalty_settings) {
$loyalty_settings = ['is_enabled' => 0, 'points_per_order' => 0, 'points_for_free_meal' => 0];
}
?>
<!doctype html>
<html lang="en">
@ -104,10 +111,19 @@ foreach ($outlets as $o) {
</a>
<div class="d-flex align-items-center gap-3">
<?php if (has_permission('kitchen_view')): ?>
<a href="kitchen.php" class="btn btn-sm btn-outline-secondary">Kitchen View</a>
<?php endif; ?>
<?php if (has_permission('pos_add')): ?>
<button class="btn btn-sm btn-outline-danger" id="recall-bill-btn"><i class="bi bi-clock-history"></i> Recall Bill</button>
<?php endif; ?>
<a href="pos.php?order_type=dine-in" class="btn btn-sm btn-outline-secondary">Waiter View</a>
<?php if (has_permission('orders_view')): ?>
<a href="admin/orders.php" class="btn btn-sm btn-outline-secondary">Current Orders</a>
<?php endif; ?>
<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) ?>
@ -186,18 +202,30 @@ foreach ($outlets as $o) {
<div class="row row-cols-2 row-cols-lg-5 g-3" id="products-container">
<?php foreach ($all_products as $product):
$has_variants = !empty($variants_by_product[$product['id']]);
$effective_price = get_product_price($product);
$is_promo = $effective_price < $product['price'];
?>
<div class="col product-item" data-category-id="<?= $product['category_id'] ?>">
<div class="card h-100 border-0 shadow-sm product-card add-to-cart"
data-id="<?= $product['id'] ?>"
data-name="<?= htmlspecialchars($product['name']) ?>"
data-price="<?= $product['price'] ?>"
data-price="<?= $effective_price ?>"
data-has-variants="<?= $has_variants ? 'true' : 'false' ?>">
<div class="position-relative">
<img src="https://picsum.photos/seed/<?= $product['id'] ?>/300/200" class="card-img-top object-fit-cover" alt="..." style="height: 120px;">
<div class="position-absolute bottom-0 end-0 m-2">
<span class="badge bg-dark rounded-pill"><?= format_currency($product['price']) ?></span>
<?php if ($is_promo): ?>
<span class="badge bg-danger rounded-pill shadow-sm"><?= format_currency($effective_price) ?></span>
<span class="badge bg-dark rounded-pill opacity-75 text-decoration-line-through x-small" style="font-size: 0.7rem;"><?= format_currency($product['price']) ?></span>
<?php else: ?>
<span class="badge bg-dark rounded-pill"><?= format_currency($product['price']) ?></span>
<?php endif; ?>
</div>
<?php if ($is_promo): ?>
<div class="position-absolute top-0 start-0 m-2">
<span class="badge bg-warning text-dark fw-bold rounded-pill">SALE</span>
</div>
<?php endif; ?>
</div>
<div class="card-body p-2">
<h6 class="card-title fw-bold small mb-1 text-truncate"><?= htmlspecialchars($product['name']) ?></h6>
@ -246,7 +274,8 @@ foreach ($outlets as $o) {
<i class="bi bi-check-circle-fill me-1"></i> <span id="customer-name-display"></span>
</div>
<!-- Loyalty Section -->
<!-- Loyalty Section (Hidden if disabled) -->
<?php if ($loyalty_settings['is_enabled']): ?>
<div id="loyalty-section" class="d-none mt-2 p-2 bg-warning-subtle rounded border border-warning">
<div class="d-flex justify-content-between align-items-center">
<div>
@ -259,6 +288,7 @@ foreach ($outlets as $o) {
</div>
<div id="loyalty-message" class="small text-muted mt-1 fst-italic" style="font-size: 0.75rem;"></div>
</div>
<?php endif; ?>
</div>
</div>
@ -290,6 +320,8 @@ foreach ($outlets as $o) {
<span class="fs-5 fw-bold">Total</span>
<span class="fs-4 fw-bold text-primary" id="cart-total-price"><?= format_currency(0) ?></span>
</div>
<?php if (has_permission('pos_add')): ?>
<div class="d-flex gap-2 w-100">
<button class="btn btn-primary w-50 btn-lg shadow-sm" id="quick-order-btn" disabled>
Quick Order <i class="bi bi-lightning-fill ms-2"></i>
@ -298,6 +330,11 @@ foreach ($outlets as $o) {
Place Order <i class="bi bi-clock ms-2"></i>
</button>
</div>
<?php else: ?>
<div class="alert alert-warning small py-2 mb-0 text-center">
<i class="bi bi-info-circle me-1"></i> View Only Mode
</div>
<?php endif; ?>
</div>
</div>
@ -402,6 +439,7 @@ foreach ($outlets as $o) {
<script>
const COMPANY_SETTINGS = <?= json_encode($settings) ?>;
const LOYALTY_SETTINGS = <?= json_encode($loyalty_settings) ?>;
const PRODUCT_VARIANTS = <?= json_encode($variants_by_product) ?>;
const PAYMENT_TYPES = <?= json_encode($payment_types) ?>;
const CURRENT_OUTLET = <?= json_encode(['id' => $outlet_id, 'name' => $current_outlet_name]) ?>;

View File

@ -90,18 +90,33 @@ foreach ($variants_raw as $v) {
<div class="row g-3" id="products-container">
<?php foreach ($all_products as $product):
$has_variants = !empty($variants_by_product[$product['id']]);
$effective_price = get_product_price($product);
$is_promo = $effective_price < (float)$product['price'];
?>
<div class="col-6 col-md-4 product-item" data-category-id="<?= $product['category_id'] ?>">
<div class="card h-100 product-card"
onclick="handleProductClick(<?= htmlspecialchars(json_encode([
'id' => $product['id'],
'name' => $product['name'],
'price' => (float)$product['price'],
'price' => (float)$effective_price,
'has_variants' => $has_variants
])) ?>)">
<div class="position-relative">
<img src="https://picsum.photos/seed/<?= $product['id'] ?>/400/300" class="card-img-top product-img" alt="<?= htmlspecialchars($product['name']) ?>">
<div class="badge-price text-primary"><?= number_format((float)$product['price'], 3) ?> OMR</div>
<div class="badge-price text-primary">
<?php if ($is_promo): ?>
<span class="text-danger fw-bold"><?= number_format($effective_price, 3) ?></span>
<small class="text-muted text-decoration-line-through ms-1" style="font-size: 0.7rem;"><?= number_format((float)$product['price'], 3) ?></small>
<?php else: ?>
<?= number_format((float)$product['price'], 3) ?>
<?php endif; ?>
OMR
</div>
<?php if ($is_promo): ?>
<div class="position-absolute top-0 start-0 m-2">
<span class="badge bg-warning text-dark fw-bold rounded-pill" style="font-size: 0.6rem;">SALE</span>
</div>
<?php endif; ?>
</div>
<div class="card-body p-2">
<h6 class="card-title mb-1 small fw-bold text-truncate"><?= htmlspecialchars($product['name']) ?></h6>