Autosave: 20260222-135217

This commit is contained in:
Flatlogic Bot 2026-02-22 13:52:18 +00:00
parent 3d24190863
commit 5db529f225
17 changed files with 1056 additions and 308 deletions

View File

@ -16,12 +16,13 @@ if (isset($_GET['delete'])) {
}
// Fetch areas with outlet names
$areas = $pdo->query("
SELECT areas.*, outlets.name as outlet_name
$query = "SELECT areas.*, outlets.name as outlet_name
FROM areas
LEFT JOIN outlets ON areas.outlet_id = outlets.id
ORDER BY areas.id DESC
")->fetchAll();
ORDER BY areas.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();
@ -38,6 +39,10 @@ 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($areas_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
@ -68,6 +73,10 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($areas_pagination); ?>
</div>
</div>
</div>

View File

@ -3,12 +3,15 @@ require_once __DIR__ . '/../db/config.php';
$pdo = db();
if (isset($_GET['delete'])) {
$pdo->prepare("DELETE FROM categories WHERE id = ?")->execute([$_GET['delete']]);
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM categories WHERE id = ?")->execute([$id]);
header("Location: categories.php");
exit;
}
$categories = $pdo->query("SELECT * FROM categories ORDER BY sort_order ASC, name ASC")->fetchAll();
$query = "SELECT * FROM categories ORDER BY sort_order ASC, name ASC";
$categories_pagination = paginate_query($pdo, $query);
$categories = $categories_pagination['data'];
include 'includes/header.php';
?>
@ -22,6 +25,10 @@ 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); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
@ -65,6 +72,10 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($categories_pagination); ?>
</div>
</div>
</div>

View File

@ -28,7 +28,9 @@ if (isset($_GET['delete'])) {
}
// Fetch Customers
$customers = $pdo->query("SELECT * FROM customers ORDER BY id DESC")->fetchAll();
$query = "SELECT * FROM customers ORDER BY id DESC";
$customers_pagination = paginate_query($pdo, $query);
$customers = $customers_pagination['data'];
include 'includes/header.php';
?>
@ -44,6 +46,10 @@ 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($customers_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
@ -78,6 +84,10 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($customers_pagination); ?>
</div>
</div>
</div>

View File

@ -90,6 +90,11 @@ function isActive($page) {
<i class="bi bi-grid me-2"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../pos.php" target="_blank">
<i class="bi bi-display me-2"></i> POS Terminal
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('orders.php') ?>" href="orders.php">
<i class="bi bi-receipt me-2"></i> Orders (POS)

View File

@ -2,7 +2,9 @@
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$customers = $pdo->query("SELECT * FROM loyalty_customers ORDER BY points DESC")->fetchAll();
$query = "SELECT * FROM loyalty_customers ORDER BY points DESC";
$customers_pagination = paginate_query($pdo, $query);
$customers = $customers_pagination['data'];
include 'includes/header.php';
?>
@ -16,6 +18,10 @@ 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($customers_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
@ -49,7 +55,11 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($customers_pagination); ?>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -13,10 +13,13 @@ if (isset($_POST['action']) && $_POST['action'] === 'update_status') {
exit;
}
$orders = $pdo->query("SELECT o.*,
$query = "SELECT o.*,
(SELECT GROUP_CONCAT(CONCAT(p.name, ' x', oi.quantity) SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items_summary
FROM orders o
ORDER BY o.created_at DESC")->fetchAll();
ORDER BY o.created_at DESC";
$orders_pagination = paginate_query($pdo, $query);
$orders = $orders_pagination['data'];
include 'includes/header.php';
?>
@ -30,6 +33,10 @@ 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($orders_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
@ -113,7 +120,11 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($orders_pagination); ?>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>
<?php include 'includes/footer.php'; ?>

View File

@ -15,7 +15,9 @@ if (isset($_GET['delete'])) {
exit;
}
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY id DESC")->fetchAll();
$query = "SELECT * FROM outlets ORDER BY id DESC";
$outlets_pagination = paginate_query($pdo, $query);
$outlets = $outlets_pagination['data'];
include 'includes/header.php';
?>
@ -29,6 +31,10 @@ 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($outlets_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
@ -54,6 +60,10 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($outlets_pagination); ?>
</div>
</div>
</div>

View File

@ -30,6 +30,18 @@ if (isset($_POST['action']) && $_POST['action'] === 'add_variant') {
exit;
}
// Handle Edit Variant
if (isset($_POST['action']) && $_POST['action'] === 'edit_variant') {
$id = $_POST['variant_id'];
$name = $_POST['name'];
$price_adj = $_POST['price_adjustment'];
$stmt = $pdo->prepare("UPDATE product_variants SET name = ?, price_adjustment = ? WHERE id = ?");
$stmt->execute([$name, $price_adj, $id]);
header("Location: product_variants.php?product_id=$product_id");
exit;
}
// Handle Delete
if (isset($_GET['delete'])) {
$pdo->prepare("DELETE FROM product_variants WHERE id = ?")->execute([$_GET['delete']]);
@ -37,9 +49,9 @@ if (isset($_GET['delete'])) {
exit;
}
$variants = $pdo->prepare("SELECT * FROM product_variants WHERE product_id = ? ORDER BY price_adjustment ASC");
$variants->execute([$product_id]);
$variants = $variants->fetchAll();
$query = "SELECT * FROM product_variants WHERE product_id = ? ORDER BY price_adjustment ASC";
$variants_pagination = paginate_query($pdo, $query, [$product_id]);
$variants = $variants_pagination['data'];
include 'includes/header.php';
?>
@ -59,6 +71,10 @@ 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($variants_pagination, ['product_id' => $product_id]); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
@ -86,6 +102,14 @@ include 'includes/header.php';
<?= format_currency($product['price'] + $variant['price_adjustment']) ?>
</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1"
data-bs-toggle="modal"
data-bs-target="#editVariantModal"
data-id="<?= $variant['id'] ?>"
data-name="<?= htmlspecialchars($variant['name']) ?>"
data-price="<?= $variant['price_adjustment'] ?>">
<i class="bi bi-pencil"></i>
</button>
<a href="?product_id=<?= $product_id ?>&delete=<?= $variant['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this variant?')"><i class="bi bi-trash"></i></a>
</td>
</tr>
@ -98,6 +122,10 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($variants_pagination, ['product_id' => $product_id]); ?>
</div>
</div>
</div>
@ -134,4 +162,58 @@ include 'includes/header.php';
</div>
</div>
<?php include 'includes/footer.php'; ?>
<!-- Edit Variant Modal -->
<div class="modal fade" id="editVariantModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Variant</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="edit_variant">
<input type="hidden" name="variant_id" id="edit_variant_id">
<div class="mb-3">
<label class="form-label">Variant Name</label>
<input type="text" name="name" id="edit_variant_name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Price Adjustment (+/-)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" step="0.01" name="price_adjustment" id="edit_variant_price" class="form-control" required>
</div>
<div class="form-text">Enter positive value for extra cost, negative for discount.</div>
</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">Update Variant</button>
</div>
</form>
</div>
</div>
</div>
<script>
var editVariantModal = document.getElementById('editVariantModal');
if (editVariantModal) {
editVariantModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var id = button.getAttribute('data-id');
var name = button.getAttribute('data-name');
var price = button.getAttribute('data-price');
var modalIdInput = editVariantModal.querySelector('#edit_variant_id');
var modalNameInput = editVariantModal.querySelector('#edit_variant_name');
var modalPriceInput = editVariantModal.querySelector('#edit_variant_price');
modalIdInput.value = id;
modalNameInput.value = name;
modalPriceInput.value = price;
});
}
</script>
<?php include 'includes/footer.php'; ?>

View File

@ -36,9 +36,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
$stmt = $pdo->prepare("INSERT INTO products (name, category_id, price, description, image_url) VALUES (?, ?, ?, ?, ?)");
if ($stmt->execute([$name, $category_id, $price, $description, $image_url])) {
$message = '<div class="alert alert-success">Product added successfully!</div>';
$message = '<div class="alert alert-success border-0 shadow-sm rounded-3"><i class="bi bi-check-circle-fill me-2"></i>Product added successfully!</div>';
} else {
$message = '<div class="alert alert-danger">Error adding product.</div>';
$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>';
}
}
@ -50,107 +50,189 @@ if (isset($_GET['delete'])) {
exit;
}
// Fetch Products with Category Name
$products = $pdo->query("SELECT p.*, c.name as category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
ORDER BY p.id DESC")->fetchAll();
// Fetch Categories for Dropdown
// 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 = [];
$query = "SELECT p.*, c.name as category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id";
if ($search) {
$where[] = "(p.name LIKE ? OR p.description LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
}
if ($category_filter) {
$where[] = "p.category_id = ?";
$params[] = $category_filter;
}
if (!empty($where)) {
$query .= " WHERE " . implode(" AND ", $where);
}
$query .= " ORDER BY p.id DESC";
$products_pagination = paginate_query($pdo, $query, $params);
$products = $products_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Products</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProductModal">
<i class="bi bi-plus-lg"></i> Add Product
<div>
<h2 class="fw-bold mb-1 text-dark">Products</h2>
<p class="text-muted mb-0">Manage your catalog</p>
</div>
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#addProductModal" style="border-radius: 10px; padding: 0.6rem 1.2rem;">
<i class="bi bi-plus-lg me-1"></i> Add Product
</button>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Image</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $product): ?>
<tr>
<td class="ps-4">
<img src="<?= htmlspecialchars(strpos($product['image_url'], 'http') === 0 ? $product['image_url'] : '../' . $product['image_url']) ?>" alt="" class="rounded" width="48" height="48" style="object-fit: cover;">
</td>
<td>
<div class="fw-bold"><?= htmlspecialchars($product['name']) ?></div>
<small class="text-muted"><?= htmlspecialchars(substr($product['description'] ?? '', 0, 50)) ?>...</small>
</td>
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars($product['category_name'] ?? 'Uncategorized') ?></span></td>
<td class="fw-bold"><?= format_currency($product['price']) ?></td>
<td>
<div class="btn-group">
<a href="product_edit.php?id=<?= $product['id'] ?>" class="btn btn-sm btn-outline-primary" title="Edit Product"><i class="bi bi-pencil"></i></a>
<a href="product_variants.php?product_id=<?= $product['id'] ?>" class="btn btn-sm btn-outline-secondary" title="Manage Variants"><i class="bi bi-gear"></i></a>
<a href="?delete=<?= $product['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')" title="Delete"><i class="bi bi-trash"></i></a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- Filter & Search Bar -->
<div class="filter-bar">
<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;">
</div>
</div>
</div>
<div class="col-md-3">
<select name="category_filter" class="form-select" onchange="this.form.submit()" style="border-radius: 10px;">
<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>
<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; ?>
</div>
<?php else: ?>
<div class="table-responsive" style="overflow-x: visible;">
<table class="friendly-table">
<thead>
<tr>
<th>Product</th>
<th>Category</th>
<th>Price</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $product): ?>
<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">
<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>
</div>
</td>
<td>
<span class="badge-soft"><?= htmlspecialchars($product['category_name'] ?? 'Uncategorized') ?></span>
</td>
<td>
<span class="text-price fs-5"><?= format_currency($product['price']) ?></span>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<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>
<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>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="mt-4">
<?php render_pagination_controls($products_pagination); ?>
</div>
<?php endif; ?>
<!-- Add Product Modal -->
<div class="modal fade" id="addProductModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Product</h5>
<div class="modal-dialog 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>
</div>
<form method="POST" enctype="multipart/form-data">
<div class="modal-body">
<div class="modal-body pt-4">
<input type="hidden" name="action" value="add_product">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" required>
<label class="form-label text-muted small fw-bold">PRODUCT NAME</label>
<input type="text" name="name" class="form-control form-control-lg" placeholder="e.g. Cheese Burger" required style="border-radius: 10px;">
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">CATEGORY</label>
<select name="category_id" class="form-select" required style="border-radius: 10px;">
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label text-muted small fw-bold">PRICE ($)</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0" style="border-radius: 10px 0 0 10px;">$</span>
<input type="number" step="0.01" name="price" class="form-control border-start-0" placeholder="0.00" required style="border-radius: 0 10px 10px 0;">
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Category</label>
<select name="category_id" class="form-select" required>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
<label class="form-label text-muted small fw-bold">DESCRIPTION</label>
<textarea name="description" class="form-control" rows="3" placeholder="Brief description..." style="border-radius: 10px;"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Price ($)</label>
<input type="number" step="0.01" name="price" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Product Image</label>
<input type="file" name="image" class="form-control" accept="image/*">
<div class="form-text">Leave empty to use a placeholder.</div>
<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>
</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 Product</button>
<div class="modal-footer border-top-0 pt-0 pb-4 px-4">
<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>
</div>
</form>
</div>

View File

@ -30,7 +30,9 @@ if (isset($_GET['delete'])) {
}
// Fetch Suppliers
$suppliers = $pdo->query("SELECT * FROM suppliers ORDER BY id DESC")->fetchAll();
$query = "SELECT * FROM suppliers ORDER BY id DESC";
$suppliers_pagination = paginate_query($pdo, $query);
$suppliers = $suppliers_pagination['data'];
include 'includes/header.php';
?>
@ -46,6 +48,10 @@ 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($suppliers_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
@ -83,6 +89,10 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($suppliers_pagination); ?>
</div>
</div>
</div>

View File

@ -16,12 +16,13 @@ if (isset($_GET['delete'])) {
}
// Fetch tables with area names
$tables = $pdo->query("
SELECT tables.*, areas.name as area_name
$query = "SELECT tables.*, areas.name as area_name
FROM tables
LEFT JOIN areas ON tables.area_id = areas.id
ORDER BY tables.id DESC
")->fetchAll();
ORDER BY tables.id DESC";
$tables_pagination = paginate_query($pdo, $query);
$tables = $tables_pagination['data'];
// Fetch areas for dropdown
$areas = $pdo->query("SELECT id, name FROM areas ORDER BY name ASC")->fetchAll();
@ -38,6 +39,10 @@ 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($tables_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
@ -70,6 +75,10 @@ include 'includes/header.php';
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($tables_pagination); ?>
</div>
</div>
</div>

View File

@ -48,7 +48,7 @@ try {
// Customer Handling
$customer_id = $data['customer_id'] ?? null;
$customer_name = $data['customer_name'] ?? null; // Fallback or manual entry
$customer_name = $data['customer_name'] ?? null;
$customer_phone = $data['customer_phone'] ?? null;
if ($customer_id) {
@ -59,24 +59,27 @@ try {
$customer_name = $cust['name'];
$customer_phone = $cust['phone'];
} else {
// Invalid customer ID, clear it but keep manual name if any (though unlikely combo)
$customer_id = null;
}
}
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, total_amount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
$stmt->execute([1, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $data['total_amount'] ?? 0]);
$discount = isset($data['discount']) ? floatval($data['discount']) : 0.00;
$total_amount = isset($data['total_amount']) ? floatval($data['total_amount']) : 0.00;
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, total_amount, discount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
$stmt->execute([1, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $total_amount, $discount]);
$order_id = $pdo->lastInsertId();
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (?, ?, ?, ?)");
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)");
if (!empty($data['items']) && is_array($data['items'])) {
foreach ($data['items'] as $item) {
$pid = $item['product_id'] ?? ($item['id'] ?? null);
$qty = $item['quantity'] ?? 1;
$price = $item['unit_price'] ?? ($item['price'] ?? 0);
$vid = $item['variant_id'] ?? null;
if ($pid) {
$item_stmt->execute([$order_id, $pid, $qty, $price]);
$item_stmt->execute([$order_id, $pid, $vid, $qty, $price]);
}
}
}
@ -90,4 +93,4 @@ try {
}
error_log("Order Error: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}

View File

@ -163,3 +163,94 @@ body {
right: 2rem;
z-index: 1050;
}
/* Friendly Table & UI Enhancements */
.filter-bar {
background: #fff;
border-radius: 12px;
padding: 1rem 1.5rem;
box-shadow: 0 2px 12px rgba(0,0,0,0.03);
margin-bottom: 1.5rem;
}
.friendly-table {
border-collapse: separate;
border-spacing: 0 12px;
width: 100%;
}
.friendly-table thead th {
border: none;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
color: #999;
letter-spacing: 0.8px;
padding: 0 1.5rem 0.5rem 1.5rem;
}
.friendly-table tbody tr {
background: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
border-radius: 12px;
transition: all 0.2s ease;
}
.friendly-table tbody tr:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.06);
}
.friendly-table td {
border: none;
padding: 1rem 1.5rem;
vertical-align: middle;
background-color: #fff; /* Ensure bg is applied to cells for radius to work if needed */
}
.friendly-table td:first-child {
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
.friendly-table td:last-child {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
}
.img-thumb-lg {
width: 64px;
height: 64px;
border-radius: 12px;
object-fit: cover;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.badge-soft {
background-color: #f3f4f6;
color: #4b5563;
font-weight: 500;
padding: 0.4em 0.8em;
border-radius: 6px;
}
.btn-icon-soft {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.2s;
background: #f8f9fa;
color: #6c757d;
border: none;
}
.btn-icon-soft:hover {
background: #e9ecef;
color: #495057;
}
.btn-icon-soft.delete:hover {
background: #fee2e2;
color: #ef4444;
}
.btn-icon-soft.edit:hover {
background: #dbeafe;
color: #3b82f6;
}
.text-price {
font-family: 'Inter', sans-serif;
font-weight: 700;
color: #111827;
}

View File

@ -3,6 +3,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 checkoutBtn = document.getElementById('checkout-btn');
// Table Management
@ -11,6 +12,11 @@ document.addEventListener('DOMContentLoaded', () => {
const tableDisplay = document.getElementById('current-table-display');
const tableModalEl = document.getElementById('tableSelectionModal');
const tableSelectionModal = new bootstrap.Modal(tableModalEl);
// Variant Management
const variantModalEl = document.getElementById('variantSelectionModal');
const variantSelectionModal = new bootstrap.Modal(variantModalEl);
let pendingProduct = null;
// Customer Search Elements
const customerSearchInput = document.getElementById('customer-search');
@ -58,7 +64,6 @@ document.addEventListener('DOMContentLoaded', () => {
btnElement.classList.add('active');
} else if (typeof btnElement === 'undefined' && categoryId !== 'all') {
// Try to find the button corresponding to this category ID
// This might happen if triggered programmatically
}
filterProducts();
@ -219,24 +224,72 @@ document.addEventListener('DOMContentLoaded', () => {
// --- Cart Logic ---
document.querySelectorAll('.add-to-cart').forEach(card => {
card.addEventListener('click', (e) => {
// Find closest card just in case click was on child
const target = e.currentTarget;
const product = {
id: target.dataset.id,
name: target.dataset.name,
price: parseFloat(target.dataset.price),
quantity: 1
base_price: parseFloat(target.dataset.price),
hasVariants: target.dataset.hasVariants === 'true',
quantity: 1,
variant_id: null,
variant_name: null
};
addToCart(product);
if (product.hasVariants) {
openVariantModal(product);
} else {
addToCart(product);
}
});
});
function openVariantModal(product) {
pendingProduct = product;
const variants = PRODUCT_VARIANTS[product.id] || [];
const list = document.getElementById('variant-list');
const title = document.getElementById('variantModalTitle');
title.textContent = `Select option for ${product.name}`;
list.innerHTML = '';
// Add "Base" option if needed, but usually variants are mandatory if they exist.
// Assuming mandatory for now or include a base option.
variants.forEach(v => {
const btn = document.createElement('button');
btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
const adj = parseFloat(v.price_adjustment);
const sign = adj > 0 ? '+' : '';
const priceText = adj !== 0 ? `${sign}${formatCurrency(adj)}` : '';
const finalPrice = product.base_price + adj;
btn.innerHTML = `
<span>${v.name}</span>
<span class="fw-bold">${formatCurrency(finalPrice)}</span>
`;
btn.onclick = () => {
pendingProduct.variant_id = v.id;
pendingProduct.variant_name = v.name;
pendingProduct.price = finalPrice;
addToCart(pendingProduct);
variantSelectionModal.hide();
};
list.appendChild(btn);
});
variantSelectionModal.show();
}
function addToCart(product) {
const existing = cart.find(item => item.id === product.id);
// Unique ID for cart item is ProductID + VariantID
const existing = cart.find(item => item.id === product.id && item.variant_id === product.variant_id);
if (existing) {
existing.quantity++;
} else {
cart.push(product);
// Clone to avoid reference issues
cart.push({...product});
}
updateCart();
}
@ -266,20 +319,21 @@ document.addEventListener('DOMContentLoaded', () => {
}
cartItemsContainer.innerHTML = '';
let total = 0;
let subtotal = 0;
cart.forEach((item, index) => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
subtotal += itemTotal;
const row = document.createElement('div');
row.className = 'd-flex justify-content-between align-items-center mb-3 border-bottom pb-2';
// Updated Cart Item Layout
const variantLabel = item.variant_name ? `<span class="badge bg-light text-dark border ms-1">${item.variant_name}</span>` : '';
row.innerHTML = `
<div class="flex-grow-1 me-2">
<div class="fw-bold text-truncate" style="max-width: 140px;">${item.name}</div>
<div class="small text-muted">${formatCurrency(item.price)}</div>
<div class="small text-muted">${formatCurrency(item.price)} ${variantLabel}</div>
</div>
<div class="d-flex align-items-center bg-light rounded px-1">
<button class="btn btn-sm text-secondary p-0" style="width: 24px;" onclick="changeQuantity(${index}, -1)"><i class="bi bi-dash"></i></button>
@ -294,11 +348,21 @@ document.addEventListener('DOMContentLoaded', () => {
cartItemsContainer.appendChild(row);
});
cartSubtotal.innerText = formatCurrency(total);
cartSubtotal.innerText = formatCurrency(subtotal);
// Discount
let discount = parseFloat(cartDiscountInput.value) || 0;
let total = subtotal - discount;
if (total < 0) total = 0;
cartTotalPrice.innerText = formatCurrency(total);
checkoutBtn.disabled = false;
}
if (cartDiscountInput) {
cartDiscountInput.addEventListener('input', updateCart);
}
window.removeFromCart = function(index) {
cart.splice(index, 1);
updateCart();
@ -317,7 +381,9 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
const totalAmount = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0);
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 custId = selectedCustomerId.value;
const orderData = {
@ -325,14 +391,15 @@ document.addEventListener('DOMContentLoaded', () => {
order_type: orderType,
customer_id: custId || null,
total_amount: totalAmount,
discount: discount,
items: cart.map(item => ({
product_id: item.id,
quantity: item.quantity,
unit_price: item.price
unit_price: item.price,
variant_id: item.variant_id
}))
};
// Show loading state
checkoutBtn.disabled = true;
checkoutBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Processing...';
@ -348,10 +415,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (data.success) {
cart = [];
cartDiscountInput.value = 0;
updateCart();
// Reset customer
if (clearCustomerBtn) clearCustomerBtn.click();
showToast(`Order #${data.order_id} placed!`, 'success');
} else {
showToast(`Error: ${data.error}`, 'danger');
@ -381,4 +447,7 @@ document.addEventListener('DOMContentLoaded', () => {
t.show();
el.addEventListener('hidden.bs.toast', () => el.remove());
}
// Initialize logic
checkOrderType();
});

View File

@ -32,3 +32,152 @@ function format_currency($amount) {
$settings = get_company_settings();
return $settings['currency_symbol'] . number_format((float)$amount, (int)$settings['currency_decimals']);
}
/**
* Paginate a query result.
*
* @param PDO $pdo The PDO connection object.
* @param string $query The base SQL query (without LIMIT/OFFSET).
* @param array $params Query parameters.
* @param int $default_limit Default items per page.
* @return array Pagination result with keys: data, total_rows, total_pages, current_page, limit.
*/
function paginate_query($pdo, $query, $params = [], $default_limit = 20) {
// Get current page
$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1;
// Get limit (allow 20, 50, 100, or -1 for all)
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : $default_limit;
// Validate limit, default to 20 if not standard, allow custom if needed but standardizing is safer
if ($limit != -1 && !in_array($limit, [20, 50, 100])) {
$limit = $default_limit;
}
// If limit is -1, fetch all
if ($limit == -1) {
$stmt = $pdo->prepare($query);
$stmt->execute($params);
$data = $stmt->fetchAll();
return [
'data' => $data,
'total_rows' => count($data),
'total_pages' => 1,
'current_page' => 1,
'limit' => -1
];
}
// Count total rows using a subquery to handle complex queries safely
$count_sql = "SELECT COUNT(*) FROM ($query) as count_table";
$stmt = $pdo->prepare($count_sql);
$stmt->execute($params);
$total_rows = $stmt->fetchColumn();
$total_pages = ceil($total_rows / $limit);
if ($page > $total_pages && $total_pages > 0) $page = $total_pages;
// Calculate offset
$offset = ($page - 1) * $limit;
if ($offset < 0) $offset = 0;
// Add LIMIT and OFFSET
// Note: PDO parameters for LIMIT/OFFSET can be tricky with some drivers, sticking to direct injection for integers is safe here
$query .= " LIMIT " . (int)$limit . " OFFSET " . (int)$offset;
$stmt = $pdo->prepare($query);
$stmt->execute($params);
$data = $stmt->fetchAll();
return [
'data' => $data,
'total_rows' => $total_rows,
'total_pages' => $total_pages,
'current_page' => $page,
'limit' => $limit
];
}
/**
* Render pagination controls and limit selector.
*
* @param array $pagination The result array from paginate_query.
* @param array $extra_params Additional GET parameters to preserve.
*/
function render_pagination_controls($pagination, $extra_params = []) {
$page = $pagination['current_page'];
$total_pages = $pagination['total_pages'];
$limit = $pagination['limit'];
// Build query string for limit change
$params = array_merge($_GET, $extra_params);
unset($params['page']); // Reset page when limit changes
// Limit Selector
$limits = [20, 50, 100, -1];
echo '<div class="d-flex justify-content-between align-items-center mb-3 bg-white p-2 rounded border">';
echo '<form method="GET" class="d-flex align-items-center mb-0">';
// Preserve other GET params
foreach ($params as $key => $val) {
if ($key !== 'limit') echo '<input type="hidden" name="'.htmlspecialchars($key).'" value="'.htmlspecialchars($val).'">';
}
echo '<small class="me-2 text-muted">Show:</small>';
echo '<select name="limit" class="form-select form-select-sm border-0 bg-light" style="width: auto; font-weight: 500;" onchange="this.form.submit()">';
foreach ($limits as $l) {
$label = $l == -1 ? 'All' : $l;
$selected = $limit == $l ? 'selected' : '';
echo "<option value='$l' $selected>$label</option>";
}
echo '</select>';
echo '</form>';
// Pagination Links
if ($total_pages > 1) {
echo '<nav><ul class="pagination pagination-sm mb-0">';
// Previous
$prev_disabled = $page <= 1 ? 'disabled' : '';
$prev_page = max(1, $page - 1);
$url_params = array_merge($params, ['page' => $prev_page, 'limit' => $limit]);
$prev_url = '?' . http_build_query($url_params);
echo "<li class='page-item $prev_disabled'><a class='page-link' href='$prev_url'>&laquo;</a></li>";
// Logic to show limited page numbers with ellipsis
// Always show first, last, current, and surrounding
$shown_pages = [];
$shown_pages[] = 1;
$shown_pages[] = $total_pages;
for ($i = $page - 2; $i <= $page + 2; $i++) {
if ($i > 1 && $i < $total_pages) {
$shown_pages[] = $i;
}
}
sort($shown_pages);
$shown_pages = array_unique($shown_pages);
$prev_p = 0;
foreach ($shown_pages as $p) {
if ($prev_p > 0 && $p > $prev_p + 1) {
echo "<li class='page-item disabled'><span class='page-link'>...</span></li>";
}
$active = $p == $page ? 'active' : '';
$url_params = array_merge($params, ['page' => $p, 'limit' => $limit]);
$url = '?' . http_build_query($url_params);
echo "<li class='page-item $active'><a class='page-link' href='$url'>$p</a></li>";
$prev_p = $p;
}
// Next
$next_disabled = $page >= $total_pages ? 'disabled' : '';
$next_page = min($total_pages, $page + 1);
$url_params = array_merge($params, ['page' => $next_page, 'limit' => $limit]);
$next_url = '?' . http_build_query($url_params);
echo "<li class='page-item $next_disabled'><a class='page-link' href='$next_url'>&raquo;</a></li>";
echo '</ul></nav>';
} else {
echo '<small class="text-muted">Total: ' . $pagination['total_rows'] . '</small>';
}
echo '</div>';
}

293
index.php
View File

@ -1,211 +1,130 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
$pdo = db();
$categories = $pdo->query("SELECT * FROM categories ORDER BY sort_order")->fetchAll();
$all_products = $pdo->query("SELECT p.*, c.name as category_name FROM products p JOIN categories c ON p.category_id = c.id")->fetchAll();
$table_id = $_GET['table'] ?? '1'; // Default table
$outlet_id = 1; // Main outlet
$settings = get_company_settings();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($settings['company_name']) ?> - POS</title>
<?php if (!empty($settings['favicon_url'])): ?>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($settings['company_name']) ?> - Welcome</title>
<?php if (!empty($settings['favicon_url'])): ?>
<link rel="icon" href="<?= htmlspecialchars($settings['favicon_url']) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<style>
body { height: 100vh; overflow: hidden; } /* 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: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; }
</style>
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<style>
body {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
}
.hero-card {
background: rgba(255, 255, 255, 0.9);
border-radius: 24px;
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.5);
transition: transform 0.3s ease;
}
.hero-card:hover {
transform: translateY(-5px);
}
.action-btn {
height: 120px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 16px;
font-weight: 600;
font-size: 1.2rem;
transition: all 0.2s ease;
border: 2px solid transparent;
}
.action-btn i {
font-size: 2.5rem;
margin-bottom: 10px;
}
.btn-dine-in {
background-color: #e3f2fd;
color: #0d6efd;
border-color: #bbdefb;
}
.btn-dine-in:hover {
background-color: #bbdefb;
color: #0b5ed7;
}
.btn-online {
background-color: #e8f5e9;
color: #198754;
border-color: #c8e6c9;
}
.btn-online:hover {
background-color: #c8e6c9;
color: #146c43;
}
.company-logo {
max-height: 80px;
width: auto;
margin-bottom: 20px;
}
</style>
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom shadow-sm" style="height: 60px;">
<div class="container-fluid">
<a href="index.php" class="navbar-brand d-flex align-items-center gap-2">
<?php if (!empty($settings['logo_url'])): ?>
<img src="<?= htmlspecialchars($settings['logo_url']) ?>" alt="Logo" style="height: 32px; width: auto;">
<?php endif; ?>
<span class="fw-bold d-none d-md-block"><?= htmlspecialchars($settings['company_name']) ?></span>
</a>
<div class="d-flex align-items-center gap-3">
<div id="current-table-display" class="badge bg-light text-dark border px-3 py-2" style="display: none; font-size: 0.9rem;">
Table <?= htmlspecialchars($table_id) ?>
</div>
<a href="admin/orders.php" class="btn btn-sm btn-outline-dark"><i class="bi bi-shield-lock"></i> Admin</a>
</div>
</div>
</nav>
<div class="container-fluid p-0">
<div class="row g-0">
<!-- Left Sidebar: Categories -->
<div class="col-md-2 d-none d-md-block category-sidebar scrollable-y p-3 border-end">
<h6 class="text-uppercase text-muted small fw-bold mb-3 ms-1">Categories</h6>
<button class="category-btn active mb-1" onclick="filterCategory('all', this)">
<i class="bi bi-grid me-2"></i> All Items
</button>
<?php foreach ($categories as $category): ?>
<button class="category-btn mb-1" onclick="filterCategory(<?= $category['id'] ?>, this)">
<?php if (!empty($category['image_url'])): ?>
<img src="<?= htmlspecialchars($category['image_url']) ?>" style="width: 20px; height: 20px; border-radius: 4px; object-fit: cover;" class="me-2">
<?php else: ?>
<i class="bi bi-tag me-2"></i>
<?php endif; ?>
<?= htmlspecialchars($category['name']) ?>
</button>
<?php endforeach; ?>
</div>
<!-- Middle: Products -->
<div class="col-md-7 col-12 product-area scrollable-y p-4 bg-light">
<!-- Mobile Category Select (Visible only on small screens) -->
<div class="d-md-none mb-3">
<select class="form-select" onchange="filterCategory(this.value)">
<option value="all">All Categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Product Search Bar -->
<div class="mb-3">
<div class="input-group shadow-sm">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span>
<input type="text" id="product-search-input" class="form-control border-start-0 ps-0" placeholder="Search products..." autocomplete="off">
</div>
</div>
<div class="row g-3" id="products-container">
<?php foreach ($all_products as $product): ?>
<div class="col-6 col-lg-3 col-xl-3 product-item" data-category-id="<?= $product['category_id'] ?>">
<div class="card h-100 border-0 shadow-sm product-card add-to-cart"
data-id="<?= $product['id'] ?>"
data-name="<?= htmlspecialchars($product['name']) ?>"
data-price="<?= $product['price'] ?>">
<div class="position-relative">
<img src="https://picsum.photos/seed/<?= $product['id'] ?>/300/200" class="card-img-top object-fit-cover" alt="..." style="height: 120px;">
<div class="position-absolute bottom-0 end-0 m-2">
<span class="badge bg-dark rounded-pill"><?= format_currency($product['price']) ?></span>
</div>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<div class="hero-card p-5 text-center">
<?php if (!empty($settings['logo_url'])): ?>
<img src="<?= htmlspecialchars($settings['logo_url']) ?>" alt="Logo" class="company-logo">
<?php else: ?>
<div class="mb-4">
<i class="bi bi-shop fs-1 text-primary"></i>
</div>
<div class="card-body p-2">
<h6 class="card-title fw-bold small mb-1 text-truncate"><?= htmlspecialchars($product['name']) ?></h6>
<p class="card-text small text-muted text-truncate mb-0"><?= htmlspecialchars($product['category_name']) ?></p>
<?php endif; ?>
<h1 class="fw-bold mb-2"><?= htmlspecialchars($settings['company_name']) ?></h1>
<p class="text-muted mb-5">Welcome! How would you like to order?</p>
<div class="row g-3">
<div class="col-6">
<a href="pos.php?order_type=dine-in" class="text-decoration-none">
<div class="action-btn btn-dine-in">
<i class="bi bi-qr-code-scan"></i>
<span>Dine In / QR</span>
</div>
</a>
</div>
<div class="col-6">
<a href="pos.php?order_type=takeaway" class="text-decoration-none">
<div class="action-btn btn-online">
<i class="bi bi-bag-check"></i>
<span>Online Order</span>
</div>
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Right: Cart & Order Info -->
<div class="col-md-3 col-12 cart-sidebar">
<!-- Top Section: Customer & Type -->
<div class="p-3 border-bottom bg-white">
<!-- Order Type -->
<div class="btn-group w-100 mb-3" role="group">
<input type="radio" class="btn-check" name="order_type" id="ot-dine-in" value="dine-in" checked>
<label class="btn btn-outline-primary btn-sm" for="ot-dine-in">Dine-In</label>
<div class="mt-5 pt-4 border-top">
<a href="admin/" class="text-muted small text-decoration-none">
<i class="bi bi-shield-lock me-1"></i> Staff Login
</a>
</div>
<input type="radio" class="btn-check" name="order_type" id="ot-takeaway" value="takeaway">
<label class="btn btn-outline-primary btn-sm" for="ot-takeaway">Takeaway</label>
<input type="radio" class="btn-check" name="order_type" id="ot-delivery" value="delivery">
<label class="btn btn-outline-primary btn-sm" for="ot-delivery">Delivery</label>
</div>
<!-- Customer Search -->
<div class="position-relative">
<div class="input-group">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-person"></i></span>
<input type="text" class="form-control border-start-0 ps-0" id="customer-search" placeholder="Search Customer..." autocomplete="off">
<button class="btn btn-outline-secondary d-none" type="button" id="clear-customer"><i class="bi bi-x"></i></button>
</div>
<div class="list-group shadow-sm search-dropdown" id="customer-results"></div>
<input type="hidden" id="selected-customer-id">
<div id="customer-info" class="small text-success mt-1 d-none">
<i class="bi bi-check-circle-fill me-1"></i> <span id="customer-name-display"></span>
</div>
</div>
</div>
<!-- Cart Items (Flex Grow) -->
<div class="flex-grow-1 overflow-auto p-3 bg-white" id="cart-items">
<div class="text-center text-muted mt-5">
<i class="bi bi-basket3 fs-1 text-light"></i>
<p class="mt-2">Cart is empty</p>
</div>
</div>
<!-- Bottom: Totals & Action -->
<div class="p-3 border-top bg-light">
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Subtotal</span>
<span class="fw-bold" id="cart-subtotal"><?= format_currency(0) ?></span>
</div>
<!-- Tax could go here -->
<div class="d-flex justify-content-between mb-3">
<span class="fs-5 fw-bold">Total</span>
<span class="fs-4 fw-bold text-primary" id="cart-total-price"><?= format_currency(0) ?></span>
</div>
<button class="btn btn-primary w-100 btn-lg shadow-sm" id="checkout-btn" disabled>
Place Order <i class="bi bi-arrow-right ms-2"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container position-fixed bottom-0 start-50 translate-middle-x p-3" id="toast-container" style="z-index: 1060;"></div>
<!-- Table Selection Modal -->
<div class="modal fade" id="tableSelectionModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold">Select Table</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="close-table-modal" style="display:none;"></button>
</div>
<div class="modal-body bg-light">
<div id="table-list-container" class="row g-3"></div>
</div>
</div>
</div>
</div>
<script>
const COMPANY_SETTINGS = <?= json_encode($settings) ?>;
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>

268
pos.php Normal file
View File

@ -0,0 +1,268 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
$pdo = db();
$categories = $pdo->query("SELECT * FROM categories ORDER BY sort_order")->fetchAll();
$all_products = $pdo->query("SELECT p.*, c.name as category_name FROM products p JOIN categories c ON p.category_id = c.id")->fetchAll();
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll();
// Fetch variants
$variants_raw = $pdo->query("SELECT * FROM product_variants ORDER BY price_adjustment ASC")->fetchAll();
$variants_by_product = [];
foreach ($variants_raw as $v) {
$variants_by_product[$v['product_id']][] = $v;
}
$table_id = $_GET['table'] ?? '1'; // Default table
$outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
$settings = get_company_settings();
$order_type = $_GET['order_type'] ?? 'dine-in';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($settings['company_name']) ?> - POS</title>
<?php if (!empty($settings['favicon_url'])): ?>
<link rel="icon" href="<?= htmlspecialchars($settings['favicon_url']) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
<style>
body { height: 100vh; overflow: hidden; } /* 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: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; }
</style>
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom shadow-sm" style="height: 60px;">
<div class="container-fluid">
<a href="pos.php" class="navbar-brand d-flex align-items-center gap-2">
<?php if (!empty($settings['logo_url'])): ?>
<img src="<?= htmlspecialchars($settings['logo_url']) ?>" alt="Logo" style="height: 32px; width: auto;">
<?php endif; ?>
<span class="fw-bold d-none d-md-block"><?= htmlspecialchars($settings['company_name']) ?></span>
</a>
<div class="d-flex align-items-center gap-3">
<div id="current-table-display" class="badge bg-light text-dark border px-3 py-2" style="display: none; font-size: 0.9rem;">
Table <?= htmlspecialchars($table_id) ?>
</div>
<a href="admin/" class="btn btn-sm btn-outline-dark"><i class="bi bi-shield-lock"></i> Admin</a>
</div>
</div>
</nav>
<div class="container-fluid p-0">
<div class="row g-0">
<!-- Left Sidebar: Categories -->
<div class="col-md-2 d-none d-md-block category-sidebar scrollable-y p-3 border-end">
<h6 class="text-uppercase text-muted small fw-bold mb-3 ms-1">Categories</h6>
<button class="category-btn active mb-1" onclick="filterCategory('all', this)">
<i class="bi bi-grid me-2"></i> All Items
</button>
<?php foreach ($categories as $category): ?>
<button class="category-btn mb-1" onclick="filterCategory(<?= $category['id'] ?>, this)">
<?php if (!empty($category['image_url'])): ?>
<img src="<?= htmlspecialchars($category['image_url']) ?>" style="width: 20px; height: 20px; border-radius: 4px; object-fit: cover;" class="me-2">
<?php else: ?>
<i class="bi bi-tag me-2"></i>
<?php endif; ?>
<?= htmlspecialchars($category['name']) ?>
</button>
<?php endforeach; ?>
</div>
<!-- Middle: Products -->
<div class="col-md-7 col-12 product-area scrollable-y p-4 bg-light">
<!-- Mobile Category Select (Visible only on small screens) -->
<div class="d-md-none mb-3">
<select class="form-select" onchange="filterCategory(this.value)">
<option value="all">All Categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Product Search Bar & Outlet Selector -->
<div class="mb-3 row g-2">
<div class="col-md-4 col-12">
<select class="form-select shadow-sm" id="outlet-select" onchange="const url = new URL(window.location); url.searchParams.set('outlet_id', this.value); window.location = url;">
<?php if (empty($outlets)): ?>
<option value="1">Main Outlet</option>
<?php else: ?>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= $outlet_id == $outlet['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($outlet['name']) ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="col-md-8 col-12">
<div class="input-group shadow-sm">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span>
<input type="text" id="product-search-input" class="form-control border-start-0 ps-0" placeholder="Search products..." autocomplete="off">
</div>
</div>
</div>
<div class="row g-3" id="products-container">
<?php foreach ($all_products as $product):
$has_variants = !empty($variants_by_product[$product['id']]);
?>
<div class="col-6 col-lg-3 col-xl-3 product-item" data-category-id="<?= $product['category_id'] ?>">
<div class="card h-100 border-0 shadow-sm product-card add-to-cart"
data-id="<?= $product['id'] ?>"
data-name="<?= htmlspecialchars($product['name']) ?>"
data-price="<?= $product['price'] ?>"
data-has-variants="<?= $has_variants ? 'true' : 'false' ?>">
<div class="position-relative">
<img src="https://picsum.photos/seed/<?= $product['id'] ?>/300/200" class="card-img-top object-fit-cover" alt="..." style="height: 120px;">
<div class="position-absolute bottom-0 end-0 m-2">
<span class="badge bg-dark rounded-pill"><?= format_currency($product['price']) ?></span>
</div>
</div>
<div class="card-body p-2">
<h6 class="card-title fw-bold small mb-1 text-truncate"><?= htmlspecialchars($product['name']) ?></h6>
<p class="card-text small text-muted text-truncate mb-0"><?= htmlspecialchars($product['category_name']) ?></p>
<?php if ($has_variants): ?>
<span class="badge bg-light text-secondary border mt-1">Options</span>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Right: Cart & Order Info -->
<div class="col-md-3 col-12 cart-sidebar">
<!-- Top Section: Customer & Type -->
<div class="p-3 border-bottom bg-white">
<!-- Order Type -->
<div class="btn-group w-100 mb-3" role="group">
<input type="radio" class="btn-check" name="order_type" id="ot-dine-in" value="dine-in" <?= $order_type === 'dine-in' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary btn-sm" for="ot-dine-in">Dine-In</label>
<input type="radio" class="btn-check" name="order_type" id="ot-takeaway" value="takeaway" <?= $order_type === 'takeaway' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary btn-sm" for="ot-takeaway">Takeaway</label>
<input type="radio" class="btn-check" name="order_type" id="ot-delivery" value="delivery" <?= $order_type === 'delivery' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary btn-sm" for="ot-delivery">Delivery</label>
</div>
<!-- Customer Search -->
<div class="position-relative">
<div class="input-group">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-person"></i></span>
<input type="text" class="form-control border-start-0 ps-0" id="customer-search" placeholder="Search Customer..." autocomplete="off">
<button class="btn btn-outline-secondary d-none" type="button" id="clear-customer"><i class="bi bi-x"></i></button>
</div>
<div class="list-group shadow-sm search-dropdown" id="customer-results"></div>
<input type="hidden" id="selected-customer-id">
<div id="customer-info" class="small text-success mt-1 d-none">
<i class="bi bi-check-circle-fill me-1"></i> <span id="customer-name-display"></span>
</div>
</div>
</div>
<!-- Cart Items (Flex Grow) -->
<div class="flex-grow-1 overflow-auto p-3 bg-white" id="cart-items">
<div class="text-center text-muted mt-5">
<i class="bi bi-basket3 fs-1 text-light"></i>
<p class="mt-2">Cart is empty</p>
</div>
</div>
<!-- Bottom: Totals & Action -->
<div class="p-3 border-top bg-light">
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Subtotal</span>
<span class="fw-bold" id="cart-subtotal"><?= format_currency(0) ?></span>
</div>
<!-- Discount Field -->
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">Discount</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">
</div>
</div>
<div class="d-flex justify-content-between mb-3">
<span class="fs-5 fw-bold">Total</span>
<span class="fs-4 fw-bold text-primary" id="cart-total-price"><?= format_currency(0) ?></span>
</div>
<button class="btn btn-primary w-100 btn-lg shadow-sm" id="checkout-btn" disabled>
Place Order <i class="bi bi-arrow-right ms-2"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container position-fixed bottom-0 start-50 translate-middle-x p-3" id="toast-container" style="z-index: 1060;"></div>
<!-- Table Selection Modal -->
<div class="modal fade" id="tableSelectionModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold">Select Table</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="close-table-modal" style="display:none;"></button>
</div>
<div class="modal-body bg-light">
<div id="table-list-container" class="row g-3"></div>
</div>
</div>
</div>
</div>
<!-- Variant Selection Modal -->
<div class="modal fade" id="variantSelectionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="variantModalTitle">Select Option</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="variant-list" class="list-group">
<!-- Variants injected by JS -->
</div>
</div>
</div>
</div>
</div>
<script>
const COMPANY_SETTINGS = <?= json_encode($settings) ?>;
const PRODUCT_VARIANTS = <?= json_encode($variants_by_product) ?>;
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>
</body>
</html>