Autosave: 20260224-051916

This commit is contained in:
Flatlogic Bot 2026-02-24 05:19:16 +00:00
parent 0bec862e83
commit b26eab2ba0
34 changed files with 4604 additions and 2526 deletions

View File

@ -1,164 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
$ad = null;
$message = '';
$isEdit = false;
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM ads_images WHERE id = ?");
$stmt->execute([$id]);
$ad = $stmt->fetch();
if ($ad) {
$isEdit = true;
} else {
header("Location: ads.php");
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = trim($_POST['title']);
$sort_order = (int)$_POST['sort_order'];
$is_active = isset($_POST['is_active']) ? 1 : 0;
$display_layout = $_POST['display_layout'] ?? 'both';
$image_path = $isEdit ? $ad['image_path'] : null;
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/ads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$fileInfo = pathinfo($_FILES['image']['name']);
$fileExt = strtolower($fileInfo['extension']);
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($fileExt, $allowedExts)) {
$fileName = uniqid('ad_') . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
if (move_uploaded_file($_FILES['image']['tmp_name'], $targetFile)) {
$image_path = 'assets/images/ads/' . $fileName;
} else {
$message = '<div class="alert alert-danger">Failed to upload image.</div>';
}
} else {
$message = '<div class="alert alert-danger">Invalid file type. Allowed: jpg, png, gif, webp.</div>';
}
}
if (empty($image_path) && !$isEdit) {
$message = '<div class="alert alert-danger">Image is required for new advertisements.</div>';
}
if (empty($message)) {
try {
if ($isEdit) {
$stmt = $pdo->prepare("UPDATE ads_images SET title = ?, sort_order = ?, is_active = ?, display_layout = ?, image_path = ? WHERE id = ?");
$stmt->execute([$title, $sort_order, $is_active, $display_layout, $image_path, $id]);
header("Location: ads.php?success=updated");
exit;
} else {
$stmt = $pdo->prepare("INSERT INTO ads_images (title, sort_order, is_active, display_layout, image_path) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$title, $sort_order, $is_active, $display_layout, $image_path]);
header("Location: ads.php?success=created");
exit;
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
if (!$isEdit) {
$ad = [
'title' => $_POST['title'] ?? '',
'sort_order' => $_POST['sort_order'] ?? 0,
'is_active' => 1,
'display_layout' => 'both',
'image_path' => ''
];
}
include 'includes/header.php';
?>
<div class="mb-4">
<a href="ads.php" class="text-decoration-none text-muted mb-2 d-inline-block"><i class="bi bi-arrow-left"></i> Back to Ads Management</a>
<h2 class="fw-bold mb-0"><?= $isEdit ? 'Edit Advertisement' : 'Add New Advertisement' ?></h2>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label class="form-label">Title / Caption</label>
<input type="text" name="title" class="form-control" value="<?= htmlspecialchars($ad['title'] ?? '') ?>" placeholder="e.g. Special Offer 50% Off">
<div class="form-text">This will be shown as a caption on the image.</div>
</div>
<div class="mb-3">
<label class="form-label">Sort Order</label>
<input type="number" name="sort_order" class="form-control" value="<?= htmlspecialchars($ad['sort_order'] ?? 0) ?>">
<div class="form-text">Lower numbers appear first in the slider.</div>
</div>
<div class="mb-3">
<label class="form-label d-block">Display Layout Preference</label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="display_layout" id="layout_both" value="both" <?= ($ad['display_layout'] ?? 'both') === 'both' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary" for="layout_both">Both Layouts</label>
<input type="radio" class="btn-check" name="display_layout" id="layout_split" value="split" <?= ($ad['display_layout'] ?? 'both') === 'split' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary" for="layout_split">Split View Only</label>
<input type="radio" class="btn-check" name="display_layout" id="layout_fullscreen" value="fullscreen" <?= ($ad['display_layout'] ?? 'both') === 'fullscreen' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary" for="layout_fullscreen">Fullscreen Only</label>
</div>
<div class="form-text mt-2">Choose where this advertisement should be visible.</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="isActiveSwitch" <?= ($ad['is_active'] ?? 1) ? 'checked' : '' ?>>
<label class="form-check-label" for="isActiveSwitch">Active (Show in Slider)</label>
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Advertisement Image <span class="text-danger">*</span></label>
<?php if (!empty($ad['image_path'])): ?>
<div class="mb-2">
<img src="../<?= htmlspecialchars($ad['image_path']) ?>"
class="img-fluid rounded border shadow-sm"
style="max-height: 250px; width: 100%; object-fit: cover;"
alt="Ad Image">
</div>
<?php else: ?>
<div class="mb-2 p-5 bg-light text-center border rounded text-muted">
<i class="bi bi-images fs-1"></i><br>No Image Selected
</div>
<?php endif; ?>
<input type="file" name="image" class="form-control" accept="image/*" <?= !$isEdit ? 'required' : '' ?>>
<div class="form-text">Recommended size: 1920x1080 (HD) or 16:9 aspect ratio.</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-end gap-2">
<a href="ads.php" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary"><?= $isEdit ? 'Save Changes' : 'Upload Image' ?></button>
</div>
</form>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -1,82 +1,121 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("ads_view");
require_permission("ads");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Ensure the table exists (idempotent)
$pdo->exec("CREATE TABLE IF NOT EXISTS ads_images (
id INT AUTO_INCREMENT PRIMARY KEY,
image_path VARCHAR(255) NOT NULL,
title VARCHAR(255) DEFAULT NULL,
sort_order INT DEFAULT 0,
is_active TINYINT(1) DEFAULT 1,
display_layout ENUM('both', 'split', 'fullscreen') DEFAULT 'both',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// Handle Add/Edit Promo
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$title = trim($_POST['title']);
$sort_order = (int)$_POST['sort_order'];
$is_active = isset($_POST['is_active']) ? 1 : 0;
$display_layout = $_POST['display_layout'] ?? 'both';
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
// Ensure display_layout column exists (for older installations)
$pdo->exec("ALTER TABLE ads_images ADD COLUMN IF NOT EXISTS display_layout ENUM('both', 'split', 'fullscreen') DEFAULT 'both' AFTER is_active");
if (isset($_GET['delete'])) {
if (!has_permission('ads_del')) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to delete advertisements.</div>';
} else {
$id = $_GET['delete'];
// Get image path to delete file
$image_path = null;
if ($id) {
$stmt = $pdo->prepare("SELECT image_path FROM ads_images WHERE id = ?");
$stmt->execute([$id]);
$ad = $stmt->fetch();
if ($ad) {
$fullPath = __DIR__ . '/../' . $ad['image_path'];
if (file_exists($fullPath) && is_file($fullPath)) {
unlink($fullPath);
$image_path = $stmt->fetchColumn();
}
if (isset($_FILES['image']) && $_FILES['image']['error'] !== UPLOAD_ERR_NO_FILE) {
if ($_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/ads/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$new_file_name = uniqid('promo_') . '.' . $file_ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $new_file_name)) {
$image_path = 'assets/images/ads/' . $new_file_name;
} else {
$message = '<div class="alert alert-danger">Failed to move uploaded file.</div>';
}
} else {
$message = '<div class="alert alert-danger">Invalid file type.</div>';
}
} else {
$message = '<div class="alert alert-danger">File upload error.</div>';
}
}
if (empty($image_path) && $action === 'add_promo' && empty($message)) {
$message = '<div class="alert alert-danger">Image is required for new advertisements.</div>';
}
if (empty($message)) {
try {
if ($action === 'edit_promo' && $id) {
if (!has_permission('ads')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE ads_images SET title = ?, sort_order = ?, is_active = ?, display_layout = ?, image_path = ? WHERE id = ?");
$stmt->execute([$title, $sort_order, $is_active, $display_layout, $image_path, $id]);
$message = '<div class="alert alert-success">Advertisement updated successfully!</div>';
}
} elseif ($action === 'add_promo') {
if (!has_permission('ads')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO ads_images (title, sort_order, is_active, display_layout, image_path) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$title, $sort_order, $is_active, $display_layout, $image_path]);
$message = '<div class="alert alert-success">Advertisement created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['delete'])) {
if (!has_permission('ads')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$id = $_GET['delete'];
$stmt = $pdo->prepare("SELECT image_path FROM ads_images WHERE id = ?");
$stmt->execute([$id]);
$promo = $stmt->fetch();
if ($promo) {
$fullPath = __DIR__ . '/../' . $promo['image_path'];
if (file_exists($fullPath) && is_file($fullPath)) unlink($fullPath);
$pdo->prepare("DELETE FROM ads_images WHERE id = ?")->execute([$id]);
}
header("Location: ads.php");
exit;
}
}
$query = "SELECT * FROM ads_images ORDER BY sort_order ASC, created_at DESC";
$ads_pagination = paginate_query($pdo, $query);
$ads = $ads_pagination['data'];
$promos_pagination = paginate_query($pdo, $query);
$promos = $promos_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-0">Advertisement Slider</h2>
<p class="text-muted mb-0">Manage pictures for the public ads display page.</p>
<h2 class="fw-bold mb-0 text-dark">Advertisement Slider</h2>
<p class="text-muted mb-0">Manage pictures for the public display page.</p>
</div>
<?php if (has_permission('ads_add')): ?>
<a href="ad_edit.php" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Add Image
</a>
<?php if (has_permission('ads')): ?>
<button class="btn btn-primary btn-lg shadow-sm px-4" data-bs-toggle="modal" data-bs-target="#promoModal" onclick="preparePromoAddForm()" style="border-radius: 10px;">
<i class="bi bi-plus-lg me-1"></i> Add Image
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="alert alert-info border-0 shadow-sm d-flex align-items-center">
<i class="bi bi-info-circle-fill me-3 fs-4"></i>
<div>
These images will be displayed in a slider on the <strong><a href="../ads.php" target="_blank" class="alert-link">ads.php</a></strong> page.
You can now choose to show specific images in <strong>Split View</strong> or <strong>Fullscreen</strong> layout.
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($ads_pagination); ?>
<div class="p-3 border-bottom bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">Items List</h6>
<?php render_pagination_controls($promos_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
@ -91,66 +130,152 @@ include 'includes/header.php';
</tr>
</thead>
<tbody>
<?php foreach ($ads as $ad): ?>
<?php foreach ($promos as $promo): ?>
<tr>
<td class="ps-4 fw-medium"><?= $ad['sort_order'] ?></td>
<td class="ps-4 fw-medium text-dark"><?= $promo['sort_order'] ?></td>
<td>
<img src="../<?= htmlspecialchars($ad['image_path']) ?>"
<?php if (!empty($promo['image_path'])): ?>
<img src="../<?= htmlspecialchars($promo['image_path']) ?>"
class="rounded object-fit-cover border shadow-sm"
width="120" height="70"
alt="<?= htmlspecialchars($ad['title'] ?? '') ?>">
width="120" height="70">
<?php else: ?>
<div class="bg-light rounded border d-flex align-items-center justify-content-center text-muted" style="width: 120px; height: 70px;">No Image</div>
<?php endif; ?>
</td>
<td>
<div class="fw-bold"><?= htmlspecialchars($ad['title'] ?: 'No title') ?></div>
<small class="text-muted"><?= htmlspecialchars($ad['image_path']) ?></small>
<div class="fw-bold text-dark"><?= htmlspecialchars($promo['title'] ?: 'No title') ?></div>
</td>
<td>
<?php
$layoutLabel = 'Both';
$layoutClass = 'bg-primary-subtle text-primary';
if ($ad['display_layout'] === 'split') {
if (isset($promo['display_layout'])) {
if ($promo['display_layout'] === 'split') {
$layoutLabel = 'Split Only';
$layoutClass = 'bg-info-subtle text-info';
} elseif ($ad['display_layout'] === 'fullscreen') {
} elseif ($promo['display_layout'] === 'fullscreen') {
$layoutLabel = 'Fullscreen Only';
$layoutClass = 'bg-warning-subtle text-warning';
}
}
?>
<span class="badge <?= $layoutClass ?> px-3"><?= $layoutLabel ?></span>
</td>
<td>
<?php if ($ad['is_active']): ?>
<?php if (isset($promo['is_active']) && $promo['is_active']): ?>
<span class="badge bg-success-subtle text-success px-3">Active</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary px-3">Inactive</span>
<?php endif; ?>
</td>
<td class="text-end pe-4">
<?php if (has_permission('ads_add')): ?>
<a href="ad_edit.php?id=<?= $ad['id'] ?>" class="btn btn-sm btn-outline-primary me-1"><i class="bi bi-pencil"></i></a>
<?php endif; ?>
<?php if (has_permission('ads_del')): ?>
<a href="?delete=<?= $ad['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to delete this image?')"><i class="bi bi-trash"></i></a>
<?php if (has_permission('ads')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3 me-1"
data-bs-toggle="modal" data-bs-target="#promoModal"
onclick='preparePromoEditForm(<?= htmlspecialchars(json_encode($promo), ENT_QUOTES, "UTF-8") ?>)'>
<i class="bi bi-pencil me-1"></i> Edit
</button>
<a href="?delete=<?= $promo['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Are you sure?')"><i class="bi bi-trash me-1"></i> Delete</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($ads)): ?>
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<i class="bi bi-images fs-1 d-block mb-3"></i>
No advertisement images found. Click "Add Image" to get started.
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($ads_pagination); ?>
<?php render_pagination_controls($promos_pagination); ?>
</div>
</div>
</div>
<!-- Promo Modal -->
<?php if (has_permission('ads')): ?>
<div class="modal fade" id="promoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title fw-bold" id="promoModalTitle">Add New Item</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="promoForm" enctype="multipart/form-data">
<div class="modal-body p-4">
<input type="hidden" name="action" id="promoAction" value="add_promo">
<input type="hidden" name="id" id="promoId">
<div class="mb-4 text-center" id="promoImagePreviewContainer" style="display: none;">
<img src="" id="promoImagePreview" class="img-fluid rounded shadow-sm border" style="max-height: 200px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">TITLE / CAPTION</label>
<input type="text" name="title" id="promoTitle" class="form-control" placeholder="e.g. Special Offer">
</div>
<div class="row g-3 mb-3">
<div class="col-6">
<label class="form-label small fw-bold text-muted">SORT ORDER</label>
<input type="number" name="sort_order" id="promoSortOrder" class="form-control" value="0">
</div>
<div class="col-6">
<label class="form-label small fw-bold text-muted">DISPLAY LAYOUT</label>
<select name="display_layout" id="promoDisplayLayout" class="form-select">
<option value="both">Both Layouts</option>
<option value="split">Split Only</option>
<option value="fullscreen">Fullscreen</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">IMAGE FILE</label>
<input type="file" name="image" id="promoImageFile" class="form-control" accept="image/*">
<small class="text-muted mt-1 d-block" id="promoImageHint">Required for new images.</small>
</div>
<div class="mb-0 form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="promoIsActive" value="1" checked>
<label class="form-check-label fw-medium text-dark" for="promoIsActive">Active (Show in Slider)</label>
</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 shadow-sm">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
<script>
function preparePromoAddForm() {
document.getElementById('promoModalTitle').innerText = 'Add New Item';
document.getElementById('promoAction').value = 'add_promo';
document.getElementById('promoForm').reset();
document.getElementById('promoId').value = '';
document.getElementById('promoImagePreviewContainer').style.display = 'none';
document.getElementById('promoImageHint').innerText = 'Required for new images.';
document.getElementById('promoImageFile').required = true;
}
function preparePromoEditForm(data) {
if (!data) return;
document.getElementById('promoModalTitle').innerText = 'Edit Item';
document.getElementById('promoAction').value = 'edit_promo';
document.getElementById('promoId').value = data.id;
document.getElementById('promoTitle').value = data.title || '';
document.getElementById('promoSortOrder').value = data.sort_order || 0;
document.getElementById('promoDisplayLayout').value = data.display_layout || 'both';
document.getElementById('promoIsActive').checked = data.is_active == 1;
document.getElementById('promoImageHint').innerText = 'Leave empty to keep current image.';
document.getElementById('promoImageFile').required = false;
if (data.image_path) {
const preview = document.getElementById('promoImagePreview');
preview.src = '../' + data.image_path;
document.getElementById('promoImagePreviewContainer').style.display = 'block';
} else {
document.getElementById('promoImagePreviewContainer').style.display = 'none';
}
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -1,74 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = $_GET['id'] ?? null;
if (!$id) {
header('Location: areas.php');
exit;
}
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'] ?? '';
$outlet_id = $_POST['outlet_id'] ?? '';
if ($name && $outlet_id) {
$stmt = $pdo->prepare("UPDATE areas SET name = ?, outlet_id = ? WHERE id = ?");
$stmt->execute([$name, $outlet_id, $id]);
header('Location: areas.php');
exit;
}
}
// Fetch Area Details
$stmt = $pdo->prepare("SELECT * FROM areas WHERE id = ?");
$stmt->execute([$id]);
$area = $stmt->fetch();
if (!$area) {
die("Area not found.");
}
// Fetch Outlets for Dropdown
$outlets = $pdo->query("SELECT id, name FROM outlets ORDER BY name ASC")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Edit Area</h2>
<a href="areas.php" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($area['name']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Outlet</label>
<select name="outlet_id" class="form-select" required>
<option value="">Select Outlet</option>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= $outlet['id'] == $area['outlet_id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($outlet['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="d-flex justify-content-end gap-2">
<a href="areas.php" class="btn btn-light">Cancel</a>
<button type="submit" class="btn btn-primary">Update Area</button>
</div>
</form>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -6,46 +6,68 @@ $pdo = db();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_area') {
if (!has_permission('areas_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add areas.</div>';
// Handle Add/Edit Area
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$name = trim($_POST['name']);
$outlet_id = (int)$_POST['outlet_id'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($name)) {
$message = '<div class="alert alert-danger">Area name is required.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO areas (outlet_id, name) VALUES (?, ?)");
$stmt->execute([$_POST['outlet_id'], $_POST['name']]);
header("Location: areas.php");
exit;
try {
if ($action === 'edit_area' && $id) {
if (!has_permission('areas_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE areas SET name = ?, outlet_id = ? WHERE id = ?");
$stmt->execute([$name, $outlet_id, $id]);
$message = '<div class="alert alert-success">Area updated successfully!</div>';
}
} elseif ($action === 'add_area') {
if (!has_permission('areas_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO areas (name, outlet_id) VALUES (?, ?)");
$stmt->execute([$name, $outlet_id]);
$message = '<div class="alert alert-success">Area created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('areas_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete areas.</div>';
} else {
$pdo->prepare("DELETE FROM areas WHERE id = ?")->execute([$_GET['delete']]);
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM areas WHERE id = ?")->execute([$id]);
header("Location: areas.php");
exit;
}
}
// Fetch areas with outlet names
$query = "SELECT areas.*, outlets.name as outlet_name
FROM areas
LEFT JOIN outlets ON areas.outlet_id = outlets.id
ORDER BY areas.id DESC";
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name ASC")->fetchAll();
$query = "SELECT a.*, o.name as outlet_name
FROM areas a
LEFT JOIN outlets o ON a.outlet_id = o.id
ORDER BY a.id DESC";
$areas_pagination = paginate_query($pdo, $query);
$areas = $areas_pagination['data'];
// Fetch outlets for dropdown
$outlets = $pdo->query("SELECT id, name FROM outlets ORDER BY name ASC")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Areas</h2>
<?php if (has_permission('areas_add')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addAreaModal">
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-plus-lg"></i> Add Area
</button>
<?php endif; ?>
@ -66,7 +88,7 @@ include 'includes/header.php';
<th class="ps-4">ID</th>
<th>Name</th>
<th>Outlet</th>
<th>Actions</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
@ -74,21 +96,22 @@ include 'includes/header.php';
<tr>
<td class="ps-4 fw-medium">#<?= $area['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($area['name']) ?></td>
<td><span class="badge bg-info text-dark"><?= htmlspecialchars($area['outlet_name'] ?? 'N/A') ?></span></td>
<td>
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars($area['outlet_name'] ?: 'None') ?></span></td>
<td class="text-end pe-4">
<?php if (has_permission('areas_add')): ?>
<a href="area_edit.php?id=<?= $area['id'] ?>" class="btn btn-sm btn-outline-primary me-1" title="Edit"><i class="bi bi-pencil"></i></a>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= htmlspecialchars(json_encode($area), ENT_QUOTES, "UTF-8") ?>)' title="Edit"><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('areas_del')): ?>
<a href="?delete=<?= $area['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this area?')" title="Delete"><i class="bi bi-trash"></i></a>
<a href="?delete=<?= $area['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this area and all its tables?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($areas)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted">No areas found. Add one to get started.</td>
<td colspan="4" class="text-center py-4 text-muted">No areas found.</td>
</tr>
<?php endif; ?>
</tbody>
@ -101,25 +124,26 @@ include 'includes/header.php';
</div>
</div>
<!-- Add Area Modal -->
<!-- Area Modal -->
<?php if (has_permission('areas_add')): ?>
<div class="modal fade" id="addAreaModal" tabindex="-1">
<div class="modal fade" id="areaModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Area</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="areaModalTitle">Add New Area</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST">
<form method="POST" id="areaForm">
<div class="modal-body">
<input type="hidden" name="action" value="add_area">
<input type="hidden" name="action" id="areaAction" value="add_area">
<input type="hidden" name="id" id="areaId">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" placeholder="e.g. Main Hall, Patio" required>
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" name="name" id="areaName" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Outlet</label>
<select name="outlet_id" class="form-select" required>
<label class="form-label">Outlet <span class="text-danger">*</span></label>
<select name="outlet_id" id="areaOutletId" class="form-select" required>
<option value="">Select Outlet</option>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>"><?= htmlspecialchars($outlet['name']) ?></option>
@ -135,6 +159,39 @@ include 'includes/header.php';
</div>
</div>
</div>
<script>
function getAreaModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('areaModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function openAddModal() {
const modal = getAreaModal();
if (!modal) return;
document.getElementById('areaModalTitle').innerText = 'Add New Area';
document.getElementById('areaAction').value = 'add_area';
document.getElementById('areaForm').reset();
document.getElementById('areaId').value = '';
modal.show();
}
function openEditModal(area) {
if (!area) return;
const modal = getAreaModal();
if (!modal) return;
document.getElementById('areaModalTitle').innerText = 'Edit Area';
document.getElementById('areaAction').value = 'edit_area';
document.getElementById('areaId').value = area.id;
document.getElementById('areaName').value = area.name || '';
document.getElementById('areaOutletId').value = area.outlet_id || '';
modal.show();
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

215
admin/backup.php Normal file
View File

@ -0,0 +1,215 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("settings_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$backupDir = __DIR__ . '/../storage/backups/';
if (!is_dir($backupDir)) {
mkdir($backupDir, 0777, true);
}
$message = '';
// Retention policy: keep 5 copies
function enforceRetention($dir) {
$files = glob($dir . '/*.sql');
if (count($files) > 5) {
usort($files, function($a, $b) {
return filemtime($a) - filemtime($b);
});
while (count($files) > 5) {
$oldest = array_shift($files);
unlink($oldest);
}
}
}
// Handle actions
$action = $_GET['action'] ?? '';
if ($action === 'backup') {
$filename = 'backup_' . date('Y-m-d_H-i-s') . '.sql';
$path = $backupDir . $filename;
$command = sprintf(
'mysqldump -h %s -u %s -p%s %s > %s',
escapeshellarg(DB_HOST),
escapeshellarg(DB_USER),
escapeshellarg(DB_PASS),
escapeshellarg(DB_NAME),
escapeshellarg($path)
);
exec($command, $output, $returnVar);
if ($returnVar === 0) {
enforceRetention($backupDir);
$message = '<div class="alert alert-success">Backup created successfully: ' . $filename . '</div>';
} else {
$message = '<div class="alert alert-danger">Error creating backup.</div>';
}
} elseif ($action === 'download' && isset($_GET['file'])) {
$file = basename($_GET['file']);
$path = $backupDir . $file;
if (file_exists($path)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $file . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
}
} elseif ($action === 'delete' && isset($_GET['file'])) {
$file = basename($_GET['file']);
$path = $backupDir . $file;
if (file_exists($path)) {
unlink($path);
$message = '<div class="alert alert-success">Backup deleted.</div>';
}
} elseif ($action === 'restore' && isset($_GET['file'])) {
$file = basename($_GET['file']);
$path = $backupDir . $file;
if (file_exists($path)) {
$command = sprintf(
'mysql -h %s -u %s -p%s %s < %s',
escapeshellarg(DB_HOST),
escapeshellarg(DB_USER),
escapeshellarg(DB_PASS),
escapeshellarg(DB_NAME),
escapeshellarg($path)
);
exec($command, $output, $returnVar);
if ($returnVar === 0) {
$message = '<div class="alert alert-success">Database restored successfully from ' . $file . '</div>';
} else {
$message = '<div class="alert alert-danger">Error restoring database.</div>';
}
}
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['toggle_auto'])) {
$status = $_POST['auto_backup_enabled'] ? 1 : 0;
$stmt = $pdo->prepare("UPDATE company_settings SET auto_backup_enabled = ?, updated_at = NOW() LIMIT 1");
$stmt->execute([$status]);
$message = '<div class="alert alert-success">Auto backup settings updated.</div>';
}
$settings = get_company_settings();
$backups = glob($backupDir . '*.sql');
usort($backups, function($a, $b) {
return filemtime($b) - filemtime($a);
});
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Backup & Restore</h2>
<a href="?action=backup" class="btn btn-primary">
<i class="bi bi-cloud-arrow-up"></i> Create Manual Backup
</a>
</div>
<?= $message ?>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h5 class="card-title fw-bold mb-3">Settings</h5>
<form method="POST">
<input type="hidden" name="toggle_auto" value="1">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="auto_backup_enabled" id="autoBackupSwitch" <?= ($settings['auto_backup_enabled'] ?? 0) ? 'checked' : '' ?> onchange="this.form.submit()">
<label class="form-check-label" for="autoBackupSwitch">Enable Auto Backup (Daily)</label>
</div>
<p class="text-muted small">
Auto backup runs once a day when you access the admin panel.
It keeps exactly 5 latest copies.
</p>
<?php if ($settings['last_auto_backup']): ?>
<div class="alert alert-info py-2 small mb-0">
Last auto backup: <?= date('Y-m-d H:i', strtotime($settings['last_auto_backup'])) ?>
</div>
<?php endif; ?>
</form>
</div>
</div>
</div>
<div class="col-md-8 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5 class="card-title fw-bold mb-3">Available Backups (Max 5)</h5>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Filename</th>
<th>Date</th>
<th>Size</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($backups)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted">No backups found.</td>
</tr>
<?php else: ?>
<?php foreach ($backups as $path): ?>
<?php
$file = basename($path);
$size = round(filesize($path) / 1024, 2) . ' KB';
$date = date('Y-m-d H:i:s', filemtime($path));
?>
<tr>
<td><code class="text-primary"><?= $file ?></code></td>
<td><?= $date ?></td>
<td><?= $size ?></td>
<td class="text-end">
<div class="btn-group gap-1">
<a href="?action=download&file=<?= urlencode($file) ?>" class="btn btn-sm btn-outline-secondary rounded" title="Download">
<i class="bi bi-download"></i>
</a>
<button onclick="confirmRestore('<?= $file ?>')" class="btn btn-sm btn-outline-warning rounded" title="Restore">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<a href="?action=delete&file=<?= urlencode($file) ?>" class="btn btn-sm btn-outline-danger rounded" title="Delete" onclick="return confirm('Are you sure?')">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
function confirmRestore(file) {
Swal.fire({
title: 'Restore Database?',
text: "This will overwrite your current database with the backup from " + file + ". This action cannot be undone!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ffc107',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Yes, Restore it!',
cancelButtonText: 'Cancel'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = '?action=restore&file=' + encodeURIComponent(file);
}
});
}
</script>
<?php include 'includes/footer.php'; ?>

View File

@ -1,15 +1,68 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("categories_view");
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('categories_view');
$message = '';
// Handle Add/Edit Category
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$name = $_POST['name'];
$description = $_POST['description'];
$image_url = null;
if ($id) {
$stmt = $pdo->prepare("SELECT image_url FROM categories WHERE id = ?");
$stmt->execute([$id]);
$image_url = $stmt->fetchColumn();
} else {
$image_url = 'https://placehold.co/400x300?text=' . urlencode($name);
}
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/categories/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$fileName = uniqid('cat_') . '.' . $file_ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $fileName)) {
$image_url = 'assets/images/categories/' . $fileName;
}
}
}
try {
if ($action === 'edit_category' && $id) {
if (!has_permission('categories_edit') && !has_permission('categories_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit categories.</div>';
} else {
$stmt = $pdo->prepare("UPDATE categories SET name = ?, description = ?, image_url = ? WHERE id = ?");
$stmt->execute([$name, $description, $image_url, $id]);
$message = '<div class="alert alert-success">Category updated successfully!</div>';
}
} elseif ($action === 'add_category') {
if (!has_permission('categories_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add categories.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO categories (name, description, image_url) VALUES (?, ?, ?)");
$stmt->execute([$name, $description, $image_url]);
$message = '<div class="alert alert-success">Category created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('categories_del')) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to delete categories.</div>';
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete categories.</div>';
} else {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM categories WHERE id = ?")->execute([$id]);
@ -18,7 +71,7 @@ if (isset($_GET['delete'])) {
}
}
$query = "SELECT * FROM categories ORDER BY sort_order ASC, name ASC";
$query = "SELECT * FROM categories ORDER BY name ASC";
$categories_pagination = paginate_query($pdo, $query);
$categories = $categories_pagination['data'];
@ -26,75 +79,141 @@ include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Categories</h2>
<div>
<h2 class="fw-bold mb-1">Product Categories</h2>
<p class="text-muted mb-0">Organize your menu and inventory</p>
</div>
<?php if (has_permission('categories_add')): ?>
<a href="category_edit.php" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Add Category
</a>
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#categoryModal" onclick="prepareAddForm()" style="border-radius: 12px;">
<i class="bi bi-plus-lg me-1"></i> Add Category
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($categories_pagination); ?>
<?php if (empty($categories)): ?>
<div class="text-center py-5 bg-white rounded-4 shadow-sm">
<i class="bi bi-tags display-1 text-muted opacity-25 mb-3 d-block"></i>
<h4 class="text-dark">No categories found</h4>
<p class="text-muted">Start by adding your first category.</p>
</div>
<?php else: ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<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 style="width: 80px;">Image</th>
<th>Name</th>
<th>Sort Order</th>
<th class="ps-4">Category</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>
<?php if (!empty($cat['image_url'])): ?>
<img src="<?= htmlspecialchars(strpos($cat['image_url'], 'http') === 0 ? $cat['image_url'] : '../' . $cat['image_url']) ?>"
class="rounded object-fit-cover"
width="50" height="50"
alt="<?= htmlspecialchars($cat['name']) ?>">
<?php else: ?>
<div class="rounded bg-light d-flex align-items-center justify-content-center text-muted border" style="width: 50px; height: 50px;">
<i class="bi bi-image"></i>
<td class="ps-4">
<div class="d-flex align-items-center py-2">
<img src="<?= htmlspecialchars(strpos($cat['image_url'], 'http') === 0 ? $cat['image_url'] : '../' . $cat['image_url']) ?>" alt="" class="rounded-3 me-3 border shadow-sm" style="width: 50px; height: 50px; object-fit: cover;">
<div class="fw-bold text-dark fs-6"><?= htmlspecialchars($cat['name']) ?></div>
</div>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($cat['name']) ?></td>
<td><?= $cat['sort_order'] ?></td>
<td>
<div class="text-muted small text-truncate" style="max-width: 300px;"><?= htmlspecialchars($cat['description'] ?? 'No description') ?></div>
</td>
<td class="text-end pe-4">
<?php if (has_permission('categories_add')): ?>
<a href="category_edit.php?id=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-primary me-1"><i class="bi bi-pencil"></i></a>
<div class="d-inline-flex gap-2">
<?php if (has_permission('categories_edit') || has_permission('categories_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3"
data-bs-toggle="modal" data-bs-target="#categoryModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($cat), ENT_QUOTES, "UTF-8") ?>)'>Edit</button>
<?php endif; ?>
<?php if (has_permission('categories_del')): ?>
<a href="?delete=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure? This might break products linked to this category.')"><i class="bi bi-trash"></i></a>
<a href="?delete=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Delete category? Ensure no products are linked.')">Delete</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($categories)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">No categories found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($categories_pagination); ?>
</div>
</div>
<?php endif; ?>
<!-- Category Modal -->
<?php if (has_permission('categories_add') || has_permission('categories_edit')): ?>
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="categoryModalTitle">Add New Category</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="categoryForm" enctype="multipart/form-data">
<div class="modal-body p-4">
<input type="hidden" name="action" id="categoryAction" value="add_category">
<input type="hidden" name="id" id="categoryId">
<div class="mb-3">
<label class="form-label small fw-bold text-muted">CATEGORY NAME <span class="text-danger">*</span></label>
<input type="text" name="name" id="categoryName" class="form-control rounded-3 border-0 bg-light" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">DESCRIPTION</label>
<textarea name="description" id="categoryDescription" class="form-control rounded-3 border-0 bg-light" rows="3" placeholder="Optional category description..."></textarea>
</div>
<div class="mb-0">
<label class="form-label small fw-bold text-muted">IMAGE</label>
<div class="d-flex align-items-center gap-3 bg-light p-3 rounded-4 border border-dashed">
<img src="" id="categoryImagePreview" class="rounded-3 border shadow-sm" style="width: 60px; height: 60px; object-fit: cover; display: none;">
<div class="flex-grow-1">
<input type="file" name="image" class="form-control border-0 bg-transparent" accept="image/*">
</div>
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Save Category</button>
</div>
</form>
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('categoryModalTitle').innerText = 'Add New Category';
document.getElementById('categoryAction').value = 'add_category';
document.getElementById('categoryForm').reset();
document.getElementById('categoryId').value = '';
document.getElementById('categoryImagePreview').style.display = 'none';
}
function prepareEditForm(cat) {
if (!cat) return;
document.getElementById('categoryModalTitle').innerText = 'Edit Category: ' + cat.name;
document.getElementById('categoryAction').value = 'edit_category';
document.getElementById('categoryId').value = cat.id;
document.getElementById('categoryName').value = cat.name;
document.getElementById('categoryDescription').value = cat.description || '';
if (cat.image_url) {
const preview = document.getElementById('categoryImagePreview');
preview.src = cat.image_url.startsWith('http') ? cat.image_url : '../' + cat.image_url;
preview.style.display = 'block';
} else {
document.getElementById('categoryImagePreview').style.display = 'none';
}
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -1,150 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
$category = null;
$message = '';
$isEdit = false;
// If ID provided, fetch category for editing
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM categories WHERE id = ?");
$stmt->execute([$id]);
$category = $stmt->fetch();
if ($category) {
$isEdit = true;
} else {
// ID not found, redirect to list
header("Location: categories.php");
exit;
}
}
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name']);
$sort_order = (int)$_POST['sort_order'];
$image_url = $isEdit ? $category['image_url'] : null;
// Basic Validation
if (empty($name)) {
$message = '<div class="alert alert-danger">Category name is required.</div>';
} else {
// Image Upload Handling
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/categories/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$fileInfo = pathinfo($_FILES['image']['name']);
$fileExt = strtolower($fileInfo['extension']);
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($fileExt, $allowedExts)) {
$fileName = uniqid('cat_') . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
if (move_uploaded_file($_FILES['image']['tmp_name'], $targetFile)) {
// Remove old image if exists and not default placeholder (optional, strict cleanup)
if ($isEdit && !empty($category['image_url']) && file_exists(__DIR__ . '/../' . $category['image_url'])) {
// unlink(__DIR__ . '/../' . $category['image_url']); // Uncomment to auto-delete old images
}
$image_url = 'assets/images/categories/' . $fileName;
} else {
$message = '<div class="alert alert-danger">Failed to upload image. Check permissions.</div>';
}
} else {
$message = '<div class="alert alert-danger">Invalid file type. Allowed: jpg, png, gif, webp.</div>';
}
}
if (empty($message)) {
try {
if ($isEdit) {
$stmt = $pdo->prepare("UPDATE categories SET name = ?, sort_order = ?, image_url = ? WHERE id = ?");
$stmt->execute([$name, $sort_order, $image_url, $id]);
$message = '<div class="alert alert-success">Category updated successfully!</div>';
// Refresh data
$stmt = $pdo->prepare("SELECT * FROM categories WHERE id = ?");
$stmt->execute([$id]);
$category = $stmt->fetch();
} else {
$stmt = $pdo->prepare("INSERT INTO categories (name, sort_order, image_url) VALUES (?, ?, ?)");
$stmt->execute([$name, $sort_order, $image_url]);
header("Location: categories.php?success=created");
exit;
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
}
// Defaults for Add Mode or Error State
if (!$isEdit) {
$category = [
'name' => $_POST['name'] ?? '',
'sort_order' => $_POST['sort_order'] ?? 0,
'image_url' => ''
];
}
include 'includes/header.php';
?>
<div class="mb-4">
<a href="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" enctype="multipart/form-data">
<div class="row">
<div class="col-md-8">
<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">Sort Order</label>
<input type="number" name="sort_order" class="form-control" value="<?= htmlspecialchars($category['sort_order']) ?>">
<div class="form-text">Lower numbers appear first. Default is 0.</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Category Image</label>
<?php if (!empty($category['image_url'])): ?>
<div class="mb-2">
<img src="<?= htmlspecialchars(strpos($category['image_url'], 'http') === 0 ? $category['image_url'] : '../' . $category['image_url']) ?>"
class="img-fluid rounded border"
style="max-height: 200px; width: auto;"
alt="Category Image">
</div>
<?php else: ?>
<div class="mb-2 p-4 bg-light text-center border rounded text-muted">
<i class="bi bi-image fs-1"></i><br>No Image
</div>
<?php endif; ?>
<input type="file" name="image" class="form-control" accept="image/*">
<div class="form-text">Allowed: JPG, PNG, GIF, WEBP. Leave empty to keep current.</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-end gap-2">
<a href="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'; ?>

View File

@ -1,45 +1,44 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("customers_view");
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('customers_view');
$message = '';
// Handle Add/Edit Customer
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'add_customer') {
$action = $_POST['action'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$name = trim($_POST['name']);
$email = trim($_POST['email']);
$phone = trim($_POST['phone']);
$address = trim($_POST['address'] ?? '');
if (empty($name)) {
$message = '<div class="alert alert-danger">Customer name is required.</div>';
} else {
try {
if ($action === 'edit_customer' && $id) {
if (!has_permission('customers_edit') && !has_permission('customers_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit customers.</div>';
} else {
$stmt = $pdo->prepare("UPDATE customers SET name = ?, email = ?, phone = ?, address = ? WHERE id = ?");
$stmt->execute([$name, $email, $phone, $address, $id]);
$message = '<div class="alert alert-success">Customer updated successfully!</div>';
}
} elseif ($action === 'add_customer') {
if (!has_permission('customers_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add customers.</div>';
} else {
$name = $_POST['name'];
$email = $_POST['email'];
$phone = $_POST['phone'];
$address = $_POST['address'];
$stmt = $pdo->prepare("INSERT INTO customers (name, email, phone, address) VALUES (?, ?, ?, ?)");
if ($stmt->execute([$name, $email, $phone, $address])) {
$message = '<div class="alert alert-success">Customer added successfully!</div>';
} else {
$message = '<div class="alert alert-danger">Error adding customer.</div>';
$stmt->execute([$name, $email, $phone, $address]);
$message = '<div class="alert alert-success">Customer created successfully!</div>';
}
}
} elseif ($_POST['action'] === 'edit_customer') {
if (!has_permission('customers_add')) { // Use customers_add for editing as well
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit customers.</div>';
} else {
$id = $_POST['id'];
$name = $_POST['name'];
$email = $_POST['email'];
$phone = $_POST['phone'];
$address = $_POST['address'];
$stmt = $pdo->prepare("UPDATE customers SET name = ?, email = ?, phone = ?, address = ? WHERE id = ?");
if ($stmt->execute([$name, $email, $phone, $address, $id])) {
$message = '<div class="alert alert-success">Customer updated successfully!</div>';
} else {
$message = '<div class="alert alert-danger">Error updating customer.</div>';
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
@ -56,118 +55,155 @@ if (isset($_GET['delete'])) {
}
}
// Fetch Customers
$query = "SELECT * FROM customers ORDER BY id DESC";
$customers_pagination = paginate_query($pdo, $query);
$search = $_GET['search'] ?? '';
$params = [];
$query = "SELECT * FROM customers";
if ($search) {
$query .= " WHERE name LIKE ? OR phone LIKE ? OR email LIKE ?";
$params = ["%$search%", "%$search%", "%$search%"];
}
$query .= " ORDER BY id DESC";
$customers_pagination = paginate_query($pdo, $query, $params);
$customers = $customers_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Customers</h2>
<div>
<h2 class="fw-bold mb-1">Customer Relationship</h2>
<p class="text-muted mb-0">Manage your customer database and contact info</p>
</div>
<?php if (has_permission('customers_add')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#customerModal" onclick="openAddModal()">
<i class="bi bi-plus-lg"></i> Add Customer
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#customerModal" onclick="prepareAddForm()" style="border-radius: 12px;">
<i class="bi bi-person-plus me-1"></i> Add Customer
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($customers_pagination); ?>
<div class="card border-0 shadow-sm mb-4 rounded-4">
<div class="card-body p-4">
<form method="GET" class="row g-3 align-items-center">
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text bg-light border-0 text-muted"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-0 bg-light rounded-3" placeholder="Search by name, phone or email..." value="<?= htmlspecialchars($search) ?>" style="border-radius: 0 10px 10px 0;">
</div>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary px-4 w-100 rounded-pill fw-bold shadow-sm">Search Records</button>
</div>
</form>
</div>
</div>
<?php if (empty($customers)): ?>
<div class="text-center py-5 bg-white rounded-4 shadow-sm">
<i class="bi bi-people display-1 text-muted opacity-25 mb-3 d-block"></i>
<h4 class="text-dark">No customers found</h4>
<p class="text-muted">No results matching your search criteria.</p>
</div>
<?php else: ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Name</th>
<th>Email</th>
<th>Phone</th>
<th class="ps-4">Customer</th>
<th>Contact Info</th>
<th>Address</th>
<th class="text-center">Redemptions</th>
<th>Actions</th>
<th>Points</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $customer): ?>
<tr>
<td class="ps-4 fw-bold"><?= htmlspecialchars($customer['name']) ?></td>
<td><?= htmlspecialchars($customer['email']) ?></td>
<td><?= htmlspecialchars($customer['phone']) ?></td>
<td><?= htmlspecialchars(substr($customer['address'] ?? '', 0, 30)) ?>...</td>
<td class="text-center">
<span class="badge bg-info text-dark"><?= intval($customer['loyalty_redemptions_count'] ?? 0) ?></span>
<td class="ps-4">
<div class="d-flex align-items-center py-2">
<div class="bg-primary-subtle text-primary rounded-circle d-flex align-items-center justify-content-center fw-bold me-3" style="width: 42px; height: 42px;">
<?= strtoupper(substr($customer['name'], 0, 1)) ?>
</div>
<div class="fw-bold text-dark fs-6"><?= htmlspecialchars($customer['name']) ?></div>
</div>
</td>
<td>
<div class="btn-group">
<?php if (has_permission('customers_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#customerModal"
onclick="openEditModal(<?= htmlspecialchars(json_encode($customer)) ?>)"
title="Edit Customer"><i class="bi bi-pencil"></i></button>
<div class="small fw-bold text-dark mb-1"><i class="bi bi-phone me-1 text-muted"></i><?= htmlspecialchars($customer['phone'] ?: '-') ?></div>
<div class="small text-muted"><i class="bi bi-envelope me-1"></i><?= htmlspecialchars($customer['email'] ?: '-') ?></div>
</td>
<td>
<div class="small text-muted text-truncate" style="max-width: 200px;"><?= htmlspecialchars($customer['address'] ?: '-') ?></div>
</td>
<td>
<span class="badge bg-info-subtle text-info border border-info rounded-pill px-3"><?= number_format($customer['loyalty_points'] ?? 0) ?> pts</span>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<?php if (has_permission('customers_edit') || has_permission('customers_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3"
data-bs-toggle="modal" data-bs-target="#customerModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($customer), ENT_QUOTES, "UTF-8") ?>)'>Edit</button>
<?php endif; ?>
<?php if (has_permission('customers_del')): ?>
<a href="?delete=<?= $customer['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')" title="Delete"><i class="bi bi-trash"></i></a>
<a href="?delete=<?= $customer['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Delete customer? This will remove their loyalty history.')">Delete</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($customers)): ?>
<tr>
<td colspan="6" class="text-center py-4 text-muted">No customers found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($customers_pagination); ?>
</div>
</div>
</div>
<?php endif; ?>
<!-- Customer Modal -->
<?php if (has_permission('customers_add')): ?>
<div class="modal fade" id="customerModal" tabindex="-1">
<?php if (has_permission('customers_add') || has_permission('customers_edit')): ?>
<div class="modal fade" id="customerModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customerModalTitle">Add New Customer</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="customerModalTitle">Add New Customer</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="customerForm">
<div class="modal-body">
<div class="modal-body p-4">
<input type="hidden" name="action" id="customerAction" value="add_customer">
<input type="hidden" name="id" id="customerId">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" id="customerName" class="form-control" required>
<label class="form-label small fw-bold text-muted">FULL NAME <span class="text-danger">*</span></label>
<input type="text" name="name" id="customerName" class="form-control rounded-3 border-0 bg-light" required>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" id="customerEmail" class="form-control">
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">PHONE NUMBER</label>
<input type="text" name="phone" id="customerPhone" class="form-control rounded-3 border-0 bg-light">
</div>
<div class="mb-3">
<label class="form-label">Phone</label>
<input type="text" name="phone" id="customerPhone" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Address</label>
<textarea name="address" id="customerAddress" class="form-control" rows="3"></textarea>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">EMAIL ADDRESS</label>
<input type="email" name="email" id="customerEmail" class="form-control rounded-3 border-0 bg-light">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Customer</button>
<div class="mb-0">
<label class="form-label small fw-bold text-muted">ADDRESS</label>
<textarea name="address" id="customerAddress" class="form-control rounded-3 border-0 bg-light" rows="3" placeholder="Street, City, State..."></textarea>
</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 shadow-sm">Save Customer Profile</button>
</div>
</form>
</div>
@ -175,20 +211,21 @@ include 'includes/header.php';
</div>
<script>
function openAddModal() {
function prepareAddForm() {
document.getElementById('customerModalTitle').innerText = 'Add New Customer';
document.getElementById('customerAction').value = 'add_customer';
document.getElementById('customerForm').reset();
document.getElementById('customerId').value = '';
}
function openEditModal(customer) {
document.getElementById('customerModalTitle').innerText = 'Edit Customer';
function prepareEditForm(customer) {
if (!customer) return;
document.getElementById('customerModalTitle').innerText = 'Edit Customer Profile';
document.getElementById('customerAction').value = 'edit_customer';
document.getElementById('customerId').value = customer.id;
document.getElementById('customerName').value = customer.name;
document.getElementById('customerEmail').value = customer.email || '';
document.getElementById('customerName').value = customer.name || '';
document.getElementById('customerPhone').value = customer.phone || '';
document.getElementById('customerEmail').value = customer.email || '';
document.getElementById('customerAddress').value = customer.address || '';
}
</script>

View File

@ -6,26 +6,55 @@ $pdo = db();
$message = '';
// Handle Add/Edit Expense Category
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$name = trim($_POST['name']);
$description = trim($_POST['description']);
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($name)) {
$message = '<div class="alert alert-danger">Category name is required.</div>';
} else {
try {
if ($action === 'edit_expense_category' && $id) {
if (!has_permission('expense_categories_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE expense_categories SET name = ?, description = ? WHERE id = ?");
$stmt->execute([$name, $description, $id]);
$message = '<div class="alert alert-success">Expense category updated successfully!</div>';
}
} elseif ($action === 'add_expense_category') {
if (!has_permission('expense_categories_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO expense_categories (name, description) VALUES (?, ?)");
$stmt->execute([$name, $description]);
$message = '<div class="alert alert-success">Expense category created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('expense_categories_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete expense categories.</div>';
} else {
$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) {
$message = '<div class="alert alert-danger">Cannot delete category as it has linked expenses.</div>';
} else {
$pdo->prepare("DELETE FROM expense_categories WHERE id = ?")->execute([$id]);
$message = '<div class="alert alert-success">Category deleted successfully.</div>';
}
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'];
$expense_categories_pagination = paginate_query($pdo, $query);
$expense_categories = $expense_categories_pagination['data'];
include 'includes/header.php';
?>
@ -33,9 +62,9 @@ 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>
<?php if (has_permission('expense_categories_add')): ?>
<a href="expense_category_edit.php" class="btn btn-primary">
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-plus-lg"></i> Add Category
</a>
</button>
<?php endif; ?>
</div>
@ -43,8 +72,9 @@ include 'includes/header.php';
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($categories_pagination); ?>
<?php render_pagination_controls($expense_categories_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
@ -57,34 +87,101 @@ include 'includes/header.php';
</tr>
</thead>
<tbody>
<?php foreach ($categories as $cat): ?>
<?php foreach ($expense_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="fw-bold"><?= htmlspecialchars($cat['name']) ?></td>
<td><small class="text-muted"><?= htmlspecialchars($cat['description'] ?: '-') ?></small></td>
<td class="text-end pe-4">
<?php if (has_permission('expense_categories_add')): ?>
<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>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= htmlspecialchars(json_encode($cat), ENT_QUOTES, "UTF-8") ?>)' title="Edit"><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('expense_categories_del')): ?>
<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>
<a href="?delete=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this expense category?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($categories)): ?>
<?php if (empty($expense_categories)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted">No categories found.</td>
<td colspan="4" class="text-center py-4 text-muted">No expense categories found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($categories_pagination); ?>
<?php render_pagination_controls($expense_categories_pagination); ?>
</div>
</div>
</div>
<!-- Expense Category Modal -->
<?php if (has_permission('expense_categories_add')): ?>
<div class="modal fade" id="expenseCategoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="expenseCategoryModalTitle">Add New Category</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="expenseCategoryForm">
<div class="modal-body">
<input type="hidden" name="action" id="expenseCategoryAction" value="add_expense_category">
<input type="hidden" name="id" id="expenseCategoryId">
<div class="mb-3">
<label class="form-label">Category Name <span class="text-danger">*</span></label>
<input type="text" name="name" id="expenseCategoryName" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" id="expenseCategoryDescription" class="form-control" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Category</button>
</div>
</form>
</div>
</div>
</div>
<script>
function getExpenseCategoryModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('expenseCategoryModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function openAddModal() {
const modal = getExpenseCategoryModal();
if (!modal) return;
document.getElementById('expenseCategoryModalTitle').innerText = 'Add New Category';
document.getElementById('expenseCategoryAction').value = 'add_expense_category';
document.getElementById('expenseCategoryForm').reset();
document.getElementById('expenseCategoryId').value = '';
modal.show();
}
function openEditModal(cat) {
if (!cat) return;
const modal = getExpenseCategoryModal();
if (!modal) return;
document.getElementById('expenseCategoryModalTitle').innerText = 'Edit Category';
document.getElementById('expenseCategoryAction').value = 'edit_expense_category';
document.getElementById('expenseCategoryId').value = cat.id;
document.getElementById('expenseCategoryName').value = cat.name || '';
document.getElementById('expenseCategoryDescription').value = cat.description || '';
modal.show();
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -1,88 +0,0 @@
<?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'; ?>

View File

@ -16,6 +16,11 @@ if (function_exists('require_login')) {
require_login();
}
// Trigger auto backup if needed
if (function_exists('trigger_auto_backup')) {
trigger_auto_backup();
}
$currentUser = function_exists('get_logged_user') ? get_logged_user() : null;
$userName = $currentUser['full_name'] ?? ($currentUser['username'] ?? 'Admin');
$userGroup = $currentUser['group_name'] ?? 'System';
@ -48,7 +53,7 @@ function getGroupToggleClass($pages) {
// Permission helper for sidebar
function can_view($module) {
if (!function_exists('has_permission')) return true;
return has_permission($module . '_view');
return has_permission($module . '_view') || has_permission($module);
}
?>
<!doctype html>
@ -499,7 +504,7 @@ function can_view($module) {
<?php endif; ?>
<?php
$settingsGroup = ['payment_types.php', 'payment_type_edit.php', 'integrations.php', 'company.php'];
$settingsGroup = ['payment_types.php', 'payment_type_edit.php', 'integrations.php', 'company.php', 'backup.php'];
$canViewSettingsGroup = can_view('payment_types') || can_view('settings');
if ($canViewSettingsGroup):
?>
@ -529,6 +534,11 @@ function can_view($module) {
<i class="bi bi-building me-2"></i> Company
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('backup.php') ?>" href="backup.php">
<i class="bi bi-cloud-arrow-down me-2"></i> Backup & Restore
</a>
</li>
<?php endif; ?>
<li class="nav-item border-top mt-2 pt-2">

View File

@ -45,6 +45,8 @@ $subtotal = 0;
foreach ($items as $item) {
$subtotal += $item['unit_price'] * $item['quantity'];
}
$vat_or_discount = (float)$order['discount'];
?>
<div class="d-flex justify-content-between align-items-center mb-4">
@ -105,18 +107,18 @@ foreach ($items as $item) {
<tr>
<td colspan="3" class="text-end py-3 ps-4">
<div class="text-muted mb-1">Subtotal</div>
<?php if ($order['discount'] > 0): ?>
<div class="text-muted mb-1">Discount</div>
<?php if ($vat_or_discount != 0): ?>
<div class="text-muted mb-1"><?= $vat_or_discount > 0 ? 'VAT' : 'Discount' ?></div>
<?php endif; ?>
<div class="text-muted mb-1">VAT / Tax</div>
<h5 class="fw-bold mb-0 text-dark">Total Amount</h5>
</td>
<td class="text-end py-3 pe-4">
<div class="mb-1"><?= format_currency($subtotal) ?></div>
<?php if ($order['discount'] > 0): ?>
<div class="mb-1 text-danger">-<?= format_currency($order['discount']) ?></div>
<?php if ($vat_or_discount != 0): ?>
<div class="mb-1 <?= $vat_or_discount < 0 ? 'text-danger' : 'text-primary' ?>">
<?= ($vat_or_discount > 0 ? '+' : '') . format_currency($vat_or_discount) ?>
</div>
<?php endif; ?>
<div class="mb-1"><?= format_currency(0) ?></div>
<h5 class="fw-bold mb-0 text-primary"><?= format_currency($order['total_amount']) ?></h5>
</td>
</tr>
@ -234,7 +236,7 @@ const BASE_URL = '<?= get_base_url() ?>';
function formatCurrency(amount) {
const symbol = COMPANY_SETTINGS.currency_symbol || '$';
const decimals = parseInt(COMPANY_SETTINGS.currency_decimals || 2);
return symbol + parseFloat(amount).toFixed(decimals);
return symbol + parseFloat(Math.abs(amount)).toFixed(decimals);
}
function printThermalReceipt() {
@ -250,12 +252,12 @@ function printThermalReceipt() {
];
}, $items)) ?>,
total: <?= (float)$order['total_amount'] ?>,
discount: <?= (float)$order['discount'] ?>,
vat: <?= (float)$order['discount'] ?>,
orderType: '<?= $order['order_type'] ?>',
tableNumber: '<?= $order['table_number'] ?>',
date: '<?= date('M d, Y H:i', strtotime($order['created_at'])) ?>',
paymentMethod: '<?= $order['payment_type_name'] ?? 'Unpaid' ?>',
loyaltyRedeemed: <?= ($order['discount'] >= $subtotal && $order['discount'] > 0) ? 'true' : 'false' ?>
loyaltyRedeemed: <?= ($order['discount'] < 0) ? 'true' : 'false' ?>
};
const width = 400;
@ -275,8 +277,7 @@ function printThermalReceipt() {
'ITEM': 'الصنف',
'TOTAL': 'المجموع',
'Subtotal': 'المجموع الفرعي',
'Discount': 'الخصم',
'Tax Included': 'شامل الضريبة',
'VAT': 'ضريبة القيمة المضافة',
'THANK YOU FOR YOUR VISIT!': 'شكراً لزيارتكم!',
'Please come again.': 'يرجى زيارتنا مرة أخرى.',
'Customer Details': 'تفاصيل العميل',
@ -326,10 +327,7 @@ function printThermalReceipt() {
const loyaltyHtml = data.loyaltyRedeemed ? `<div style="color: #d63384; font-weight: bold; margin: 5px 0; text-align: center;">* Loyalty Reward Applied *</div>` : '';
const vatRate = parseFloat(COMPANY_SETTINGS.vat_rate) || 0;
const subtotal = data.total + data.discount;
const vatAmount = vatRate > 0 ? (data.total * (vatRate / (100 + vatRate))) : 0;
const subtotal = data.total - data.vat;
const logoHtml = COMPANY_SETTINGS.logo_url ? `<img src="${BASE_URL}${COMPANY_SETTINGS.logo_url}" style="max-height: 80px; max-width: 150px; margin-bottom: 10px;">` : '';
const html = `
@ -425,15 +423,10 @@ function printThermalReceipt() {
<td>Subtotal / ${tr['Subtotal']}</td>
<td style="text-align: right">${formatCurrency(subtotal)}</td>
</tr>
${data.discount > 0 ? `
${Math.abs(data.vat) > 0 ? `
<tr>
<td>Discount / ${tr['Discount']}</td>
<td style="text-align: right">-${formatCurrency(data.discount)}</td>
</tr>` : ''}
${vatRate > 0 ? `
<tr>
<td style="font-size: 11px;">Tax Incl. (${vatRate}%) / ${tr['Tax Included']}</td>
<td style="text-align: right">${formatCurrency(vatAmount)}</td>
<td>${data.vat < 0 ? 'Discount' : 'VAT'} / ${tr['VAT']}</td>
<td style="text-align: right">${data.vat < 0 ? '-' : '+'}${formatCurrency(Math.abs(data.vat))}</td>
</tr>` : ''}
<tr style="font-weight: bold; font-size: 18px;">
<td style="padding-top: 10px;">TOTAL / ${tr['TOTAL']}</td>
@ -450,7 +443,7 @@ function printThermalReceipt() {
<div>Please come again.</div>
<div class="rtl">${tr['Please come again.']}</div>
${COMPANY_SETTINGS.email ? `<div style="margin-top: 5px; font-size: 10px;">${COMPANY_SETTINGS.email}</div>` : ''}
<div style="margin-top: 15px; font-size: 10px; color: #888;">Powered by Flatlogic POS</div>
<div style="margin-top: 15px; font-size: 10px; color: #888;">Powered by Abidarcafe</div>
</div>
<script>

View File

@ -1,75 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if (!isset($_GET['id'])) {
header("Location: outlets.php");
exit;
}
$id = $_GET['id'];
$message = '';
// Fetch Outlet
$stmt = $pdo->prepare("SELECT * FROM outlets WHERE id = ?");
$stmt->execute([$id]);
$outlet = $stmt->fetch();
if (!$outlet) {
header("Location: outlets.php");
exit;
}
// Handle Update
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'];
$address = $_POST['address'];
if (empty($name)) {
$message = '<div class="alert alert-danger">Name is required.</div>';
} else {
$stmt = $pdo->prepare("UPDATE outlets SET name = ?, address = ? WHERE id = ?");
if ($stmt->execute([$name, $address, $id])) {
$message = '<div class="alert alert-success">Outlet updated successfully!</div>';
// Refresh outlet data
$stmt = $pdo->prepare("SELECT * FROM outlets WHERE id = ?");
$stmt->execute([$id]);
$outlet = $stmt->fetch();
} else {
$message = '<div class="alert alert-danger">Error updating outlet.</div>';
}
}
}
include 'includes/header.php';
?>
<div class="mb-4">
<a href="outlets.php" class="text-decoration-none text-muted mb-2 d-inline-block"><i class="bi bi-arrow-left"></i> Back to Outlets</a>
<h2 class="fw-bold mb-0">Edit Outlet: <?= htmlspecialchars($outlet['name']) ?></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">Name</label>
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($outlet['name']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Address</label>
<textarea name="address" class="form-control" rows="3"><?= htmlspecialchars($outlet['address']) ?></textarea>
</div>
<hr>
<div class="d-flex justify-content-end gap-2">
<a href="outlets.php" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -6,22 +6,47 @@ $pdo = db();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_outlet') {
// Handle Add/Edit Outlet
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$name = trim($_POST['name']);
$address = trim($_POST['address']);
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($name)) {
$message = '<div class="alert alert-danger">Outlet name is required.</div>';
} else {
try {
if ($action === 'edit_outlet' && $id) {
if (!has_permission('outlets_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add outlets.</div>';
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE outlets SET name = ?, address = ? WHERE id = ?");
$stmt->execute([$name, $address, $id]);
$message = '<div class="alert alert-success">Outlet updated successfully!</div>';
}
} elseif ($action === 'add_outlet') {
if (!has_permission('outlets_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO outlets (name, address) VALUES (?, ?)");
$stmt->execute([$_POST['name'], $_POST['address']]);
header("Location: outlets.php");
exit;
$stmt->execute([$name, $address]);
$message = '<div class="alert alert-success">Outlet created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('outlets_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete outlets.</div>';
} else {
$pdo->prepare("DELETE FROM outlets WHERE id = ?")->execute([$_GET['delete']]);
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM outlets WHERE id = ?")->execute([$id]);
header("Location: outlets.php");
exit;
}
@ -37,7 +62,7 @@ include 'includes/header.php';
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Outlets</h2>
<?php if (has_permission('outlets_add')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addOutletModal">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#outletModal" onclick="prepareAddForm()">
<i class="bi bi-plus-lg"></i> Add Outlet
</button>
<?php endif; ?>
@ -58,7 +83,7 @@ include 'includes/header.php';
<th class="ps-4">ID</th>
<th>Name</th>
<th>Address</th>
<th>Actions</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
@ -67,9 +92,11 @@ include 'includes/header.php';
<td class="ps-4 fw-medium">#<?= $outlet['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($outlet['name']) ?></td>
<td><small class="text-muted"><?= htmlspecialchars($outlet['address']) ?></small></td>
<td>
<td class="text-end pe-4">
<?php if (has_permission('outlets_add')): ?>
<a href="outlet_edit.php?id=<?= $outlet['id'] ?>" class="btn btn-sm btn-outline-secondary me-1"><i class="bi bi-pencil"></i></a>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
data-bs-toggle="modal" data-bs-target="#outletModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($outlet), ENT_QUOTES, "UTF-8") ?>)'><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('outlets_del')): ?>
@ -78,6 +105,11 @@ include 'includes/header.php';
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($outlets)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted">No outlets found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
@ -88,25 +120,26 @@ include 'includes/header.php';
</div>
</div>
<!-- Add Outlet Modal -->
<!-- Outlet Modal -->
<?php if (has_permission('outlets_add')): ?>
<div class="modal fade" id="addOutletModal" tabindex="-1">
<div class="modal fade" id="outletModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Outlet</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="outletModalTitle">Add New Outlet</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST">
<form method="POST" id="outletForm">
<div class="modal-body">
<input type="hidden" name="action" value="add_outlet">
<input type="hidden" name="action" id="outletAction" value="add_outlet">
<input type="hidden" name="id" id="outletId">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" required>
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" name="name" id="outletName" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Address</label>
<textarea name="address" class="form-control" rows="2"></textarea>
<textarea name="address" id="outletAddress" class="form-control" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
@ -117,6 +150,24 @@ include 'includes/header.php';
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('outletModalTitle').innerText = 'Add New Outlet';
document.getElementById('outletAction').value = 'add_outlet';
document.getElementById('outletForm').reset();
document.getElementById('outletId').value = '';
}
function prepareEditForm(outlet) {
if (!outlet) return;
document.getElementById('outletModalTitle').innerText = 'Edit Outlet';
document.getElementById('outletAction').value = 'edit_outlet';
document.getElementById('outletId').value = outlet.id;
document.getElementById('outletName').value = outlet.name || '';
document.getElementById('outletAddress').value = outlet.address || '';
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -1,85 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = $_GET['id'] ?? null;
$pt = [
'name' => '',
'type' => 'cash',
'api_provider' => '',
'is_active' => 1
];
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM payment_types WHERE id = ?");
$stmt->execute([$id]);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existing) {
$pt = $existing;
} else {
header("Location: payment_types.php");
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'];
$type = $_POST['type'];
$api_provider = $_POST['api_provider'] ?: null;
$is_active = isset($_POST['is_active']) ? 1 : 0;
if ($id) {
$stmt = $pdo->prepare("UPDATE payment_types SET name = ?, type = ?, api_provider = ?, is_active = ? WHERE id = ?");
$stmt->execute([$name, $type, $api_provider, $is_active, $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO payment_types (name, type, api_provider, is_active) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $type, $api_provider, $is_active]);
}
header("Location: payment_types.php?msg=saved");
exit;
}
require_once __DIR__ . '/includes/header.php';
?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0 text-gray-800"><?= $id ? 'Edit' : 'Add' ?> Payment Type</h2>
</div>
<div class="card shadow mb-4">
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($pt['name']) ?>" required>
</div>
<div class="mb-3">
<label for="type" class="form-label">Type</label>
<select class="form-select" id="type" name="type" required>
<option value="cash" <?= $pt['type'] == 'cash' ? 'selected' : '' ?>>Cash</option>
<option value="card" <?= $pt['type'] == 'card' ? 'selected' : '' ?>>Card</option>
<option value="api" <?= $pt['type'] == 'api' ? 'selected' : '' ?>>API Integration</option>
</select>
</div>
<div class="mb-3">
<label for="api_provider" class="form-label">API Provider (if Type is API)</label>
<select class="form-select" id="api_provider" name="api_provider">
<option value="">None</option>
<option value="thawani" <?= $pt['api_provider'] == 'thawani' ? 'selected' : '' ?>>Thawani</option>
<option value="wablas" <?= $pt['api_provider'] == 'wablas' ? 'selected' : '' ?>>Wablas (WhatsApp)</option>
</select>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="is_active" name="is_active" <?= $pt['is_active'] ? 'checked' : '' ?>>
<label class="form-check-label" for="is_active">Active</label>
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="payment_types.php" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

View File

@ -6,100 +6,200 @@ $pdo = db();
$message = '';
// Handle Delete
if (isset($_GET['delete_id'])) {
if (!has_permission('payment_types_del')) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to delete payment types.</div>';
// Handle Add/Edit Payment Type
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$name = trim($_POST['name']);
$code = trim($_POST['code']);
$is_active = isset($_POST['is_active']) ? 1 : 0;
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($name) || empty($code)) {
$message = '<div class="alert alert-danger">Name and code are required.</div>';
} else {
$stmt = $pdo->prepare("DELETE FROM payment_types WHERE id = ?");
$stmt->execute([$_GET['delete_id']]);
header("Location: payment_types.php?msg=deleted");
try {
if ($action === 'edit_payment_type' && $id) {
if (!has_permission('payment_types_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE payment_types SET name = ?, code = ?, is_active = ? WHERE id = ?");
$stmt->execute([$name, $code, $is_active, $id]);
$message = '<div class="alert alert-success">Payment type updated successfully!</div>';
}
} elseif ($action === 'add_payment_type') {
if (!has_permission('payment_types_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO payment_types (name, code, is_active) VALUES (?, ?, ?)");
$stmt->execute([$name, $code, $is_active]);
$message = '<div class="alert alert-success">Payment type created successfully!</div>';
}
}
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$message = '<div class="alert alert-danger">Code already exists.</div>';
} else {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('payment_types_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete payment types.</div>';
} else {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM payment_types WHERE id = ?")->execute([$id]);
header("Location: payment_types.php");
exit;
}
}
// Fetch Payment Types
$stmt = $pdo->query("SELECT * FROM payment_types ORDER BY id DESC");
$paymentTypes = $stmt->fetchAll(PDO::FETCH_ASSOC);
$query = "SELECT * FROM payment_types ORDER BY id ASC";
$payments_pagination = paginate_query($pdo, $query);
$payment_types = $payments_pagination['data'];
require_once __DIR__ . '/includes/header.php';
include 'includes/header.php';
?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0 text-gray-800">Payment Types</h2>
<h2 class="fw-bold mb-0">Payment Types</h2>
<?php if (has_permission('payment_types_add')): ?>
<a href="payment_type_edit.php" class="btn btn-primary">
<i class="bi bi-plus-lg me-2"></i>Add Payment Type
</a>
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-plus-lg"></i> Add Payment Type
</button>
<?php endif; ?>
</div>
<?= $message ?>
<?php if (isset($_GET['msg']) && $_GET['msg'] == 'deleted'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
Payment Type deleted successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($payments_pagination); ?>
</div>
<?php endif; ?>
<div class="card shadow mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th>ID</th>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Type</th>
<th>Provider</th>
<th>Code</th>
<th>Status</th>
<th class="text-end">Actions</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($paymentTypes)): ?>
<tr><td colspan="6" class="text-center py-4">No payment types found.</td></tr>
<?php else: ?>
<?php foreach ($paymentTypes as $pt): ?>
<?php foreach ($payment_types as $type): ?>
<tr>
<td><?= $pt['id'] ?></td>
<td class="fw-medium"><?= htmlspecialchars($pt['name']) ?></td>
<td class="ps-4 fw-medium">#<?= $type['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($type['name']) ?></td>
<td><code><?= htmlspecialchars($type['code']) ?></code></td>
<td>
<span class="badge bg-secondary"><?= htmlspecialchars(ucfirst($pt['type'])) ?></span>
</td>
<td><?= htmlspecialchars($pt['api_provider'] ?? '-') ?></td>
<td>
<?php if ($pt['is_active']): ?>
<span class="badge bg-success">Active</span>
<?php if ($type['is_active']): ?>
<span class="badge bg-success-subtle text-success px-3">Active</span>
<?php else: ?>
<span class="badge bg-danger">Inactive</span>
<span class="badge bg-secondary-subtle text-secondary px-3">Inactive</span>
<?php endif; ?>
</td>
<td class="text-end">
<td class="text-end pe-4">
<?php if (has_permission('payment_types_add')): ?>
<a href="payment_type_edit.php?id=<?= $pt['id'] ?>" class="btn btn-sm btn-outline-primary me-1">
<i class="bi bi-pencil"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= htmlspecialchars(json_encode($type), ENT_QUOTES, "UTF-8") ?>)' title="Edit"><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('payment_types_del')): ?>
<a href="payment_types.php?delete_id=<?= $pt['id'] ?>"
class="btn btn-sm btn-outline-danger"
onclick="return confirm('Are you sure you want to delete this payment type?');">
<i class="bi bi-trash"></i>
</a>
<a href="?delete=<?= $type['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this payment type?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($payment_types)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">No payment types found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($payments_pagination); ?>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
<!-- Payment Type Modal -->
<?php if (has_permission('payment_types_add')): ?>
<div class="modal fade" id="paymentTypeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="paymentTypeModalTitle">Add New Payment Type</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="paymentTypeForm">
<div class="modal-body">
<input type="hidden" name="action" id="paymentTypeAction" value="add_payment_type">
<input type="hidden" name="id" id="paymentTypeId">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" name="name" id="paymentTypeName" class="form-control" required placeholder="e.g. Cash, Credit Card">
</div>
<div class="mb-3">
<label class="form-label">Code <span class="text-danger">*</span></label>
<input type="text" name="code" id="paymentTypeCode" class="form-control" required placeholder="e.g. cash, card, loyalty">
</div>
<div class="mb-3 form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="paymentTypeIsActive" checked>
<label class="form-check-label" for="paymentTypeIsActive">Is Active</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Payment Type</button>
</div>
</form>
</div>
</div>
</div>
<script>
function getPaymentTypeModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('paymentTypeModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function openAddModal() {
const modal = getPaymentTypeModal();
if (!modal) return;
document.getElementById('paymentTypeModalTitle').innerText = 'Add New Payment Type';
document.getElementById('paymentTypeAction').value = 'add_payment_type';
document.getElementById('paymentTypeForm').reset();
document.getElementById('paymentTypeId').value = '';
modal.show();
}
function openEditModal(type) {
if (!type) return;
const modal = getPaymentTypeModal();
if (!modal) return;
document.getElementById('paymentTypeModalTitle').innerText = 'Edit Payment Type';
document.getElementById('paymentTypeAction').value = 'edit_payment_type';
document.getElementById('paymentTypeId').value = type.id;
document.getElementById('paymentTypeName').value = type.name || '';
document.getElementById('paymentTypeCode').value = type.code || '';
document.getElementById('paymentTypeIsActive').checked = type.is_active == 1;
modal.show();
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -1,178 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('products_add'); // Assuming edit requires same permission as add
if (!isset($_GET['id'])) {
header("Location: products.php");
exit;
}
$id = $_GET['id'];
$message = '';
// Fetch Product
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$id]);
$product = $stmt->fetch();
if (!$product) {
header("Location: products.php");
exit;
}
// Handle Update
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'];
$category_id = $_POST['category_id'];
$price = $_POST['price'];
$cost_price = $_POST['cost_price'] ?: 0;
$stock_quantity = $_POST['stock_quantity'] ?: 0;
$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/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$fileInfo = pathinfo($_FILES['image']['name']);
$fileExt = strtolower($fileInfo['extension']);
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($fileExt, $allowedExts)) {
$fileName = uniqid('prod_') . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
if (move_uploaded_file($_FILES['image']['tmp_name'], $targetFile)) {
$image_url = 'assets/images/products/' . $fileName;
} else {
$message = '<div class="alert alert-danger">Failed to upload image.</div>';
}
} else {
$message = '<div class="alert alert-danger">Invalid file type. Allowed: jpg, png, gif, webp.</div>';
}
}
if (empty($message)) {
$stmt = $pdo->prepare("UPDATE products SET name = ?, category_id = ?, price = ?, cost_price = ?, stock_quantity = ?, description = ?, image_url = ?, promo_discount_percent = ?, promo_date_from = ?, promo_date_to = ? WHERE id = ?");
if ($stmt->execute([$name, $category_id, $price, $cost_price, $stock_quantity, $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 = ?");
$stmt->execute([$id]);
$product = $stmt->fetch();
} else {
$message = '<div class="alert alert-danger">Error updating product.</div>';
}
}
}
// Fetch Categories
$categories = $pdo->query("SELECT * FROM categories ORDER BY name")->fetchAll();
include 'includes/header.php';
?>
<div class="mb-4">
<a href="products.php" class="text-decoration-none text-muted mb-2 d-inline-block"><i class="bi bi-arrow-left"></i> Back to Products</a>
<h2 class="fw-bold mb-0">Edit Product: <?= htmlspecialchars($product['name']) ?></h2>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label class="form-label text-muted small fw-bold">PRODUCT NAME</label>
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($product['name']) ?>" required>
</div>
<div class="row g-3">
<div class="col-md-6 mb-3">
<label class="form-label text-muted small fw-bold">CATEGORY</label>
<select name="category_id" class="form-select" required>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $cat['id'] == $product['category_id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-muted small fw-bold">SELLING PRICE ($)</label>
<input type="number" step="0.01" name="price" class="form-control" value="<?= htmlspecialchars($product['price']) ?>" required>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 mb-3">
<label class="form-label text-muted small fw-bold">COST PRICE ($)</label>
<input type="number" step="0.01" name="cost_price" class="form-control" value="<?= htmlspecialchars($product['cost_price'] ?? '0.00') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label text-muted small fw-bold">STOCK QUANTITY</label>
<input type="number" name="stock_quantity" class="form-control" value="<?= htmlspecialchars($product['stock_quantity'] ?? '0') ?>">
</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 text-muted small fw-bold">DESCRIPTION</label>
<textarea name="description" class="form-control" rows="5"><?= htmlspecialchars($product['description']) ?></textarea>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label text-muted small fw-bold">CURRENT IMAGE</label>
<div class="mb-2">
<img src="<?= htmlspecialchars(strpos($product['image_url'], 'http') === 0 ? $product['image_url'] : '../' . $product['image_url']) ?>" class="img-fluid rounded border" alt="Product Image">
</div>
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold">UPLOAD NEW IMAGE</label>
<input type="file" name="image" class="form-control" accept="image/*">
<div class="form-text">Leave empty to keep current image.</div>
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-end gap-2">
<a href="products.php" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -7,11 +7,10 @@ require_permission('products_view');
$message = '';
// Handle Add Product
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_product') {
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 {
// Handle Add/Edit Product
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$name = $_POST['name'];
$category_id = $_POST['category_id'];
$price = $_POST['price'];
@ -23,42 +22,56 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
$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
$image_url = null;
if ($id) {
$stmt = $pdo->prepare("SELECT image_url FROM products WHERE id = ?");
$stmt->execute([$id]);
$image_url = $stmt->fetchColumn();
} else {
$image_url = 'https://placehold.co/400x300?text=' . urlencode($name);
}
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/products/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$fileInfo = pathinfo($_FILES['image']['name']);
$fileExt = strtolower($fileInfo['extension']);
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($fileExt, $allowedExts)) {
$fileName = uniqid('prod_') . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
if (move_uploaded_file($_FILES['image']['tmp_name'], $targetFile)) {
$file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$fileName = uniqid('prod_') . '.' . $file_ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $fileName)) {
$image_url = 'assets/images/products/' . $fileName;
}
}
}
$stmt = $pdo->prepare("INSERT INTO products (name, category_id, price, cost_price, stock_quantity, description, image_url, promo_discount_percent, promo_date_from, promo_date_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
if ($stmt->execute([$name, $category_id, $price, $cost_price, $stock_quantity, $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>';
try {
if ($action === 'edit_product' && $id) {
// Check for edit OR add (for backward compatibility)
if (!has_permission('products_edit') && !has_permission('products_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit products.</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("UPDATE products SET name = ?, category_id = ?, price = ?, cost_price = ?, stock_quantity = ?, description = ?, image_url = ?, promo_discount_percent = ?, promo_date_from = ?, promo_date_to = ? WHERE id = ?");
$stmt->execute([$name, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to, $id]);
$message = '<div class="alert alert-success">Product updated successfully!</div>';
}
} elseif ($action === 'add_product') {
if (!has_permission('products_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add products.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO products (name, category_id, price, cost_price, stock_quantity, description, image_url, promo_discount_percent, promo_date_from, promo_date_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$name, $category_id, $price, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to]);
$message = '<div class="alert alert-success">Product created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
// Handle Delete
if (isset($_GET['delete'])) {
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>';
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete products.</div>';
} else {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM products WHERE id = ?")->execute([$id]);
@ -67,13 +80,10 @@ if (isset($_GET['delete'])) {
}
}
// Fetch Categories for Dropdown (moved up for filter usage)
$categories = $pdo->query("SELECT * FROM categories ORDER BY name")->fetchAll();
// Handle Search and Filter
$search = $_GET['search'] ?? '';
$category_filter = $_GET['category_filter'] ?? '';
$params = [];
$where = [];
@ -92,10 +102,7 @@ if ($category_filter) {
$params[] = $category_filter;
}
if (!empty($where)) {
$query .= " WHERE " . implode(" AND ", $where);
}
if (!empty($where)) $query .= " WHERE " . implode(" AND ", $where);
$query .= " ORDER BY p.id DESC";
$products_pagination = paginate_query($pdo, $query, $params);
@ -106,11 +113,11 @@ 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">Products</h2>
<p class="text-muted mb-0">Manage your catalog</p>
<h2 class="fw-bold mb-1">Products Catalog</h2>
<p class="text-muted mb-0">Manage items, stock, and pricing</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;">
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#productModal" onclick="prepareAddForm()" style="border-radius: 12px;">
<i class="bi bi-plus-lg me-1"></i> Add Product
</button>
<?php endif; ?>
@ -118,53 +125,43 @@ include 'includes/header.php';
<?= $message ?>
<!-- Filter & Search Bar -->
<div class="filter-bar">
<div class="filter-bar mb-4 p-3 bg-white rounded-4 shadow-sm">
<form method="GET" class="row g-3 align-items-center">
<div class="col-md-5">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted ps-3" style="border-radius: 10px 0 0 10px;"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-start-0 ps-0" placeholder="Search by name, description..." value="<?= htmlspecialchars($search) ?>" style="border-radius: 0 10px 10px 0;">
<span class="input-group-text bg-light border-0 text-muted"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-0 bg-light" placeholder="Search products..." value="<?= htmlspecialchars($search) ?>" style="border-radius: 0 10px 10px 0;">
</div>
</div>
<div class="col-md-3">
<select name="category_filter" class="form-select" onchange="this.form.submit()" style="border-radius: 10px;">
<select name="category_filter" class="form-select border-0 bg-light rounded-3" onchange="this.form.submit()">
<option value="">All Categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $category_filter == $cat['id'] ? 'selected' : '' ?>><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4 text-md-end">
<?php if ($search || $category_filter): ?>
<a href="products.php" class="btn btn-light text-muted" style="border-radius: 10px;"><i class="bi bi-x-circle me-1"></i> Clear Filters</a>
<?php endif; ?>
</div>
</form>
</div>
<!-- Friendly Product List -->
<?php if (empty($products)): ?>
<div class="text-center py-5 bg-white rounded-3 shadow-sm">
<div class="mb-3 text-muted display-1"><i class="bi bi-box-seam"></i></div>
<div class="text-center py-5 bg-white rounded-4 shadow-sm">
<i class="bi bi-box-seam display-1 text-muted opacity-25 mb-3 d-block"></i>
<h4 class="text-dark">No products found</h4>
<p class="text-muted">Try adjusting your search or filters.</p>
<?php if ($search || $category_filter): ?>
<a href="products.php" class="btn btn-outline-primary rounded-pill px-4">Clear All Filters</a>
<?php endif; ?>
<p class="text-muted">Try adjusting your filters or search terms.</p>
</div>
<?php else: ?>
<div class="table-responsive" style="overflow-x: visible;">
<table class="friendly-table">
<thead>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th>Product</th>
<th class="ps-4">Product</th>
<th>Category</th>
<th>Stock</th>
<th>Cost Price</th>
<th>Selling Price</th>
<th>Price</th>
<th>Promotion</th>
<th class="text-end">Actions</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
@ -176,69 +173,51 @@ include 'includes/header.php';
!empty($product['promo_date_to']) &&
$today >= $product['promo_date_from'] &&
$today <= $product['promo_date_to'];
$stock = intval($product['stock_quantity'] ?? 0);
$stock_class = $stock <= 5 ? 'text-danger fw-bold' : ($stock <= 20 ? 'text-warning' : 'text-success');
?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<img src="<?= htmlspecialchars(strpos($product['image_url'], 'http') === 0 ? $product['image_url'] : '../' . $product['image_url']) ?>" alt="" class="img-thumb-lg me-3">
<img src="<?= htmlspecialchars(strpos($product['image_url'], 'http') === 0 ? $product['image_url'] : '../' . $product['image_url']) ?>" alt="" class="rounded-3 me-3 border shadow-sm" style="width: 48px; height: 48px; object-fit: cover;">
<div>
<div class="fw-bold text-dark mb-1" style="font-size: 1.05rem;"><?= htmlspecialchars($product['name']) ?></div>
<div class="text-muted small text-truncate" style="max-width: 250px;"><?= htmlspecialchars($product['description'] ?? '') ?></div>
<div class="fw-bold text-dark"><?= htmlspecialchars($product['name']) ?></div>
<div class="text-muted small text-truncate" style="max-width: 180px;"><?= htmlspecialchars($product['description'] ?? '') ?></div>
</div>
</div>
</td>
<td><span class="badge bg-light text-dark border px-2 py-1"><?= htmlspecialchars($product['category_name'] ?? 'Uncategorized') ?></span></td>
<td>
<span class="badge-soft"><?= htmlspecialchars($product['category_name'] ?? 'Uncategorized') ?></span>
</td>
<td>
<span class="<?= $stock_class ?>"><?= $stock ?></span>
</td>
<td>
<span class="text-muted"><?= format_currency($product['cost_price'] ?? 0) ?></span>
<?php $stock = (int)$product['stock_quantity']; ?>
<span class="badge <?= $stock <= 5 ? 'bg-danger-subtle text-danger' : ($stock <= 20 ? 'bg-warning-subtle text-warning' : 'bg-success-subtle text-success') ?> px-2 py-1 border border-<?= $stock <= 5 ? 'danger' : ($stock <= 20 ? 'warning' : 'success') ?>">
<?= $stock ?> in stock
</span>
</td>
<td>
<?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="fw-bold text-primary"><?= format_currency($product['price'] * (1 - ($product['promo_discount_percent'] / 100))) ?></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>
<div class="fw-bold text-dark"><?= format_currency($product['price']) ?></div>
<?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>
<span class="badge bg-success rounded-pill px-2">-<?= floatval($product['promo_discount_percent']) ?>%</span>
<?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; ?>
<span class="badge bg-secondary rounded-pill px-2">Inactive</span>
<?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 if (has_permission('products_edit') || has_permission('products_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3"
data-bs-toggle="modal" data-bs-target="#productModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($product), ENT_QUOTES, "UTF-8") ?>)'>Edit</button>
<?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>
<a href="?delete=<?= $product['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Delete this product? This action cannot be undone.')">Delete</a>
<?php endif; ?>
</div>
</td>
@ -247,100 +226,139 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="mt-4">
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($products_pagination); ?>
</div>
</div>
<?php endif; ?>
<!-- Add Product Modal -->
<?php if (has_permission('products_add')): ?>
<div class="modal fade" id="addProductModal" tabindex="-1">
<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>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<!-- Product Modal -->
<?php if (has_permission('products_add') || has_permission('products_edit')): ?>
<div class="modal fade" id="productModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="productModalTitle">Add New Product</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" enctype="multipart/form-data">
<div class="modal-body pt-4">
<input type="hidden" name="action" value="add_product">
<div class="row">
<div class="col-md-7">
<form method="POST" id="productForm" enctype="multipart/form-data">
<div class="modal-body p-4">
<input type="hidden" name="action" id="productAction" value="add_product">
<input type="hidden" name="id" id="productId">
<div class="row g-3 mb-4">
<div class="col-md-8">
<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;">
<label class="form-label small fw-bold text-muted">PRODUCT NAME <span class="text-danger">*</span></label>
<input type="text" name="name" id="productName" class="form-control rounded-3" required>
</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;">
<div class="row g-3">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">CATEGORY <span class="text-danger">*</span></label>
<select name="category_id" id="productCategoryId" class="form-select rounded-3" required>
<?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">SELLING PRICE ($)</label>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">SELLING PRICE <span class="text-danger">*</span></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;">
<span class="input-group-text bg-light border-0"><?= get_company_settings()['currency_symbol'] ?></span>
<input type="number" step="0.01" name="price" id="productPrice" class="form-control rounded-3 border-0 bg-light" required>
</div>
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">COST 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="cost_price" class="form-control border-start-0" placeholder="0.00" style="border-radius: 0 10px 10px 0;">
<div class="row g-3">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">COST PRICE</label>
<input type="number" step="0.01" name="cost_price" id="productCostPrice" class="form-control rounded-3 border-0 bg-light">
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">STOCK QUANTITY</label>
<input type="number" name="stock_quantity" id="productStockQuantity" class="form-control rounded-3 border-0 bg-light">
</div>
</div>
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">INITIAL STOCK</label>
<input type="number" name="stock_quantity" class="form-control" placeholder="0" style="border-radius: 10px;">
</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-5">
<div class="card bg-light border-0 h-100">
<div class="col-md-4">
<div class="card bg-light border-0 h-100 rounded-4">
<div class="card-body">
<h6 class="fw-bold mb-3"><i class="bi bi-tag-fill me-2 text-primary"></i>Promotion</h6>
<h6 class="fw-bold mb-3"><i class="bi bi-percent 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;">
<label class="form-label small fw-bold text-muted">DISCOUNT (%)</label>
<input type="number" step="0.01" name="promo_discount_percent" id="productPromoDiscount" class="form-control border-0 rounded-3">
</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 class="mb-2">
<label class="form-label small fw-bold text-muted">START DATE</label>
<input type="date" name="promo_date_from" id="productPromoFrom" class="form-control border-0 rounded-3">
</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 class="mb-0">
<label class="form-label small fw-bold text-muted">END DATE</label>
<input type="date" name="promo_date_to" id="productPromoTo" class="form-control border-0 rounded-3">
</div>
</div>
</div>
</div>
</div>
<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>
<div class="mb-4">
<label class="form-label small fw-bold text-muted">DESCRIPTION</label>
<textarea name="description" id="productDescription" class="form-control rounded-3" rows="3" placeholder="Describe your product..."></textarea>
</div>
<div class="mb-0">
<label class="form-label small fw-bold text-muted">PRODUCT IMAGE</label>
<div class="d-flex align-items-center gap-3 bg-light p-3 rounded-4 border border-dashed">
<img src="" id="productImagePreview" class="rounded-3 border shadow-sm" style="width: 64px; height: 64px; object-fit: cover; display: none;">
<div class="flex-grow-1">
<input type="file" name="image" class="form-control border-0 bg-transparent" accept="image/*">
<small class="text-muted">Recommended: Square image, max 2MB.</small>
</div>
</div>
<div class="modal-footer border-top-0 pt-0 pb-4 px-4">
</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">Save Product</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Save Product</button>
</div>
</form>
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('productModalTitle').innerText = 'Add New Product';
document.getElementById('productAction').value = 'add_product';
document.getElementById('productForm').reset();
document.getElementById('productId').value = '';
document.getElementById('productImagePreview').style.display = 'none';
}
function prepareEditForm(prod) {
if (!prod) return;
document.getElementById('productModalTitle').innerText = 'Edit Product: ' + prod.name;
document.getElementById('productAction').value = 'edit_product';
document.getElementById('productId').value = prod.id;
document.getElementById('productName').value = prod.name;
document.getElementById('productCategoryId').value = prod.category_id;
document.getElementById('productPrice').value = prod.price;
document.getElementById('productCostPrice').value = prod.cost_price || '';
document.getElementById('productStockQuantity').value = prod.stock_quantity || '0';
document.getElementById('productDescription').value = prod.description || '';
document.getElementById('productPromoDiscount').value = prod.promo_discount_percent || '';
document.getElementById('productPromoFrom').value = prod.promo_date_from || '';
document.getElementById('productPromoTo').value = prod.promo_date_to || '';
if (prod.image_url) {
const preview = document.getElementById('productImagePreview');
preview.src = prod.image_url.startsWith('http') ? prod.image_url : '../' + prod.image_url;
preview.style.display = 'block';
} else {
document.getElementById('productImagePreview').style.display = 'none';
}
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -59,29 +59,29 @@ 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">Purchases</h2>
<p class="text-muted mb-0">Manage inventory restocks and supplier invoices</p>
<h2 class="fw-bold mb-1 text-dark">Purchases Inventory</h2>
<p class="text-muted mb-0">Manage restocks, supplier invoices and inventory tracking</p>
</div>
<?php if (has_permission('purchases_add')): ?>
<a href="purchase_edit.php" class="btn btn-primary shadow-sm">
<i class="bi bi-plus-lg me-1"></i> New Purchase
<a href="purchase_edit.php" class="btn btn-primary btn-lg shadow-sm" style="border-radius: 12px;">
<i class="bi bi-plus-lg me-1"></i> New Purchase Order
</a>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<div class="card border-0 shadow-sm mb-4 rounded-4">
<div class="card-body p-4">
<form method="GET" class="row g-3 align-items-center">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-start-0" placeholder="Search notes..." value="<?= htmlspecialchars($search) ?>">
<span class="input-group-text bg-light border-0 text-muted"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-0 bg-light" placeholder="Search by notes..." value="<?= htmlspecialchars($search) ?>" style="border-radius: 0 10px 10px 0;">
</div>
</div>
<div class="col-md-3">
<select name="supplier_filter" class="form-select">
<select name="supplier_filter" class="form-select border-0 bg-light rounded-3">
<option value="">All Suppliers</option>
<?php foreach ($suppliers as $s): ?>
<option value="<?= $s['id'] ?>" <?= $supplier_filter == $s['id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option>
@ -89,7 +89,7 @@ include 'includes/header.php';
</select>
</div>
<div class="col-md-2">
<select name="status_filter" class="form-select">
<select name="status_filter" class="form-select border-0 bg-light rounded-3">
<option value="">All Status</option>
<option value="pending" <?= $status_filter == 'pending' ? 'selected' : '' ?>>Pending</option>
<option value="completed" <?= $status_filter == 'completed' ? 'selected' : '' ?>>Completed</option>
@ -97,22 +97,22 @@ include 'includes/header.php';
</select>
</div>
<div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 w-100">Filter</button>
<button type="submit" class="btn btn-primary px-4 w-100 rounded-pill fw-bold">Filter Results</button>
<?php if ($search || $supplier_filter || $status_filter): ?>
<a href="purchases.php" class="btn btn-light text-muted px-3"><i class="bi bi-x-lg"></i></a>
<a href="purchases.php" class="btn btn-light text-muted px-3 rounded-circle d-flex align-items-center justify-content-center"><i class="bi bi-x-lg"></i></a>
<?php endif; ?>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<thead class="bg-light">
<tr>
<th class="ps-4">Date</th>
<th class="ps-4 py-3">Purchase Details</th>
<th>Supplier</th>
<th>Status</th>
<th>Total Amount</th>
@ -122,33 +122,33 @@ include 'includes/header.php';
<tbody>
<?php foreach ($purchases as $p):
$status_badge = 'bg-secondary';
if ($p['status'] === 'completed') $status_badge = 'bg-success';
if ($p['status'] === 'pending') $status_badge = 'bg-warning text-dark';
if ($p['status'] === 'cancelled') $status_badge = 'bg-danger';
if ($p['status'] === 'completed') $status_badge = 'bg-success-subtle text-success border border-success';
if ($p['status'] === 'pending') $status_badge = 'bg-warning-subtle text-warning border border-warning';
if ($p['status'] === 'cancelled') $status_badge = 'bg-danger-subtle text-danger border border-danger';
?>
<tr>
<td class="ps-4">
<div class="fw-bold text-dark"><?= date('M d, Y', strtotime($p['purchase_date'])) ?></div>
<div class="small text-muted">ID: #<?= $p['id'] ?></div>
<div class="small text-muted fw-medium">Ref: #INV-<?= str_pad($p['id'], 5, '0', STR_PAD_LEFT) ?></div>
</td>
<td>
<div class="fw-medium text-dark"><?= htmlspecialchars($p['supplier_name'] ?? 'Direct Purchase') ?></div>
<div class="fw-bold text-dark"><?= htmlspecialchars($p['supplier_name'] ?? 'Direct Purchase') ?></div>
</td>
<td>
<span class="badge <?= $status_badge ?> rounded-pill px-3"><?= ucfirst($p['status']) ?></span>
<span class="badge <?= $status_badge ?> rounded-pill px-3 py-2 small fw-bold text-uppercase" style="font-size: 0.7rem;"><?= ucfirst($p['status']) ?></span>
</td>
<td>
<div class="fw-bold text-primary"><?= format_currency($p['total_amount']) ?></div>
<div class="fw-bold text-primary fs-5"><?= format_currency($p['total_amount']) ?></div>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<?php if (has_permission('purchases_add')): ?>
<a href="purchase_edit.php?id=<?= $p['id'] ?>" class="btn btn-sm btn-outline-primary" title="Edit/View">
<?php if (has_permission('purchases_edit') || has_permission('purchases_add')): ?>
<a href="purchase_edit.php?id=<?= $p['id'] ?>" class="btn btn-sm btn-outline-primary rounded-pill px-3" title="Edit/View">
<i class="bi bi-pencil-square me-1"></i> Edit
</a>
<?php endif; ?>
<?php if (has_permission('purchases_del')): ?>
<a href="?delete=<?= $p['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to delete this purchase record?')" title="Delete">
<a href="?delete=<?= $p['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Are you sure you want to delete this purchase record?')" title="Delete">
<i class="bi bi-trash me-1"></i> Delete
</a>
<?php endif; ?>
@ -159,8 +159,9 @@ include 'includes/header.php';
<?php if (empty($purchases)): ?>
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<div class="mb-2 display-6"><i class="bi bi-cart-x"></i></div>
<div>No purchase records found.</div>
<div class="mb-2 display-6 opacity-25"><i class="bi bi-cart-x"></i></div>
<h5 class="fw-bold">No purchase records found.</h5>
<p class="small">Try different search terms or start by adding a new purchase.</p>
</td>
</tr>
<?php endif; ?>

View File

@ -1,286 +1,181 @@
<?php
require_once 'includes/header.php';
// Check permission
if (function_exists('require_permission')) {
require_permission('users_view'); // Reuse user view permission for ratings
}
require_once __DIR__ . "/../includes/functions.php";
require_permission("ratings_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$tab = $_GET['tab'] ?? 'staff';
$date_from = $_GET['date_from'] ?? '';
$date_to = $_GET['date_to'] ?? '';
$params = [];
$where_clauses = [];
$message = '';
if ($date_from) {
$where_clauses[] = "r.created_at >= :date_from";
$params['date_from'] = $date_from . ' 00:00:00';
// Handle Add Rating (Manual)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_rating') {
$user_id = (int)$_POST['user_id'];
$order_id = !empty($_POST['order_id']) ? (int)$_POST['order_id'] : null;
$rating = (int)$_POST['rating'];
$comment = trim($_POST['comment']);
try {
$stmt = $pdo->prepare("INSERT INTO staff_ratings (user_id, order_id, rating, comment) VALUES (?, ?, ?, ?)");
$stmt->execute([$user_id, $order_id, $rating, $comment]);
$message = '<div class="alert alert-success">Rating added successfully!</div>';
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
if ($date_to) {
$where_clauses[] = "r.created_at <= :date_to";
$params['date_to'] = $date_to . ' 23:59:59';
}
$where_sql = !empty($where_clauses) ? "WHERE " . implode(" AND ", $where_clauses) : "";
// Fetch Staff summary stats
$summaryQuery = "
SELECT u.id, u.full_name, u.username, u.profile_pic,
AVG(r.rating) as avg_rating, COUNT(r.id) as total_ratings
FROM users u
JOIN staff_ratings r ON u.id = r.user_id
$where_sql
GROUP BY u.id
ORDER BY avg_rating DESC
";
$summaryStmt = $pdo->prepare($summaryQuery);
$summaryStmt->execute($params);
$summaries = $summaryStmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch Service summary stats
$serviceWhereSql = str_replace('r.created_at', 'created_at', $where_sql);
$serviceParams = $params;
$serviceSummaryQuery = "SELECT AVG(rating) as avg_rating, COUNT(id) as total_ratings FROM service_ratings $serviceWhereSql";
$serviceSummaryStmt = $pdo->prepare($serviceSummaryQuery);
$serviceSummaryStmt->execute($serviceParams);
$serviceSummary = $serviceSummaryStmt->fetch(PDO::FETCH_ASSOC);
if ($tab === 'service') {
$query = "SELECT * FROM service_ratings $serviceWhereSql ORDER BY created_at DESC";
$list_params = $serviceParams;
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('settings')) { // Use settings permission for deletion
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$query = "
SELECT r.*, u.full_name, u.username, u.profile_pic
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM staff_ratings WHERE id = ?")->execute([$id]);
header("Location: ratings.php");
exit;
}
}
$staff = $pdo->query("SELECT id, full_name, username FROM users WHERE is_ratable = 1 ORDER BY full_name ASC")->fetchAll();
$query = "SELECT r.*, u.full_name as staff_name, u.username as staff_username
FROM staff_ratings r
JOIN users u ON r.user_id = u.id
$where_sql
ORDER BY r.created_at DESC
";
$list_params = $params;
}
ORDER BY r.created_at DESC";
$ratings_pagination = paginate_query($pdo, $query);
$ratings = $ratings_pagination['data'];
$pagination = paginate_query($pdo, $query, $list_params);
$ratings = $pagination['data'];
include 'includes/header.php';
?>
<style>
.extra-small { font-size: 0.7rem; }
.table-ratings { font-size: 0.8rem; }
.table-ratings th { font-weight: 600; text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.03em; }
</style>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0 text-gray-800">Ratings & Feedback</h2>
<div class="d-flex gap-2">
<button class="btn btn-warning btn-sm rounded-pill" onclick="showRatingQR()">
<i class="bi bi-qr-code"></i> Share Rating QR
<h2 class="fw-bold mb-0">Staff Ratings</h2>
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-star"></i> Add Manual Rating
</button>
<a href="../rate.php" target="_blank" class="btn btn-outline-primary btn-sm rounded-pill">
<i class="bi bi-box-arrow-up-right"></i> Open Public Rating Page
</a>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body py-3">
<form method="GET" class="row g-3 align-items-end">
<input type="hidden" name="tab" value="<?= htmlspecialchars($tab) ?>">
<div class="col-md-3">
<label class="form-label small fw-bold mb-1">From Date</label>
<input type="date" name="date_from" class="form-control form-control-sm" value="<?= htmlspecialchars($date_from) ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold mb-1">To Date</label>
<input type="date" name="date_to" class="form-control form-control-sm" value="<?= htmlspecialchars($date_to) ?>">
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary btn-sm px-3">
<i class="bi bi-filter"></i> Filter
</button>
<a href="ratings.php?tab=<?= htmlspecialchars($tab) ?>" class="btn btn-light btn-sm px-3 border">
<i class="bi bi-arrow-clockwise"></i> Reset
</a>
</div>
</form>
</div>
</div>
<?= $message ?>
<ul class="nav nav-pills mb-4 bg-white p-2 rounded-4 shadow-sm d-inline-flex">
<li class="nav-item">
<a class="nav-link rounded-pill <?= $tab === 'staff' ? 'active' : '' ?>" href="?tab=staff&date_from=<?= $date_from ?>&date_to=<?= $date_to ?>">
<i class="bi bi-people me-1"></i> Staff Performance
</a>
</li>
<li class="nav-item">
<a class="nav-link rounded-pill <?= $tab === 'service' ? 'active' : '' ?>" href="?tab=service&date_from=<?= $date_from ?>&date_to=<?= $date_to ?>">
<i class="bi bi-shop me-1"></i> Restaurant Service
</a>
</li>
</ul>
<?php if ($tab === 'staff'): ?>
<!-- Staff Summary Cards -->
<?php if (empty($summaries)): ?>
<div class="alert alert-info border-0 shadow-sm rounded-4 text-center py-4 mb-5">
<i class="bi bi-info-circle fs-2 mb-2"></i>
<p class="mb-0">No staff members have been rated yet for the selected period.</p>
</div>
<?php else: ?>
<div class="row g-4 mb-5">
<?php foreach ($summaries as $summary): ?>
<div class="col-md-3">
<div class="card border-0 shadow-sm rounded-4 h-100">
<div class="card-body text-center">
<?php if ($summary['profile_pic']): ?>
<img src="../<?= htmlspecialchars($summary['profile_pic']) ?>" alt="Staff" class="rounded-circle mb-3" style="width: 56px; height: 56px; object-fit: cover;">
<?php else: ?>
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-3" style="width: 56px; height: 56px;">
<i class="bi bi-person fs-3 text-primary"></i>
</div>
<?php endif; ?>
<h5 class="card-title mb-1 small fw-bold text-truncate" title="<?= htmlspecialchars($summary['full_name'] ?: $summary['username']) ?>">
<?= htmlspecialchars($summary['full_name'] ?: $summary['username']) ?>
</h5>
<div class="text-warning mb-2" style="font-size: 0.8rem;">
<?php
$avg = round($summary['avg_rating']);
for ($i = 1; $i <= 5; $i++) {
echo $i <= $avg ? '<i class="bi bi-star-fill"></i>' : '<i class="bi bi-star"></i>';
}
?>
<span class="text-muted ms-1 small">(<?= number_format($summary['avg_rating'], 1) ?>)</span>
</div>
<p class="text-muted extra-small mb-0"><?= $summary['total_ratings'] ?> ratings</p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php else: ?>
<!-- Service Summary Card -->
<div class="row mb-5">
<div class="col-md-4">
<div class="card border-0 shadow-sm rounded-4 bg-primary text-white">
<div class="card-body text-center py-4">
<i class="bi bi-shop fs-1 mb-3"></i>
<h4 class="fw-bold mb-1">Overall Service</h4>
<div class="fs-2 fw-bold mb-2">
<?php if ($serviceSummary['total_ratings'] > 0): ?>
<?= number_format($serviceSummary['avg_rating'], 1) ?> / 5.0
<?php else: ?>
N/A
<?php endif; ?>
</div>
<div class="text-white-50 extra-small">
Based on <?= $serviceSummary['total_ratings'] ?> reviews
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<!-- Detailed List -->
<div class="card border-0 shadow-sm rounded-4">
<div class="card-header bg-white py-3 border-bottom-0">
<h5 class="card-title mb-0 small fw-bold"><?= $tab === 'service' ? 'Service Feedback' : 'Staff Feedback' ?></h5>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($ratings_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 table-ratings">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-3">Date</th>
<?php if ($tab === 'staff'): ?><th>Staff Member</th><?php endif; ?>
<th class="ps-4">Date</th>
<th>Staff Member</th>
<th>Rating</th>
<th>Comment</th>
<th>Order ID</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($ratings)): ?>
<?php foreach ($ratings as $r): ?>
<tr>
<td colspan="<?= $tab === 'staff' ? 4 : 3 ?>" class="text-center py-4 text-muted small">No ratings found yet.</td>
</tr>
<?php else: ?>
<?php foreach ($ratings as $rating): ?>
<tr>
<td class="ps-3 text-muted">
<?= date('M d, Y H:i', strtotime($rating['created_at'])) ?>
<td class="ps-4">
<div class="fw-bold"><?= date('M d, Y', strtotime($r['created_at'])) ?></div>
<small class="text-muted"><?= date('H:i', strtotime($r['created_at'])) ?></small>
</td>
<?php if ($tab === 'staff'): ?>
<td>
<div class="d-flex align-items-center">
<?php if ($rating['profile_pic']): ?>
<img src="../<?= htmlspecialchars($rating['profile_pic']) ?>" class="rounded-circle me-2 shadow-sm" style="width: 24px; height: 24px; object-fit: cover;">
<?php else: ?>
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-2 shadow-sm" style="width: 24px; height: 24px;">
<i class="bi bi-person text-primary" style="font-size: 0.7rem;"></i>
</div>
<?php endif; ?>
<span class="fw-medium text-dark"><?= htmlspecialchars($rating['full_name'] ?: $rating['username']) ?></span>
</div>
<div class="fw-bold text-dark"><?= htmlspecialchars($r['staff_name'] ?: $r['staff_username']) ?></div>
</td>
<?php endif; ?>
<td>
<div class="text-warning">
<?php
for ($i = 1; $i <= 5; $i++) {
echo $i <= $rating['rating'] ? '<i class="bi bi-star-fill"></i>' : '<i class="bi bi-star text-muted" style="opacity: 0.3;"></i>';
}
?>
<?php for($i=1; $i<=5; $i++): ?>
<i class="bi bi-star<?= $i <= $r['rating'] ? '-fill' : '' ?>"></i>
<?php endfor; ?>
</div>
</td>
<td class="text-wrap" style="max-width: 300px;">
<span class="text-muted italic"><?= htmlspecialchars($rating['comment'] ?: '-') ?></span>
<td><small><?= htmlspecialchars($r['comment'] ?: '-') ?></small></td>
<td><?= $r['order_id'] ? '#' . $r['order_id'] : '-' ?></td>
<td class="text-end pe-4">
<?php if (has_permission('settings')): ?>
<a href="?delete=<?= $r['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this rating?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($ratings)): ?>
<tr>
<td colspan="6" class="text-center py-4 text-muted">No ratings found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="card-footer bg-white py-3">
<?php render_pagination_controls($pagination, ['date_from' => $date_from, 'date_to' => $date_to]); ?>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($ratings_pagination); ?>
</div>
</div>
</div>
<!-- Rating QR Modal -->
<div class="modal fade" id="ratingQRModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-warning border-0">
<h5 class="modal-title fw-bold text-dark"><i class="bi bi-star-fill me-2"></i> Customer Rating QR</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<!-- Rating Modal -->
<div class="modal fade" id="ratingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Add Manual Rating</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center py-4">
<p class="text-muted mb-4 extra-small px-4">Show this QR code to the customer to rate our staff and service.</p>
<div class="mb-4">
<img id="rating-qr-img" src="" alt="Rating QR Code" class="img-fluid border p-3 bg-white shadow-sm rounded-3" style="max-width: 180px;">
<form method="POST" id="ratingForm">
<div class="modal-body">
<input type="hidden" name="action" value="add_rating">
<div class="mb-3">
<label class="form-label">Staff Member <span class="text-danger">*</span></label>
<select name="user_id" class="form-select" required>
<option value="">Select Staff</option>
<?php foreach ($staff as $s): ?>
<option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['full_name'] ?: $s['username']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="alert alert-light border extra-small text-break text-center mx-4 mb-0" id="rating-url-text"></div>
<div class="mb-3">
<label class="form-label">Order ID (Optional)</label>
<input type="number" name="order_id" class="form-control" placeholder="e.g. 1024">
</div>
<div class="modal-footer border-top-0 justify-content-center pb-4">
<button class="btn btn-primary px-4 fw-bold btn-sm rounded-pill" onclick="window.print()">
<i class="bi bi-printer me-2"></i> Print QR Code
</button>
<div class="mb-3">
<label class="form-label">Rating <span class="text-danger">*</span></label>
<select name="rating" class="form-select" required>
<option value="5">5 Stars - Excellent</option>
<option value="4">4 Stars - Very Good</option>
<option value="3" selected>3 Stars - Good</option>
<option value="2">2 Stars - Fair</option>
<option value="1">1 Star - Poor</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Comment</label>
<textarea name="comment" class="form-control" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Rating</button>
</div>
</form>
</div>
</div>
</div>
<script>
const BASE_URL = '<?= get_base_url() ?>';
function getRatingModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('ratingModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function showRatingQR() {
const ratingUrl = BASE_URL + 'rate.php';
document.getElementById('rating-qr-img').src = 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=' + encodeURIComponent(ratingUrl);
document.getElementById('rating-url-text').textContent = ratingUrl;
const modal = new bootstrap.Modal(document.getElementById('ratingQRModal'));
function openAddModal() {
const modal = getRatingModal();
if (!modal) return;
document.getElementById('ratingForm').reset();
modal.show();
}
</script>
<?php require_once 'includes/footer.php'; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -8,42 +8,37 @@ $message = '';
// Handle Add/Edit Supplier
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'add_supplier') {
if (!has_permission('suppliers_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add suppliers.</div>';
} else {
$name = $_POST['name'];
$contact_person = $_POST['contact_person'];
$email = $_POST['email'];
$phone = $_POST['phone'];
$address = $_POST['address'];
$vat_no = $_POST['vat_no'];
$action = $_POST['action'];
$name = trim($_POST['name']);
$contact_person = trim($_POST['contact_person']);
$email = trim($_POST['email']);
$phone = trim($_POST['phone']);
$address = trim($_POST['address']);
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$stmt = $pdo->prepare("INSERT INTO suppliers (name, contact_person, email, phone, address, vat_no) VALUES (?, ?, ?, ?, ?, ?)");
if ($stmt->execute([$name, $contact_person, $email, $phone, $address, $vat_no])) {
$message = '<div class="alert alert-success">Supplier added successfully!</div>';
if (empty($name)) {
$message = '<div class="alert alert-danger">Supplier name is required.</div>';
} else {
$message = '<div class="alert alert-danger">Error adding supplier.</div>';
}
}
} elseif ($_POST['action'] === 'edit_supplier') {
try {
if ($action === 'edit_supplier' && $id) {
if (!has_permission('suppliers_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit suppliers.</div>';
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$id = $_POST['id'];
$name = $_POST['name'];
$contact_person = $_POST['contact_person'];
$email = $_POST['email'];
$phone = $_POST['phone'];
$address = $_POST['address'];
$vat_no = $_POST['vat_no'];
$stmt = $pdo->prepare("UPDATE suppliers SET name = ?, contact_person = ?, email = ?, phone = ?, address = ?, vat_no = ? WHERE id = ?");
if ($stmt->execute([$name, $contact_person, $email, $phone, $address, $vat_no, $id])) {
$stmt = $pdo->prepare("UPDATE suppliers SET name = ?, contact_person = ?, email = ?, phone = ?, address = ? WHERE id = ?");
$stmt->execute([$name, $contact_person, $email, $phone, $address, $id]);
$message = '<div class="alert alert-success">Supplier updated successfully!</div>';
} else {
$message = '<div class="alert alert-danger">Error updating supplier.</div>';
}
} elseif ($action === 'add_supplier') {
if (!has_permission('suppliers_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO suppliers (name, contact_person, email, phone, address) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$name, $contact_person, $email, $phone, $address]);
$message = '<div class="alert alert-success">Supplier created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
@ -60,8 +55,7 @@ if (isset($_GET['delete'])) {
}
}
// Fetch Suppliers
$query = "SELECT * FROM suppliers ORDER BY id DESC";
$query = "SELECT * FROM suppliers ORDER BY name ASC";
$suppliers_pagination = paginate_query($pdo, $query);
$suppliers = $suppliers_pagination['data'];
@ -71,7 +65,7 @@ include 'includes/header.php';
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Suppliers</h2>
<?php if (has_permission('suppliers_add')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#supplierModal" onclick="openAddModal()">
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-plus-lg"></i> Add Supplier
</button>
<?php endif; ?>
@ -89,43 +83,40 @@ include 'includes/header.php';
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Company Name</th>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Contact Person</th>
<th>Email / Phone</th>
<th>VAT No</th>
<th>Actions</th>
<th>Address</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($suppliers as $supplier): ?>
<tr>
<td class="ps-4 fw-bold"><?= htmlspecialchars($supplier['name']) ?></td>
<td><?= htmlspecialchars($supplier['contact_person']) ?></td>
<td class="ps-4 fw-medium">#<?= $supplier['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($supplier['name']) ?></td>
<td><?= htmlspecialchars($supplier['contact_person'] ?: '-') ?></td>
<td>
<div><?= htmlspecialchars($supplier['email']) ?></div>
<small class="text-muted"><?= htmlspecialchars($supplier['phone']) ?></small>
<div><?= htmlspecialchars($supplier['email'] ?: '-') ?></div>
<small class="text-muted"><?= htmlspecialchars($supplier['phone'] ?: '-') ?></small>
</td>
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars($supplier['vat_no']) ?></span></td>
<td>
<div class="btn-group">
<td><small class="text-muted"><?= htmlspecialchars($supplier['address'] ?: '-') ?></small></td>
<td class="text-end pe-4">
<?php if (has_permission('suppliers_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#supplierModal"
onclick="openEditModal(<?= htmlspecialchars(json_encode($supplier)) ?>)"
title="Edit Supplier"><i class="bi bi-pencil"></i></button>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= htmlspecialchars(json_encode($supplier), ENT_QUOTES, "UTF-8") ?>)' title="Edit"><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('suppliers_del')): ?>
<a href="?delete=<?= $supplier['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')" title="Delete"><i class="bi bi-trash"></i></a>
<a href="?delete=<?= $supplier['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this supplier?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($suppliers)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">No suppliers found.</td>
<td colspan="6" class="text-center py-4 text-muted">No suppliers found.</td>
</tr>
<?php endif; ?>
</tbody>
@ -140,42 +131,36 @@ include 'includes/header.php';
<!-- Supplier Modal -->
<?php if (has_permission('suppliers_add')): ?>
<div class="modal fade" id="supplierModal" tabindex="-1">
<div class="modal fade" id="supplierModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="supplierModalTitle">Add New Supplier</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="supplierForm">
<div class="modal-body">
<input type="hidden" name="action" id="supplierAction" value="add_supplier">
<input type="hidden" name="id" id="supplierId">
<div class="mb-3">
<label class="form-label">Company Name</label>
<label class="form-label">Supplier Name <span class="text-danger">*</span></label>
<input type="text" name="name" id="supplierName" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Contact Person</label>
<input type="text" name="contact_person" id="supplierContactPerson" class="form-control">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" id="supplierEmail" class="form-control">
</div>
<div class="col-md-6 mb-3">
<div class="mb-3">
<label class="form-label">Phone</label>
<input type="text" name="phone" id="supplierPhone" class="form-control">
</div>
</div>
<div class="mb-3">
<label class="form-label">VAT No</label>
<input type="text" name="vat_no" id="supplierVatNo" class="form-control" placeholder="e.g. GB123456789">
</div>
<div class="mb-3">
<label class="form-label">Address</label>
<textarea name="address" id="supplierAddress" class="form-control" rows="3"></textarea>
<textarea name="address" id="supplierAddress" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
@ -188,23 +173,38 @@ include 'includes/header.php';
</div>
<script>
function getSupplierModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('supplierModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function openAddModal() {
const modal = getSupplierModal();
if (!modal) return;
document.getElementById('supplierModalTitle').innerText = 'Add New Supplier';
document.getElementById('supplierAction').value = 'add_supplier';
document.getElementById('supplierForm').reset();
document.getElementById('supplierId').value = '';
modal.show();
}
function openEditModal(supplier) {
if (!supplier) return;
const modal = getSupplierModal();
if (!modal) return;
document.getElementById('supplierModalTitle').innerText = 'Edit Supplier';
document.getElementById('supplierAction').value = 'edit_supplier';
document.getElementById('supplierId').value = supplier.id;
document.getElementById('supplierName').value = supplier.name;
document.getElementById('supplierName').value = supplier.name || '';
document.getElementById('supplierContactPerson').value = supplier.contact_person || '';
document.getElementById('supplierEmail').value = supplier.email || '';
document.getElementById('supplierPhone').value = supplier.phone || '';
document.getElementById('supplierVatNo').value = supplier.vat_no || '';
document.getElementById('supplierAddress').value = supplier.address || '';
modal.show();
}
</script>
<?php endif; ?>

View File

@ -1,83 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = $_GET['id'] ?? null;
if (!$id) {
header('Location: tables.php');
exit;
}
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'] ?? '';
$area_id = $_POST['area_id'] ?? '';
$capacity = $_POST['capacity'] ?? 4;
if ($name && $area_id) {
$stmt = $pdo->prepare("UPDATE tables SET name = ?, area_id = ?, capacity = ? WHERE id = ?");
$stmt->execute([$name, $area_id, $capacity, $id]);
header('Location: tables.php');
exit;
}
}
// Fetch Table Details
$stmt = $pdo->prepare("SELECT * FROM tables WHERE id = ?");
$stmt->execute([$id]);
$table = $stmt->fetch();
if (!$table) {
die("Table not found.");
}
// Fetch Areas for Dropdown (with outlet names)
$areas = $pdo->query("SELECT areas.id, areas.name, outlets.name as outlet_name
FROM areas
JOIN outlets ON areas.outlet_id = outlets.id
ORDER BY outlets.name ASC, areas.name ASC")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Edit Table</h2>
<a href="tables.php" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label class="form-label">Table Name/Number</label>
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($table['name']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label">Area (Sorted by Outlet)</label>
<select name="area_id" class="form-select" required>
<option value="">Select Area</option>
<?php foreach ($areas as $area): ?>
<option value="<?= $area['id'] ?>" <?= $area['id'] == $table['area_id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($area['outlet_name']) ?> &raquo; <?= htmlspecialchars($area['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Capacity (Pax)</label>
<input type="number" name="capacity" class="form-control" value="<?= htmlspecialchars($table['capacity']) ?>" min="1" required>
</div>
<div class="d-flex justify-content-end gap-2">
<a href="tables.php" class="btn btn-light">Cancel</a>
<button type="submit" class="btn btn-primary">Update Table</button>
</div>
</form>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -6,59 +6,70 @@ $pdo = db();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_table') {
if (!has_permission('tables_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add tables.</div>';
// Handle Add/Edit Table
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$table_number = trim($_POST['table_number']);
$capacity = (int)$_POST['capacity'];
$area_id = (int)$_POST['area_id'];
$status = $_POST['status'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($table_number)) {
$message = '<div class="alert alert-danger">Table number is required.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO tables (area_id, name, capacity) VALUES (?, ?, ?)");
$stmt->execute([$_POST['area_id'], $_POST['name'], $_POST['capacity']]);
header("Location: tables.php");
exit;
try {
if ($action === 'edit_table' && $id) {
if (!has_permission('tables_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE tables SET table_number = ?, capacity = ?, area_id = ?, status = ? WHERE id = ?");
$stmt->execute([$table_number, $capacity, $area_id, $status, $id]);
$message = '<div class="alert alert-success">Table updated successfully!</div>';
}
} elseif ($action === 'add_table') {
if (!has_permission('tables_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO tables (table_number, capacity, area_id, status) VALUES (?, ?, ?, ?)");
$stmt->execute([$table_number, $capacity, $area_id, $status]);
$message = '<div class="alert alert-success">Table created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('tables_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete tables.</div>';
} else {
$pdo->prepare("DELETE FROM tables WHERE id = ?")->execute([$_GET['delete']]);
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM tables WHERE id = ?")->execute([$id]);
header("Location: tables.php");
exit;
}
}
// Fetch tables with area and outlet names
$query = "SELECT tables.*, areas.name as area_name, outlets.name as outlet_name
FROM tables
LEFT JOIN areas ON tables.area_id = areas.id
LEFT JOIN outlets ON areas.outlet_id = outlets.id
ORDER BY tables.id DESC";
$areas = $pdo->query("SELECT * FROM areas ORDER BY name ASC")->fetchAll();
$query = "SELECT t.*, a.name as area_name
FROM tables t
LEFT JOIN areas a ON t.area_id = a.id
ORDER BY a.name ASC, t.table_number ASC";
$tables_pagination = paginate_query($pdo, $query);
$tables = $tables_pagination['data'];
// Fetch areas for dropdown (with outlet names)
$areas = $pdo->query("SELECT areas.id, areas.name, outlets.name as outlet_name
FROM areas
JOIN outlets ON areas.outlet_id = outlets.id
ORDER BY outlets.name ASC, areas.name ASC")->fetchAll();
include 'includes/header.php';
// Determine base URL for QR codes
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'
|| $_SERVER['SERVER_PORT'] == 443;
$protocol = $isHttps ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'];
$dir = dirname($_SERVER['PHP_SELF'], 2);
$baseUrl = $protocol . $host . ($dir === '/' ? '' : $dir) . '/qorder.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Tables</h2>
<?php if (has_permission('tables_add')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addTableModal">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#tableModal" onclick="prepareAddForm()">
<i class="bi bi-plus-lg"></i> Add Table
</button>
<?php endif; ?>
@ -77,43 +88,45 @@ $baseUrl = $protocol . $host . ($dir === '/' ? '' : $dir) . '/qorder.php';
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Outlet / Area</th>
<th>Table Number</th>
<th>Area</th>
<th>Capacity</th>
<th>Actions</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($tables as $table):
$qrUrl = $baseUrl . '?table_id=' . $table['id'];
?>
<?php foreach ($tables as $table): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $table['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($table['name']) ?></td>
<td class="fw-bold"><?= htmlspecialchars($table['table_number']) ?></td>
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars($table['area_name'] ?: 'None') ?></span></td>
<td><?= $table['capacity'] ?> Persons</td>
<td>
<span class="badge bg-info text-dark"><?= htmlspecialchars($table['outlet_name'] ?? 'N/A') ?></span>
<span class="badge bg-secondary"><?= htmlspecialchars($table['area_name'] ?? 'N/A') ?></span>
<?php if ($table['status'] === 'available'): ?>
<span class="badge bg-success-subtle text-success px-3">Available</span>
<?php elseif ($table['status'] === 'occupied'): ?>
<span class="badge bg-danger-subtle text-danger px-3">Occupied</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary px-3"><?= ucfirst($table['status']) ?></span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($table['capacity']) ?> pax</td>
<td>
<button class="btn btn-sm btn-dark me-1"
onclick="showQR('<?= htmlspecialchars($table['name'], ENT_QUOTES) ?>', '<?= $qrUrl ?>')"
title="View QR Code">
<i class="bi bi-qr-code me-1"></i> QR
</button>
<td class="text-end pe-4">
<?php if (has_permission('tables_add')): ?>
<a href="table_edit.php?id=<?= $table['id'] ?>" class="btn btn-sm btn-outline-primary me-1" title="Edit"><i class="bi bi-pencil"></i></a>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
data-bs-toggle="modal" data-bs-target="#tableModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($table), ENT_QUOTES, "UTF-8") ?>)' title="Edit"><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('tables_del')): ?>
<a href="?delete=<?= $table['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this table?')" title="Delete"><i class="bi bi-trash"></i></a>
<a href="?delete=<?= $table['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this table?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($tables)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">No tables found. Add one to get started.</td>
<td colspan="6" class="text-center py-4 text-muted">No tables found.</td>
</tr>
<?php endif; ?>
</tbody>
@ -126,34 +139,44 @@ $baseUrl = $protocol . $host . ($dir === '/' ? '' : $dir) . '/qorder.php';
</div>
</div>
<!-- Add Table Modal -->
<!-- Table Modal -->
<?php if (has_permission('tables_add')): ?>
<div class="modal fade" id="addTableModal" tabindex="-1">
<div class="modal fade" id="tableModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Table</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="tableModalTitle">Add New Table</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST">
<form method="POST" id="tableForm">
<div class="modal-body">
<input type="hidden" name="action" value="add_table">
<input type="hidden" name="action" id="tableAction" value="add_table">
<input type="hidden" name="id" id="tableId">
<div class="mb-3">
<label class="form-label">Table Name/Number</label>
<input type="text" name="name" class="form-control" placeholder="e.g. T1, Window 5" required>
<label class="form-label">Table Number <span class="text-danger">*</span></label>
<input type="text" name="table_number" id="tableNumber" class="form-control" required placeholder="e.g. T1">
</div>
<div class="mb-3">
<label class="form-label">Area (Sorted by Outlet)</label>
<select name="area_id" class="form-select" required>
<label class="form-label">Area <span class="text-danger">*</span></label>
<select name="area_id" id="tableAreaId" class="form-select" required>
<option value="">Select Area</option>
<?php foreach ($areas as $area): ?>
<option value="<?= $area['id'] ?>"><?= htmlspecialchars($area['outlet_name']) ?> &raquo; <?= htmlspecialchars($area['name']) ?></option>
<option value="<?= $area['id'] ?>"><?= htmlspecialchars($area['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Capacity (Pax)</label>
<input type="number" name="capacity" class="form-control" value="4" min="1" required>
<label class="form-label">Capacity (Persons)</label>
<input type="number" name="capacity" id="tableCapacity" class="form-control" value="2" min="1">
</div>
<div class="mb-3">
<label class="form-label">Status</label>
<select name="status" id="tableStatus" class="form-select">
<option value="available">Available</option>
<option value="occupied">Occupied</option>
<option value="reserved">Reserved</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
<div class="modal-footer">
@ -164,53 +187,26 @@ $baseUrl = $protocol . $host . ($dir === '/' ? '' : $dir) . '/qorder.php';
</div>
</div>
</div>
<?php endif; ?>
<!-- QR Code Modal -->
<div class="modal fade" id="qrModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="qrModalTitle">Table QR Code</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<div id="qr-container" class="mb-3">
<img id="qr-image" src="" alt="QR Code" class="img-fluid border p-2 bg-white" onerror="this.src='https://placehold.co/300x300?text=QR+Error'">
</div>
<div id="qr-url" class="small text-muted text-break mb-3"></div>
<button class="btn btn-sm btn-outline-primary" onclick="printQR()">
<i class="bi bi-printer"></i> Print QR
</button>
</div>
</div>
</div>
</div>
<script>
function showQR(tableName, url) {
document.getElementById('qrModalTitle').textContent = 'QR Code - ' + tableName;
document.getElementById('qr-image').src = 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=' + encodeURIComponent(url);
document.getElementById('qr-url').textContent = url;
const modal = new bootstrap.Modal(document.getElementById('qrModal'));
modal.show();
function prepareAddForm() {
document.getElementById('tableModalTitle').innerText = 'Add New Table';
document.getElementById('tableAction').value = 'add_table';
document.getElementById('tableForm').reset();
document.getElementById('tableId').value = '';
}
function printQR() {
const img = document.getElementById('qr-image').src;
const title = document.getElementById('qrModalTitle').textContent;
const win = window.open('', '_blank');
const html = '<html><head><title>Print QR</title><style>' +
'body { text-align: center; font-family: sans-serif; padding: 40px; }' +
'img { width: 300px; height: 300px; border: 1px solid #ccc; padding: 10px; }' +
'h1 { margin-bottom: 20px; }</style></head>' +
'<body onload="window.print(); window.close();">' +
'<h1>' + title + '</h1>' +
'<img src="' + img + '">' +
'<p>Scan to order</p></body></html>';
win.document.write(html);
win.document.close();
function prepareEditForm(table) {
if (!table) return;
document.getElementById('tableModalTitle').innerText = 'Edit Table';
document.getElementById('tableAction').value = 'edit_table';
document.getElementById('tableId').value = table.id;
document.getElementById('tableNumber').value = table.table_number || '';
document.getElementById('tableAreaId').value = table.area_id || '';
document.getElementById('tableCapacity').value = table.capacity || 2;
document.getElementById('tableStatus').value = table.status || 'available';
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -1,311 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('all');
$id = $_GET['id'] ?? null;
$user = null;
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
header('Location: users.php');
exit;
}
} else {
// Default values for new user
$user = [
'id' => null,
'username' => '',
'full_name' => '',
'email' => '',
'group_id' => '',
'employee_id' => '',
'is_active' => 1,
'is_ratable' => 0,
'profile_pic' => '',
'created_at' => date('Y-m-d H:i:s')
];
}
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$full_name = $_POST['full_name'];
$username = $_POST['username'];
$email = $_POST['email'];
$group_id = $_POST['group_id'];
$employee_id = $_POST['employee_id'] ?? null;
$is_active = isset($_POST['is_active']) ? 1 : 0;
$is_ratable = isset($_POST['is_ratable']) ? 1 : 0;
$assigned_outlets = $_POST['outlets'] ?? [];
$password = $_POST['password'] ?? '';
// Validation
if (!$id && empty($password)) {
$message = '<div class="alert alert-danger">Password is required for new users.</div>';
} else {
// Check if username already exists
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ? AND id != ?");
$stmt->execute([$username, (int)$id]);
if ($stmt->fetch()) {
$message = '<div class="alert alert-danger">Username already taken.</div>';
}
}
if (!$message) {
$pdo->beginTransaction();
try {
if ($id) {
// Update
$sql = "UPDATE users SET full_name = ?, username = ?, email = ?, group_id = ?, is_active = ?, is_ratable = ?, employee_id = ? WHERE id = ?";
$params = [$full_name, $username, $email, $group_id, $is_active, $is_ratable, $employee_id, $id];
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
if (!empty($password)) {
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$pdo->prepare("UPDATE users SET password = ? WHERE id = ?")->execute([$hashed_password, $id]);
}
$user_id = $id;
} else {
// Insert
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$sql = "INSERT INTO users (full_name, username, email, group_id, is_active, is_ratable, employee_id, password) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
$params = [$full_name, $username, $email, $group_id, $is_active, $is_ratable, $employee_id, $hashed_password];
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$user_id = $pdo->lastInsertId();
}
// Update assigned outlets
$pdo->prepare("DELETE FROM user_outlets WHERE user_id = ?")->execute([$user_id]);
if (!empty($assigned_outlets)) {
$stmt_outlet = $pdo->prepare("INSERT INTO user_outlets (user_id, outlet_id) VALUES (?, ?)");
foreach ($assigned_outlets as $outlet_id) {
$stmt_outlet->execute([$user_id, $outlet_id]);
}
}
// Handle Profile Picture Upload
if (isset($_FILES['profile_pic']) && $_FILES['profile_pic']['error'] === UPLOAD_ERR_OK) {
$upload_dir = __DIR__ . '/../assets/images/users/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0775, true);
}
$file_tmp = $_FILES['profile_pic']['tmp_name'];
$file_name = $_FILES['profile_pic']['name'];
$file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
$allowed_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($file_ext, $allowed_exts)) {
$new_file_name = 'user_' . $user_id . '_' . uniqid() . '.' . $file_ext;
$upload_path = $upload_dir . $new_file_name;
if (move_uploaded_file($file_tmp, $upload_path)) {
// Delete old profile pic if exists
if ($user['profile_pic'] && file_exists(__DIR__ . '/../' . $user['profile_pic'])) {
unlink(__DIR__ . '/../' . $user['profile_pic']);
}
$profile_pic_path = 'assets/images/users/' . $new_file_name;
$pdo->prepare("UPDATE users SET profile_pic = ? WHERE id = ?")->execute([$profile_pic_path, $user_id]);
}
}
}
$pdo->commit();
if ($id) {
$message = '<div class="alert alert-success">User updated successfully!</div>';
} else {
header("Location: user_edit.php?id=$user_id&success=1");
exit;
}
// Refresh user data
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$pdo->rollBack();
$message = '<div class="alert alert-danger">Error saving user: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['success'])) {
$message = '<div class="alert alert-success">User created successfully!</div>';
}
$groups = $pdo->query("SELECT * FROM user_groups ORDER BY name")->fetchAll();
$all_outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll();
$assigned_outlet_ids = [];
if ($id) {
$user_outlets = $pdo->prepare("SELECT outlet_id FROM user_outlets WHERE user_id = ?");
$user_outlets->execute([$id]);
$assigned_outlet_ids = $user_outlets->fetchAll(PDO::FETCH_COLUMN);
}
include 'includes/header.php';
?>
<div class="mb-4">
<a href="users.php" class="text-decoration-none text-muted small"><i class="bi bi-arrow-left"></i> Back to Users</a>
<div class="d-flex align-items-center mt-2">
<h2 class="fw-bold mb-0"><?= $id ? 'Edit' : 'Add' ?> User<?= $user['username'] ? ': ' . htmlspecialchars($user['username']) : '' ?></h2>
</div>
</div>
<?= $message ?>
<div class="row">
<div class="col-md-8">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<form method="POST" enctype="multipart/form-data">
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">FULL NAME</label>
<input type="text" name="full_name" class="form-control" value="<?= htmlspecialchars($user['full_name']) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">USERNAME</label>
<input type="text" name="username" class="form-control" value="<?= htmlspecialchars($user['username']) ?>" required>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">EMAIL</label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($user['email']) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">EMPLOYEE / BIOMETRIC ID</label>
<input type="text" name="employee_id" class="form-control mb-3" value="<?= htmlspecialchars($user['employee_id'] ?? '') ?>" placeholder="e.g. 101">
<label class="form-label small fw-bold text-muted">USER GROUP / ROLE</label>
<select name="group_id" class="form-select" required>
<option value="">Select Group</option>
<?php foreach ($groups as $group): ?>
<option value="<?= $group['id'] ?>" <?= $user['group_id'] == $group['id'] ? 'selected' : '' ?>><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="row mb-4">
<div class="col-md-12">
<label class="form-label small fw-bold text-muted">PROFILE PICTURE</label>
<div class="d-flex align-items-center gap-3">
<?php if ($user['profile_pic']): ?>
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle shadow-sm" style="width: 80px; height: 80px; object-fit: cover;">
<?php else: ?>
<div class="bg-primary bg-gradient text-white rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 80px; height: 80px; font-weight: 700; font-size: 1.5rem;">
<?= strtoupper(substr($user['full_name'] ?: $user['username'] ?: 'U', 0, 1)) ?>
</div>
<?php endif; ?>
<div class="flex-grow-1">
<input type="file" name="profile_pic" class="form-control" accept="image/*">
<div class="form-text mt-1">Allowed: JPG, PNG, GIF, WebP. Recommended: Square image.</div>
</div>
</div>
</div>
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted"><?= $id ? 'NEW PASSWORD (LEAVE BLANK TO KEEP CURRENT)' : 'PASSWORD' ?></label>
<input type="password" name="password" class="form-control" placeholder="<?= $id ? '******' : 'Enter password' ?>" <?= $id ? '' : 'required' ?>>
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted d-block mb-2">ASSIGNED OUTLETS</label>
<div class="row">
<?php foreach ($all_outlets as $outlet): ?>
<div class="col-md-4 mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="outlets[]" value="<?= $outlet['id'] ?>" id="outlet_<?= $outlet['id'] ?>" <?= in_array($outlet['id'], $assigned_outlet_ids) ? 'checked' : '' ?>>
<label class="form-check-label small" for="outlet_<?= $outlet['id'] ?>">
<?= htmlspecialchars($outlet['name']) ?>
</label>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="form-text mt-1">Assign one or more outlets to this user.</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="isActiveSwitch" <?= $user['is_active'] ? 'checked' : '' ?>>
<label class="form-check-label fw-bold text-muted small" for="isActiveSwitch">ACTIVE ACCOUNT</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_ratable" id="isRatableSwitch" <?= $user['is_ratable'] ? 'checked' : '' ?>>
<label class="form-check-label fw-bold text-muted small" for="isRatableSwitch">DISPLAY IN STAFF RATINGS</label>
</div>
<div class="form-text small mt-1">Enable this to allow customers to rate this staff member in the public rating page.</div>
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-end gap-2">
<a href="users.php" class="btn btn-light rounded-pill px-4">Cancel</a>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold"><?= $id ? 'Update' : 'Create' ?> User</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm rounded-4 bg-primary bg-gradient text-white shadow">
<div class="card-body p-4">
<h5 class="fw-bold mb-3">User Info</h5>
<div class="d-flex align-items-center mb-3">
<?php if ($user['profile_pic']): ?>
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle shadow-sm border border-2 border-white me-3" style="width: 60px; height: 60px; object-fit: cover;">
<?php else: ?>
<div class="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center me-3" style="width:60px;height:60px; font-weight:700; font-size:1.5rem;">
<?= strtoupper(substr($user['full_name'] ?: $user['username'] ?: 'U', 0, 1)) ?>
</div>
<?php endif; ?>
<div>
<div class="fw-bold"><?= htmlspecialchars($user['full_name'] ?: 'New User') ?></div>
<div class="small opacity-75">Member since <?= date('M Y', strtotime($user['created_at'])) ?></div>
</div>
</div>
<div class="small">
<div class="mb-1"><i class="bi bi-envelope me-2"></i> <?= htmlspecialchars($user['email'] ?: 'Email not set') ?></div>
<div><i class="bi bi-person-badge me-2"></i> User ID: <?= $id ? '#' . $id : '<i>New</i>' ?></div>
</div>
<hr class="opacity-25 my-3">
<div class="small">
<div class="fw-bold mb-2">Assigned Outlets:</div>
<?php if (empty($assigned_outlet_ids)): ?>
<div class="opacity-75">No outlets assigned.</div>
<?php else: ?>
<ul class="list-unstyled mb-0 opacity-75">
<?php foreach ($all_outlets as $outlet): ?>
<?php if (in_array($outlet['id'], $assigned_outlet_ids)): ?>
<li><i class="bi bi-geo-alt me-1"></i> <?= htmlspecialchars($outlet['name']) ?></li>
<?php endif; ?>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -46,13 +46,18 @@ $modules = [
'areas' => 'Areas',
'tables' => 'Tables',
'suppliers' => 'Suppliers',
'purchases' => 'Purchases',
'expenses' => 'Expenses',
'expense_categories' => 'Expense Categories',
'payment_types' => 'Payment Types',
'loyalty' => 'Loyalty',
'ads' => 'Ads',
'reports' => 'Reports',
'users' => 'Users',
'user_groups' => 'User Groups',
'settings' => 'Settings'
'settings' => 'Settings',
'attendance' => 'Attendance',
'ratings' => 'Staff Ratings'
];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_group') {
@ -88,7 +93,13 @@ 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 class="d-flex justify-content-between align-items-center mt-2">
<h2 class="fw-bold">Edit Group Permissions: <?= htmlspecialchars($group['name']) ?></h2>
<div>
<button type="button" class="btn btn-outline-primary btn-sm rounded-pill" onclick="toggleAll(true)">Select All</button>
<button type="button" class="btn btn-outline-secondary btn-sm rounded-pill" onclick="toggleAll(false)">Deselect All</button>
</div>
</div>
</div>
<?= $message ?>
@ -112,9 +123,10 @@ include 'includes/header.php';
<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">Add</th>
<th class="text-center py-3">Edit</th>
<th class="text-center py-3">Delete</th>
<th class="text-center py-3">Full Access</th>
<th class="text-center py-3">Actions</th>
</tr>
</thead>
<tbody>
@ -131,6 +143,11 @@ include 'includes/header.php';
<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 ?>_edit" id="perm_<?= $key ?>_edit" <?= (in_array($key . '_edit', $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' : '' ?>>
@ -149,7 +166,7 @@ include 'includes/header.php';
<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 colspan="4" 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' : '' ?>>
@ -175,9 +192,21 @@ include 'includes/header.php';
<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;
const view = document.getElementById('perm_' + key + '_view');
const add = document.getElementById('perm_' + key + '_add');
const edit = document.getElementById('perm_' + key + '_edit');
const del = document.getElementById('perm_' + key + '_del');
if(view) view.checked = state;
if(add) add.checked = state;
if(edit) edit.checked = state;
if(del) del.checked = state;
}
function toggleAll(state) {
document.querySelectorAll('.perm-checkbox').forEach(cb => {
if (!cb.disabled) cb.checked = state;
});
}
// If Super Admin is checked, maybe disable others? Or just let them be.

View File

@ -1,21 +1,46 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("user_groups_view");
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('user_groups_view');
$message = '';
// Handle New Group Creation
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_user_group') {
if (!has_permission('user_groups_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$name = trim($_POST['name']);
if (empty($name)) {
$message = '<div class="alert alert-danger">Group name is required.</div>';
} else {
try {
$stmt = $pdo->prepare("INSERT INTO user_groups (name, permissions) VALUES (?, '')");
$stmt->execute([$name]);
$newId = $pdo->lastInsertId();
header("Location: user_group_edit.php?id=" . $newId);
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
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>';
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete user 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>';
// Don't allow deleting Administrator group
$stmt = $pdo->prepare("SELECT name FROM user_groups WHERE id = ?");
$stmt->execute([$id]);
$groupName = $stmt->fetchColumn();
if ($groupName === 'Administrator') {
$message = '<div class="alert alert-danger">The Administrator group cannot be deleted.</div>';
} else {
$pdo->prepare("DELETE FROM user_groups WHERE id = ?")->execute([$id]);
header("Location: user_groups.php");
@ -24,105 +49,158 @@ if (isset($_GET['delete'])) {
}
}
// 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);
$availablePermissions = [
'dashboard' => 'Dashboard',
'pos' => 'POS Terminal',
'orders' => 'Orders',
'kitchen' => 'Kitchen View',
'products' => 'Products',
'categories' => 'Categories',
'customers' => 'Customers',
'outlets' => 'Outlets',
'areas' => 'Areas',
'tables' => 'Tables',
'suppliers' => 'Suppliers',
'purchases' => 'Purchases',
'expenses' => 'Expenses',
'expense_categories' => 'Expense Categories',
'payment_types' => 'Payment Types',
'loyalty' => 'Loyalty',
'ads' => 'Ads',
'reports' => 'Reports',
'users' => 'Users',
'user_groups' => 'User Groups',
'settings' => 'Settings',
'attendance' => 'Attendance',
'ratings' => 'Staff Ratings'
];
$query = "SELECT * FROM user_groups ORDER BY id ASC";
$groups_pagination = paginate_query($pdo, $query);
$groups = $groups_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">User Groups / Roles</h2>
<p class="text-muted mb-0">Define permissions and access levels</p>
<h2 class="fw-bold mb-0">User Roles & Groups</h2>
<p class="text-muted mb-0">Manage permissions and access levels</p>
</div>
<?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 class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addGroupModal">
<i class="bi bi-plus-lg"></i> Add Group
</button>
<?php endif; ?>
</div>
<?= $message ?>
<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 class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($groups_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>Group Name</th>
<th>Permissions Summary</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td class="ps-4 fw-medium text-muted">#<?= $group['id'] ?></td>
<td class="fw-bold text-dark"><?= htmlspecialchars($group['name']) ?></td>
<td>
<?php
if ($group['permissions'] === 'all') {
echo '<span class="badge bg-danger-subtle text-danger border border-danger">Super Admin (All)</span>';
} else {
$perms = explode(',', $group['permissions']);
$modules_found = [];
foreach ($perms as $p) {
$mod = explode('_', $p)[0];
if (isset($availablePermissions[$mod]) && !in_array($availablePermissions[$mod], $modules_found)) {
$modules_found[] = $availablePermissions[$mod];
}
}
if (count($modules_found) > 0) {
echo '<div class="d-flex flex-wrap gap-1">';
$i = 0;
foreach ($modules_found as $m) {
if ($i < 5) {
echo '<span class="badge bg-light text-dark border small">' . $m . '</span>';
}
$i++;
}
if (count($modules_found) > 5) {
echo '<span class="badge bg-light text-muted border small">+' . (count($modules_found) - 5) . ' more</span>';
}
echo '</div>';
} elseif (!empty($group['permissions'])) {
echo '<small class="text-muted">' . htmlspecialchars(substr($group['permissions'], 0, 30)) . '...</small>';
} else {
echo '<small class="text-muted">No permissions defined</small>';
}
}
?>
</td>
<td class="text-end pe-4">
<?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 href="user_group_edit.php?id=<?= $group['id'] ?>" class="btn btn-sm btn-outline-primary rounded-pill px-3 me-1" title="Manage Permissions">
<i class="bi bi-shield-lock me-1"></i> Permissions
</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 if (has_permission('user_groups_del') && $group['name'] !== 'Administrator'): ?>
<a href="?delete=<?= $group['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Delete this user group?')">
<i class="bi bi-trash me-1"></i> Delete
</a>
<?php endif; ?>
</td>
</tr>
<?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; ?>
</tbody>
</table>
</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; ?>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($groups_pagination); ?>
</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 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 class="modal fade" id="addGroupModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">Create New Group</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="user_group_edit.php" method="POST">
<div class="modal-body p-4">
<form method="POST">
<div class="modal-body py-4">
<input type="hidden" name="action" value="add_user_group">
<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 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>
<input type="text" name="name" class="form-control form-control-lg border-0 bg-light rounded-3" required placeholder="e.g. Supervisor" autofocus>
</div>
<p class="text-muted small mb-0">After creating, you will be redirected to the permissions page to configure access levels.</p>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<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 class="modal-footer border-0 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 shadow-sm">Create & Configure</button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -1,38 +1,107 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("users_view");
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('users_view');
$message = '';
// Handle Add/Edit User
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$username = trim($_POST['username']);
$full_name = trim($_POST['full_name']);
$email = trim($_POST['email']);
$group_id = (int)$_POST['group_id'];
$is_active = isset($_POST['is_active']) ? 1 : 0;
$is_ratable = isset($_POST['is_ratable']) ? 1 : 0;
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$profile_pic = null;
if ($id) {
$stmt = $pdo->prepare("SELECT profile_pic FROM users WHERE id = ?");
$stmt->execute([$id]);
$profile_pic = $stmt->fetchColumn();
}
if (isset($_FILES['profile_pic']) && $_FILES['profile_pic']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/users/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$file_ext = strtolower(pathinfo($_FILES['profile_pic']['name'], PATHINFO_EXTENSION));
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$new_file_name = 'user_' . ($id ?: uniqid()) . '_' . uniqid() . '.' . $file_ext;
if (move_uploaded_file($_FILES['profile_pic']['tmp_name'], $uploadDir . $new_file_name)) {
$profile_pic = 'assets/images/users/' . $new_file_name;
}
}
}
if (empty($username)) {
$message = '<div class="alert alert-danger">Username is required.</div>';
} else {
try {
if ($action === 'edit_user' && $id) {
if (!has_permission('users_edit') && !has_permission('users_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit users.</div>';
} else {
$sql = "UPDATE users SET username = ?, full_name = ?, email = ?, group_id = ?, is_active = ?, is_ratable = ?, profile_pic = ? WHERE id = ?";
$params = [$username, $full_name, $email, $group_id, $is_active, $is_ratable, $profile_pic, $id];
if (!empty($_POST['password'])) {
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$sql = "UPDATE users SET username = ?, full_name = ?, email = ?, group_id = ?, is_active = ?, is_ratable = ?, profile_pic = ?, password = ? WHERE id = ?";
$params = [$username, $full_name, $email, $group_id, $is_active, $is_ratable, $profile_pic, $password, $id];
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$message = '<div class="alert alert-success">User updated successfully!</div>';
}
} elseif ($action === 'add_user') {
if (!has_permission('users_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add users.</div>';
} else {
$password = password_hash($_POST['password'] ?: '123456', PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password, full_name, email, group_id, is_active, is_ratable, profile_pic) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$username, $password, $full_name, $email, $group_id, $is_active, $is_ratable, $profile_pic]);
$message = '<div class="alert alert-success">User created successfully!</div>';
}
}
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$message = '<div class="alert alert-danger">Username or Email already exists.</div>';
} else {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
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>';
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete users.</div>';
} else {
$id = $_GET['delete'];
// Don't allow deleting current user
if ($id == $_SESSION['user']['id']) {
$message = '<div class="alert alert-danger text-center">You cannot delete your own account.</div>';
} else {
$pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$id]);
header("Location: users.php");
exit;
}
}
// 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 ? OR u.employee_id LIKE ?";
$params = ["%$search%", "%$search%", "%$search%", "%$search%"];
}
$query .= " ORDER BY u.id DESC";
$users_pagination = paginate_query($pdo, $query, $params);
$groups = $pdo->query("SELECT * FROM user_groups ORDER BY name ASC")->fetchAll();
$query = "SELECT u.*, g.name as group_name
FROM users u
LEFT JOIN user_groups g ON u.group_id = g.id
ORDER BY u.id DESC";
$users_pagination = paginate_query($pdo, $query);
$users = $users_pagination['data'];
include 'includes/header.php';
@ -40,45 +109,33 @@ include 'includes/header.php';
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">Users</h2>
<p class="text-muted mb-0">Manage system staff and access</p>
<h2 class="fw-bold mb-1">User Management</h2>
<p class="text-muted mb-0">Manage staff accounts, roles and access permissions</p>
</div>
<?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>
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#userModal" onclick="prepareAddForm()" style="border-radius: 12px;">
<i class="bi bi-plus-lg"></i> Add New User
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<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) ?>">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($users_pagination); ?>
</div>
</div>
<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>Emp. ID</th>
<th>Email</th>
<th>Ratable</th>
<th>Role / Group</th>
<th>Status</th>
<th>Joined</th>
<th>Ratable</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
@ -88,61 +145,174 @@ include 'includes/header.php';
<td class="ps-4">
<div class="d-flex align-items-center">
<?php if ($user['profile_pic']): ?>
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>?v=<?= time() ?>" alt="Profile" class="rounded-circle me-3 shadow-sm border border-2 border-white" style="width: 45px; height: 45px; object-fit: cover;">
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>" class="rounded-circle me-3 border border-2 border-white shadow-sm" width="48" height="48" style="object-fit: cover;">
<?php else: ?>
<div class="rounded-circle bg-primary bg-opacity-10 text-primary d-flex align-items-center justify-content-center me-3 shadow-sm" style="width: 45px; height: 45px; font-weight: 600;">
<?= strtoupper(substr($user['username'], 0, 1)) ?>
</div>
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center text-white me-3 shadow-sm" style="width:48px;height:48px; font-weight:700; font-size: 1.2rem;"><?= strtoupper(substr($user['username'],0,1)) ?></div>
<?php endif; ?>
<div>
<div class="fw-bold text-dark"><?= htmlspecialchars($user['full_name'] ?: $user['username']) ?></div>
<div class="text-muted small">@<?= htmlspecialchars($user['username']) ?></div>
<div class="fw-bold text-dark fs-6"><?= htmlspecialchars($user['full_name'] ?: $user['username']) ?></div>
<small class="text-muted fw-medium">@<?= htmlspecialchars($user['username']) ?></small>
</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['employee_id'] ?: '-') ?></td>
<td><?= htmlspecialchars($user['email'] ?: '-') ?></td>
<td>
<?php if ($user['is_ratable']): ?>
<span class="badge bg-warning bg-opacity-10 text-warning border border-warning border-opacity-25 px-2 rounded-pill"><i class="bi bi-star-fill small"></i> Yes</span>
<?php else: ?>
<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25 px-2 rounded-pill">No</span>
<?php endif; ?>
</td>
<td><span class="badge bg-light text-dark border px-2 py-1"><?= htmlspecialchars($user['group_name'] ?: 'None') ?></span></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>
<span class="badge bg-success-subtle text-success px-3 py-1 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>
<span class="badge bg-danger-subtle text-danger px-3 py-1 rounded-pill">Inactive</span>
<?php endif; ?>
</td>
<td>
<?php if ($user['is_ratable']): ?>
<span class="badge bg-info-subtle text-info px-3 py-1 rounded-pill">Yes</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary px-3 py-1 rounded-pill">No</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 if (has_permission('users_edit') || has_permission('users_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3 me-1"
data-bs-toggle="modal" data-bs-target="#userModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($user), ENT_QUOTES, "UTF-8") ?>)' title="Edit Profile">
<i class="bi bi-pencil me-1"></i> Edit</button>
<?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>
<a href="?delete=<?= $user['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Permanently delete this user account?')">
<i class="bi bi-trash"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($users)): ?>
<tr>
<td colspan="6" class="text-center py-5 text-muted">No users found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="p-3 border-top">
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($users_pagination); ?>
</div>
</div>
</div>
<!-- User Modal -->
<?php if (has_permission('users_add') || has_permission('users_edit')): ?>
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="userModalTitle">Add New User</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="userForm" enctype="multipart/form-data">
<div class="modal-body p-4">
<input type="hidden" name="action" id="userAction" value="add_user">
<input type="hidden" name="id" id="userId">
<div class="mb-4 text-center" id="userImagePreviewContainer" style="display: none;">
<img src="" id="userImagePreview" class="img-fluid rounded-circle border border-4 border-white shadow mb-2" style="width: 100px; height: 100px; object-fit: cover;">
<p class="small text-muted mb-0">Current Profile Picture</p>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">FULL NAME</label>
<input type="text" name="full_name" id="userFullName" class="form-control rounded-3 border-0 bg-light" required>
</div>
<div class="row g-3">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">USERNAME <span class="text-danger">*</span></label>
<input type="text" name="username" id="userUsername" class="form-control rounded-3 border-0 bg-light" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">EMAIL</label>
<input type="email" name="email" id="userEmail" class="form-control rounded-3 border-0 bg-light">
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">PASSWORD <span id="pwdLabel" class="text-danger">*</span></label>
<input type="password" name="password" id="userPassword" class="form-control rounded-3 border-0 bg-light" placeholder="Min. 6 characters">
<small class="text-muted" id="pwdHint" style="display:none;">Leave blank to keep the current password.</small>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">ROLE / USER GROUP <span class="text-danger">*</span></label>
<select name="group_id" id="userGroupId" class="form-select rounded-3 border-0 bg-light" required>
<option value="">Select Group</option>
<?php foreach ($groups as $group): ?>
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted">PROFILE PICTURE</label>
<input type="file" name="profile_pic" id="userProfilePicFile" class="form-control rounded-3 border-0 bg-light" accept="image/*">
</div>
<div class="row bg-light p-3 rounded-4 g-2">
<div class="col-6">
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" name="is_active" id="userIsActive" checked>
<label class="form-check-label fw-bold small" for="userIsActive">Active Account</label>
</div>
</div>
<div class="col-6">
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" name="is_ratable" id="userIsRatable" checked>
<label class="form-check-label fw-bold small" for="userIsRatable">Ratable Staff</label>
</div>
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Save User Profile</button>
</div>
</form>
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('userModalTitle').innerText = 'Add New User';
document.getElementById('userAction').value = 'add_user';
document.getElementById('userForm').reset();
document.getElementById('userId').value = '';
document.getElementById('pwdLabel').style.display = 'inline';
document.getElementById('pwdHint').style.display = 'none';
document.getElementById('userPassword').required = true;
document.getElementById('userImagePreviewContainer').style.display = 'none';
}
function prepareEditForm(user) {
if (!user) return;
document.getElementById('userModalTitle').innerText = 'Edit User: ' + (user.full_name || user.username);
document.getElementById('userAction').value = 'edit_user';
document.getElementById('userId').value = user.id;
document.getElementById('userFullName').value = user.full_name || '';
document.getElementById('userUsername').value = user.username || '';
document.getElementById('userEmail').value = user.email || '';
document.getElementById('userGroupId').value = user.group_id || '';
document.getElementById('userIsActive').checked = user.is_active == 1;
document.getElementById('userIsRatable').checked = user.is_ratable == 1;
document.getElementById('userPassword').required = false;
document.getElementById('pwdLabel').style.display = 'none';
document.getElementById('pwdHint').style.display = 'block';
if (user.profile_pic) {
const preview = document.getElementById('userImagePreview');
preview.src = '../' + user.profile_pic;
document.getElementById('userImagePreviewContainer').style.display = 'block';
} else {
document.getElementById('userImagePreviewContainer').style.display = 'none';
}
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -126,7 +126,10 @@ try {
$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;
// VAT vs Discount: We repurpose the 'discount' column in the database to store VAT value.
// If it's a positive value, it's VAT. If negative, it acts as a discount (loyalty).
$vat = isset($data['vat']) ? floatval($data['vat']) : 0.00;
// Total amount will be recalculated on server to be safe
$calculated_total = 0;
@ -172,7 +175,7 @@ try {
}
}
$final_total = max(0, $calculated_total - $discount);
$final_total = max(0, $calculated_total + $vat);
// Check for Existing Order ID (Update Mode)
$order_id = isset($data['order_id']) ? intval($data['order_id']) : null;
@ -198,18 +201,16 @@ try {
$stmt->execute([
$outlet_id, $table_id, $table_number, $order_type,
$customer_id, $customer_name, $customer_phone,
$payment_type_id, $final_total, $discount, $user_id,
$payment_type_id, $final_total, $vat, $user_id,
$order_id
]);
// Clear existing items and revert stock (if you want to be precise)
// For simplicity, we'll just handle stock for new items and assume updates are for current session.
$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, $final_total, $discount, $user_id]);
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $payment_type_id, $final_total, $vat, $user_id]);
$order_id = $pdo->lastInsertId();
}

View File

@ -17,7 +17,7 @@ document.addEventListener('DOMContentLoaded', () => {
const cartItemsContainer = document.getElementById('cart-items');
const cartTotalPrice = document.getElementById('cart-total-price');
const cartSubtotal = document.getElementById('cart-subtotal');
const cartDiscountInput = document.getElementById('cart-discount-input');
const cartVatInput = document.getElementById('cart-vat-input');
// Updated Button References
const quickOrderBtn = document.getElementById('quick-order-btn');
@ -188,7 +188,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Populate Cart
cart = data.items; // Assuming format matches
cartDiscountInput.value = data.order.discount || 0;
cartVatInput.value = data.order.discount || 0; // We still use the discount field for VAT value
updateCart();
if (recallModal) recallModal.hide();
@ -287,7 +287,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Hide Loyalty
if (loyaltySection) loyaltySection.classList.add('d-none');
isLoyaltyRedemption = false;
cartDiscountInput.value = 0;
cartVatInput.value = 0;
updateCart();
customerSearchInput.focus();
@ -324,9 +324,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (result.isConfirmed) {
isLoyaltyRedemption = true;
// Calculate total and apply as discount
// Calculate total and apply as discount (which is now negative VAT internally)
const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0);
cartDiscountInput.value = subtotal.toFixed(2);
cartVatInput.value = -subtotal.toFixed(2);
updateCart();
showToast("Loyalty Redemption Applied!", "success");
@ -555,13 +555,14 @@ document.addEventListener('DOMContentLoaded', () => {
cartSubtotal.innerText = formatCurrency(subtotal);
let discount = parseFloat(cartDiscountInput.value) || 0;
let vat = parseFloat(cartVatInput.value) || 0;
if (isLoyaltyRedemption) {
discount = subtotal;
cartDiscountInput.value = subtotal.toFixed(2);
// Internal trick: send negative VAT to represent discount for loyalty
vat = -subtotal;
cartVatInput.value = (-subtotal).toFixed(2);
}
let total = subtotal - discount;
let total = subtotal + vat;
if (total < 0) total = 0;
cartTotalPrice.innerText = formatCurrency(total);
@ -569,8 +570,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (placeOrderBtn) placeOrderBtn.disabled = false;
}
if (cartDiscountInput) {
cartDiscountInput.addEventListener('input', () => {
if (cartVatInput) {
cartVatInput.addEventListener('input', () => {
updateCart();
});
}
@ -593,7 +594,7 @@ document.addEventListener('DOMContentLoaded', () => {
}).then((result) => {
if (result.isConfirmed) {
cart = [];
cartDiscountInput.value = 0;
cartVatInput.value = 0;
currentOrderId = null;
isLoyaltyRedemption = false;
updateCart();
@ -683,8 +684,8 @@ document.addEventListener('DOMContentLoaded', () => {
const orderTypeInput = document.querySelector('input[name="order_type"]:checked');
const orderType = orderTypeInput ? orderTypeInput.value : 'takeaway';
const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0);
const discount = parseFloat(cartDiscountInput.value) || 0;
const totalAmount = Math.max(0, subtotal - discount);
const vat = parseFloat(cartVatInput.value) || 0;
const totalAmount = Math.max(0, subtotal + vat);
const custId = selectedCustomerId.value;
const orderData = {
@ -695,7 +696,7 @@ document.addEventListener('DOMContentLoaded', () => {
outlet_id: new URLSearchParams(window.location.search).get('outlet_id') || 1,
payment_type_id: paymentTypeId,
total_amount: totalAmount,
discount: discount,
vat: vat, // Send as vat
redeem_loyalty: isLoyaltyRedemption,
items: cart.map(item => ({
product_id: item.id,
@ -736,7 +737,7 @@ document.addEventListener('DOMContentLoaded', () => {
customer: currentCustomer,
items: [...cart],
total: totalAmount,
discount: discount,
vat: vat,
orderType: orderType,
tableNumber: (orderType === 'dine-in') ? currentTableName : null,
date: new Date().toLocaleString(),
@ -745,7 +746,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
cart = [];
cartDiscountInput.value = 0;
cartVatInput.value = 0;
currentOrderId = null; // Reset
isLoyaltyRedemption = false; // Reset
updateCart();
@ -784,8 +785,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (!currentOrderId) return;
const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0);
const discount = parseFloat(cartDiscountInput.value) || 0;
const totalAmount = Math.max(0, subtotal - discount);
const vat = parseFloat(cartVatInput.value) || 0;
const totalAmount = Math.max(0, subtotal + vat);
const orderType = document.querySelector('input[name="order_type"]:checked').value;
printReceipt({
@ -793,7 +794,7 @@ document.addEventListener('DOMContentLoaded', () => {
customer: currentCustomer,
items: [...cart],
total: totalAmount,
discount: discount,
vat: vat,
orderType: orderType,
tableNumber: (orderType === 'dine-in') ? currentTableName : null,
date: new Date().toLocaleString() + ' (Reprint)',
@ -821,7 +822,7 @@ document.addEventListener('DOMContentLoaded', () => {
'ITEM': 'الصنف',
'TOTAL': 'المجموع',
'Subtotal': 'المجموع الفرعي',
'Discount': 'الخصم',
'VAT': 'ضريبة القيمة المضافة',
'Tax Included': 'شامل الضريبة',
'THANK YOU FOR YOUR VISIT!': 'شكراً لزيارتكم!',
'Please come again.': 'يرجى زيارتنا مرة أخرى.',
@ -872,9 +873,9 @@ document.addEventListener('DOMContentLoaded', () => {
const loyaltyHtml = data.loyaltyRedeemed ? `<div style="color: #d63384; font-weight: bold; margin: 5px 0; text-align: center;">* Loyalty Reward Applied *</div>` : '';
const vatRate = parseFloat(settings.vat_rate) || 0;
const subtotal = data.total + data.discount;
const vatAmount = vatRate > 0 ? (data.total * (vatRate / (100 + vatRate))) : 0;
const subtotal = data.total - data.vat;
const vatRateSettings = parseFloat(settings.vat_rate) || 0;
const vatAmount = data.vat; // User manually entered VAT
const logoHtml = settings.logo_url ? `<img src="${BASE_URL}${settings.logo_url}" style="max-height: 80px; max-width: 150px; margin-bottom: 10px;">` : '';
@ -971,15 +972,10 @@ document.addEventListener('DOMContentLoaded', () => {
<td>Subtotal / ${tr['Subtotal']}</td>
<td style="text-align: right">${formatCurrency(subtotal)}</td>
</tr>
${data.discount > 0 ? `
${Math.abs(data.vat) > 0 ? `
<tr>
<td>Discount / ${tr['Discount']}</td>
<td style="text-align: right">-${formatCurrency(data.discount)}</td>
</tr>` : ''}
${vatRate > 0 ? `
<tr>
<td style="font-size: 11px;">Tax Incl. (${vatRate}%) / ${tr['Tax Included']}</td>
<td style="text-align: right">${formatCurrency(vatAmount)}</td>
<td>${data.vat < 0 ? 'Discount' : 'VAT'} / ${tr['VAT']}</td>
<td style="text-align: right">${data.vat < 0 ? '-' : '+'}${formatCurrency(Math.abs(data.vat))}</td>
</tr>` : ''}
<tr style="font-weight: bold; font-size: 18px;">
<td style="padding-top: 10px;">TOTAL / ${tr['TOTAL']}</td>
@ -996,7 +992,7 @@ document.addEventListener('DOMContentLoaded', () => {
<div>Please come again.</div>
<div class="rtl">${tr['Please come again.']}</div>
${settings.email ? `<div style="margin-top: 5px; font-size: 10px;">${settings.email}</div>` : ''}
<div style="margin-top: 15px; font-size: 10px; color: #888;">Powered by Flatlogic POS</div>
<div style="margin-top: 15px; font-size: 10px; color: #888;">Powered by Abidarcafe</div>
</div>
<script>

View File

@ -0,0 +1,2 @@
ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS auto_backup_enabled TINYINT(1) DEFAULT 0;
ALTER TABLE company_settings ADD COLUMN IF NOT EXISTS last_auto_backup TIMESTAMP NULL DEFAULT NULL;

View File

@ -305,3 +305,61 @@ function get_base_url() {
return $protocol . $domainName . $script_dir . '/';
}
/**
* Backup functions
*/
function create_backup() {
$backupDir = __DIR__ . '/../storage/backups/';
if (!is_dir($backupDir)) {
mkdir($backupDir, 0777, true);
}
$filename = 'backup_' . date('Y-m-d_H-i-s') . '.sql';
$path = $backupDir . $filename;
$command = sprintf(
'mysqldump -h %s -u %s -p%s %s > %s',
escapeshellarg(DB_HOST),
escapeshellarg(DB_USER),
escapeshellarg(DB_PASS),
escapeshellarg(DB_NAME),
escapeshellarg($path)
);
exec($command, $output, $returnVar);
if ($returnVar === 0) {
// Enforce retention: keep 5 latest
$files = glob($backupDir . '/*.sql');
if (count($files) > 5) {
usort($files, function($a, $b) {
return filemtime($a) - filemtime($b);
});
while (count($files) > 5) {
$oldest = array_shift($files);
unlink($oldest);
}
}
return $filename;
}
return false;
}
function trigger_auto_backup() {
$settings = get_company_settings();
if (empty($settings['auto_backup_enabled'])) return;
$lastBackup = !empty($settings['last_auto_backup']) ? strtotime($settings['last_auto_backup']) : 0;
$now = time();
// Run once every 24 hours
if ($now - $lastBackup > 86400) {
if (create_backup()) {
$pdo = db();
$stmt = $pdo->prepare("UPDATE company_settings SET last_auto_backup = NOW(), updated_at = NOW() LIMIT 1");
$stmt->execute();
}
}
}

26
pos.php
View File

@ -85,18 +85,22 @@ if (!$loyalty_settings) {
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
body { height: 100vh; overflow: hidden; } /* Fix body for scrolling areas */
body { height: 100vh; overflow: hidden; font-family: 'Inter', sans-serif; } /* Fix body for scrolling areas */
.scrollable-y { overflow-y: auto; height: 100%; scrollbar-width: thin; }
.category-sidebar { height: calc(100vh - 60px); background: #f8f9fa; }
.product-area { height: calc(100vh - 60px); background: #fff; }
.cart-sidebar { height: calc(100vh - 60px); background: #fff; border-left: 1px solid #dee2e6; display: flex; flex-direction: column; }
.product-card { transition: transform 0.1s; cursor: pointer; }
.product-card:active { transform: scale(0.98); }
.category-btn { text-align: left; border: none; background: none; padding: 10px 15px; width: 100%; display: block; border-radius: 8px; color: #333; font-weight: 500; }
.category-btn { text-align: left; border: none; background: none; padding: 10px 15px; width: 100%; display: block; border-radius: 8px; color: #333; font-weight: 500; font-size: 0.95rem; }
.category-btn:hover { background-color: #e9ecef; }
.category-btn.active { background-color: #0d6efd; color: white; }
.search-dropdown { position: absolute; width: 100%; z-index: 1000; max-height: 200px; overflow-y: auto; display: none; }
.payment-btn { height: 80px; font-size: 1.2rem; font-weight: bold; }
.payment-btn { height: 70px; font-size: 1.05rem; font-weight: bold; }
.btn-lg { font-size: 1.1rem; padding: 0.75rem 1rem; }
.btn-sm { font-size: 0.85rem; }
.navbar-brand { font-size: 1.15rem; }
.card-title { font-size: 0.9rem; }
</style>
</head>
<body>
@ -119,12 +123,6 @@ if (!$loyalty_settings) {
<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; ?>
<button class="btn btn-sm btn-outline-warning" onclick="showRatingQR()">
<i class="bi bi-qr-code"></i> Rating QR
</button>
@ -319,12 +317,12 @@ if (!$loyalty_settings) {
<span class="fw-bold" id="cart-subtotal"><?= format_currency(0) ?></span>
</div>
<!-- Discount Field -->
<!-- VAT Field -->
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">Discount</span>
<span class="text-muted">VAT</span>
<div class="input-group input-group-sm w-50">
<span class="input-group-text bg-white border-end-0 text-muted">-</span>
<input type="number" id="cart-discount-input" class="form-control border-start-0 text-end" value="0" min="0" step="0.01">
<span class="input-group-text bg-white border-end-0 text-muted">+</span>
<input type="number" id="cart-vat-input" class="form-control border-start-0 text-end" value="0" step="0.01">
</div>
</div>
@ -335,7 +333,7 @@ if (!$loyalty_settings) {
<?php if (has_permission('pos_add')): ?>
<div class="mb-2">
<button class="btn btn-outline-danger w-100 py-2 fw-bold mb-2" onclick="clearCart()">
<button class="btn btn-outline-danger w-100 py-2 fw-bold mb-2 btn-sm" onclick="clearCart()">
<i class="bi bi-trash me-1"></i> CLEAR CART
</button>
<div class="d-flex gap-2">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff