Autosave: 20260222-135217
This commit is contained in:
parent
3d24190863
commit
5db529f225
@ -16,12 +16,13 @@ if (isset($_GET['delete'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch areas with outlet names
|
// Fetch areas with outlet names
|
||||||
$areas = $pdo->query("
|
$query = "SELECT areas.*, outlets.name as outlet_name
|
||||||
SELECT areas.*, outlets.name as outlet_name
|
|
||||||
FROM areas
|
FROM areas
|
||||||
LEFT JOIN outlets ON areas.outlet_id = outlets.id
|
LEFT JOIN outlets ON areas.outlet_id = outlets.id
|
||||||
ORDER BY areas.id DESC
|
ORDER BY areas.id DESC";
|
||||||
")->fetchAll();
|
|
||||||
|
$areas_pagination = paginate_query($pdo, $query);
|
||||||
|
$areas = $areas_pagination['data'];
|
||||||
|
|
||||||
// Fetch outlets for dropdown
|
// Fetch outlets for dropdown
|
||||||
$outlets = $pdo->query("SELECT id, name FROM outlets ORDER BY name ASC")->fetchAll();
|
$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 border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
@ -68,6 +73,10 @@ include 'includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bottom Pagination -->
|
||||||
|
<div class="p-3 border-top bg-light">
|
||||||
|
<?php render_pagination_controls($areas_pagination); ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,15 @@ require_once __DIR__ . '/../db/config.php';
|
|||||||
$pdo = db();
|
$pdo = db();
|
||||||
|
|
||||||
if (isset($_GET['delete'])) {
|
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");
|
header("Location: categories.php");
|
||||||
exit;
|
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';
|
include 'includes/header.php';
|
||||||
?>
|
?>
|
||||||
@ -22,6 +25,10 @@ include 'includes/header.php';
|
|||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
@ -65,6 +72,10 @@ include 'includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bottom Pagination -->
|
||||||
|
<div class="p-3 border-top bg-light">
|
||||||
|
<?php render_pagination_controls($categories_pagination); ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,9 @@ if (isset($_GET['delete'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch Customers
|
// 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';
|
include 'includes/header.php';
|
||||||
?>
|
?>
|
||||||
@ -44,6 +46,10 @@ include 'includes/header.php';
|
|||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
@ -78,6 +84,10 @@ include 'includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bottom Pagination -->
|
||||||
|
<div class="p-3 border-top bg-light">
|
||||||
|
<?php render_pagination_controls($customers_pagination); ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -90,6 +90,11 @@ function isActive($page) {
|
|||||||
<i class="bi bi-grid me-2"></i> Dashboard
|
<i class="bi bi-grid me-2"></i> Dashboard
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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">
|
<li class="nav-item">
|
||||||
<a class="nav-link <?= isActive('orders.php') ?>" href="orders.php">
|
<a class="nav-link <?= isActive('orders.php') ?>" href="orders.php">
|
||||||
<i class="bi bi-receipt me-2"></i> Orders (POS)
|
<i class="bi bi-receipt me-2"></i> Orders (POS)
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
require_once __DIR__ . '/../db/config.php';
|
require_once __DIR__ . '/../db/config.php';
|
||||||
$pdo = db();
|
$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';
|
include 'includes/header.php';
|
||||||
?>
|
?>
|
||||||
@ -16,6 +18,10 @@ include 'includes/header.php';
|
|||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
@ -49,7 +55,11 @@ include 'includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bottom Pagination -->
|
||||||
|
<div class="p-3 border-top bg-light">
|
||||||
|
<?php render_pagination_controls($customers_pagination); ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
<?php include 'includes/footer.php'; ?>
|
||||||
@ -13,10 +13,13 @@ if (isset($_POST['action']) && $_POST['action'] === 'update_status') {
|
|||||||
exit;
|
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
|
(SELECT GROUP_CONCAT(CONCAT(p.name, ' x', oi.quantity) SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items_summary
|
||||||
FROM orders o
|
FROM orders o
|
||||||
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';
|
include 'includes/header.php';
|
||||||
?>
|
?>
|
||||||
@ -30,6 +33,10 @@ include 'includes/header.php';
|
|||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
@ -113,7 +120,11 @@ include 'includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bottom Pagination -->
|
||||||
|
<div class="p-3 border-top bg-light">
|
||||||
|
<?php render_pagination_controls($orders_pagination); ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
<?php include 'includes/footer.php'; ?>
|
||||||
@ -15,7 +15,9 @@ if (isset($_GET['delete'])) {
|
|||||||
exit;
|
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';
|
include 'includes/header.php';
|
||||||
?>
|
?>
|
||||||
@ -29,6 +31,10 @@ include 'includes/header.php';
|
|||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
@ -54,6 +60,10 @@ include 'includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bottom Pagination -->
|
||||||
|
<div class="p-3 border-top bg-light">
|
||||||
|
<?php render_pagination_controls($outlets_pagination); ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,18 @@ if (isset($_POST['action']) && $_POST['action'] === 'add_variant') {
|
|||||||
exit;
|
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
|
// Handle Delete
|
||||||
if (isset($_GET['delete'])) {
|
if (isset($_GET['delete'])) {
|
||||||
$pdo->prepare("DELETE FROM product_variants WHERE id = ?")->execute([$_GET['delete']]);
|
$pdo->prepare("DELETE FROM product_variants WHERE id = ?")->execute([$_GET['delete']]);
|
||||||
@ -37,9 +49,9 @@ if (isset($_GET['delete'])) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$variants = $pdo->prepare("SELECT * FROM product_variants WHERE product_id = ? ORDER BY price_adjustment ASC");
|
$query = "SELECT * FROM product_variants WHERE product_id = ? ORDER BY price_adjustment ASC";
|
||||||
$variants->execute([$product_id]);
|
$variants_pagination = paginate_query($pdo, $query, [$product_id]);
|
||||||
$variants = $variants->fetchAll();
|
$variants = $variants_pagination['data'];
|
||||||
|
|
||||||
include 'includes/header.php';
|
include 'includes/header.php';
|
||||||
?>
|
?>
|
||||||
@ -59,6 +71,10 @@ include 'includes/header.php';
|
|||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
@ -86,6 +102,14 @@ include 'includes/header.php';
|
|||||||
<?= format_currency($product['price'] + $variant['price_adjustment']) ?>
|
<?= format_currency($product['price'] + $variant['price_adjustment']) ?>
|
||||||
</td>
|
</td>
|
||||||
<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>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -98,6 +122,10 @@ include 'includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bottom Pagination -->
|
||||||
|
<div class="p-3 border-top bg-light">
|
||||||
|
<?php render_pagination_controls($variants_pagination, ['product_id' => $product_id]); ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -134,4 +162,58 @@ include 'includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</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'; ?>
|
||||||
|
|||||||
@ -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 (?, ?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO products (name, category_id, price, description, image_url) VALUES (?, ?, ?, ?, ?)");
|
||||||
if ($stmt->execute([$name, $category_id, $price, $description, $image_url])) {
|
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 {
|
} 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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch Products with Category Name
|
// Fetch Categories for Dropdown (moved up for filter usage)
|
||||||
$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
|
|
||||||
$categories = $pdo->query("SELECT * FROM categories ORDER BY name")->fetchAll();
|
$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';
|
include 'includes/header.php';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2 class="fw-bold mb-0">Products</h2>
|
<div>
|
||||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addProductModal">
|
<h2 class="fw-bold mb-1 text-dark">Products</h2>
|
||||||
<i class="bi bi-plus-lg"></i> Add Product
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?= $message ?>
|
<?= $message ?>
|
||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<!-- Filter & Search Bar -->
|
||||||
<div class="card-body p-0">
|
<div class="filter-bar">
|
||||||
<div class="table-responsive">
|
<form method="GET" class="row g-3 align-items-center">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<div class="col-md-5">
|
||||||
<thead class="bg-light">
|
<div class="input-group">
|
||||||
<tr>
|
<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>
|
||||||
<th class="ps-4">Image</th>
|
<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;">
|
||||||
<th>Name</th>
|
</div>
|
||||||
<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>
|
|
||||||
</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>
|
</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 -->
|
<!-- Add Product Modal -->
|
||||||
<div class="modal fade" id="addProductModal" tabindex="-1">
|
<div class="modal fade" id="addProductModal" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content border-0 shadow-lg" style="border-radius: 16px;">
|
||||||
<div class="modal-header">
|
<div class="modal-header border-bottom-0 pb-0">
|
||||||
<h5 class="modal-title">Add New Product</h5>
|
<h5 class="modal-title fw-bold">Add New Product</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<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">
|
<input type="hidden" name="action" value="add_product">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Name</label>
|
<label class="form-label text-muted small fw-bold">PRODUCT NAME</label>
|
||||||
<input type="text" name="name" class="form-control" required>
|
<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>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Category</label>
|
<label class="form-label text-muted small fw-bold">DESCRIPTION</label>
|
||||||
<select name="category_id" class="form-select" required>
|
<textarea name="description" class="form-control" rows="3" placeholder="Brief description..." style="border-radius: 10px;"></textarea>
|
||||||
<?php foreach ($categories as $cat): ?>
|
|
||||||
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Price ($)</label>
|
<label class="form-label text-muted small fw-bold">IMAGE</label>
|
||||||
<input type="number" step="0.01" name="price" class="form-control" required>
|
<input type="file" name="image" class="form-control" accept="image/*" style="border-radius: 10px;">
|
||||||
</div>
|
<div class="form-text">Optional. Placeholder used if empty.</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer border-top-0 pt-0 pb-4 px-4">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary">Save Product</button>
|
<button type="submit" class="btn btn-primary rounded-pill px-4">Save Product</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,7 +30,9 @@ if (isset($_GET['delete'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch Suppliers
|
// 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';
|
include 'includes/header.php';
|
||||||
?>
|
?>
|
||||||
@ -46,6 +48,10 @@ include 'includes/header.php';
|
|||||||
|
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
@ -83,6 +89,10 @@ include 'includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bottom Pagination -->
|
||||||
|
<div class="p-3 border-top bg-light">
|
||||||
|
<?php render_pagination_controls($suppliers_pagination); ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -16,12 +16,13 @@ if (isset($_GET['delete'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch tables with area names
|
// Fetch tables with area names
|
||||||
$tables = $pdo->query("
|
$query = "SELECT tables.*, areas.name as area_name
|
||||||
SELECT tables.*, areas.name as area_name
|
|
||||||
FROM tables
|
FROM tables
|
||||||
LEFT JOIN areas ON tables.area_id = areas.id
|
LEFT JOIN areas ON tables.area_id = areas.id
|
||||||
ORDER BY tables.id DESC
|
ORDER BY tables.id DESC";
|
||||||
")->fetchAll();
|
|
||||||
|
$tables_pagination = paginate_query($pdo, $query);
|
||||||
|
$tables = $tables_pagination['data'];
|
||||||
|
|
||||||
// Fetch areas for dropdown
|
// Fetch areas for dropdown
|
||||||
$areas = $pdo->query("SELECT id, name FROM areas ORDER BY name ASC")->fetchAll();
|
$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 border-0 shadow-sm">
|
||||||
<div class="card-body p-0">
|
<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">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0">
|
||||||
<thead class="bg-light">
|
<thead class="bg-light">
|
||||||
@ -70,6 +75,10 @@ include 'includes/header.php';
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bottom Pagination -->
|
||||||
|
<div class="p-3 border-top bg-light">
|
||||||
|
<?php render_pagination_controls($tables_pagination); ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -48,7 +48,7 @@ try {
|
|||||||
|
|
||||||
// Customer Handling
|
// Customer Handling
|
||||||
$customer_id = $data['customer_id'] ?? null;
|
$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;
|
$customer_phone = $data['customer_phone'] ?? null;
|
||||||
|
|
||||||
if ($customer_id) {
|
if ($customer_id) {
|
||||||
@ -59,24 +59,27 @@ try {
|
|||||||
$customer_name = $cust['name'];
|
$customer_name = $cust['name'];
|
||||||
$customer_phone = $cust['phone'];
|
$customer_phone = $cust['phone'];
|
||||||
} else {
|
} else {
|
||||||
// Invalid customer ID, clear it but keep manual name if any (though unlikely combo)
|
|
||||||
$customer_id = null;
|
$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')");
|
$discount = isset($data['discount']) ? floatval($data['discount']) : 0.00;
|
||||||
$stmt->execute([1, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $data['total_amount'] ?? 0]);
|
$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();
|
$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'])) {
|
if (!empty($data['items']) && is_array($data['items'])) {
|
||||||
foreach ($data['items'] as $item) {
|
foreach ($data['items'] as $item) {
|
||||||
$pid = $item['product_id'] ?? ($item['id'] ?? null);
|
$pid = $item['product_id'] ?? ($item['id'] ?? null);
|
||||||
$qty = $item['quantity'] ?? 1;
|
$qty = $item['quantity'] ?? 1;
|
||||||
$price = $item['unit_price'] ?? ($item['price'] ?? 0);
|
$price = $item['unit_price'] ?? ($item['price'] ?? 0);
|
||||||
|
$vid = $item['variant_id'] ?? null;
|
||||||
|
|
||||||
if ($pid) {
|
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());
|
error_log("Order Error: " . $e->getMessage());
|
||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
@ -163,3 +163,94 @@ body {
|
|||||||
right: 2rem;
|
right: 2rem;
|
||||||
z-index: 1050;
|
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;
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const cartItemsContainer = document.getElementById('cart-items');
|
const cartItemsContainer = document.getElementById('cart-items');
|
||||||
const cartTotalPrice = document.getElementById('cart-total-price');
|
const cartTotalPrice = document.getElementById('cart-total-price');
|
||||||
const cartSubtotal = document.getElementById('cart-subtotal');
|
const cartSubtotal = document.getElementById('cart-subtotal');
|
||||||
|
const cartDiscountInput = document.getElementById('cart-discount-input');
|
||||||
const checkoutBtn = document.getElementById('checkout-btn');
|
const checkoutBtn = document.getElementById('checkout-btn');
|
||||||
|
|
||||||
// Table Management
|
// Table Management
|
||||||
@ -11,6 +12,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const tableDisplay = document.getElementById('current-table-display');
|
const tableDisplay = document.getElementById('current-table-display');
|
||||||
const tableModalEl = document.getElementById('tableSelectionModal');
|
const tableModalEl = document.getElementById('tableSelectionModal');
|
||||||
const tableSelectionModal = new bootstrap.Modal(tableModalEl);
|
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
|
// Customer Search Elements
|
||||||
const customerSearchInput = document.getElementById('customer-search');
|
const customerSearchInput = document.getElementById('customer-search');
|
||||||
@ -58,7 +64,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
btnElement.classList.add('active');
|
btnElement.classList.add('active');
|
||||||
} else if (typeof btnElement === 'undefined' && categoryId !== 'all') {
|
} else if (typeof btnElement === 'undefined' && categoryId !== 'all') {
|
||||||
// Try to find the button corresponding to this category ID
|
// Try to find the button corresponding to this category ID
|
||||||
// This might happen if triggered programmatically
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filterProducts();
|
filterProducts();
|
||||||
@ -219,24 +224,72 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// --- Cart Logic ---
|
// --- Cart Logic ---
|
||||||
document.querySelectorAll('.add-to-cart').forEach(card => {
|
document.querySelectorAll('.add-to-cart').forEach(card => {
|
||||||
card.addEventListener('click', (e) => {
|
card.addEventListener('click', (e) => {
|
||||||
// Find closest card just in case click was on child
|
|
||||||
const target = e.currentTarget;
|
const target = e.currentTarget;
|
||||||
const product = {
|
const product = {
|
||||||
id: target.dataset.id,
|
id: target.dataset.id,
|
||||||
name: target.dataset.name,
|
name: target.dataset.name,
|
||||||
price: parseFloat(target.dataset.price),
|
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) {
|
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) {
|
if (existing) {
|
||||||
existing.quantity++;
|
existing.quantity++;
|
||||||
} else {
|
} else {
|
||||||
cart.push(product);
|
// Clone to avoid reference issues
|
||||||
|
cart.push({...product});
|
||||||
}
|
}
|
||||||
updateCart();
|
updateCart();
|
||||||
}
|
}
|
||||||
@ -266,20 +319,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cartItemsContainer.innerHTML = '';
|
cartItemsContainer.innerHTML = '';
|
||||||
let total = 0;
|
let subtotal = 0;
|
||||||
|
|
||||||
cart.forEach((item, index) => {
|
cart.forEach((item, index) => {
|
||||||
const itemTotal = item.price * item.quantity;
|
const itemTotal = item.price * item.quantity;
|
||||||
total += itemTotal;
|
subtotal += itemTotal;
|
||||||
|
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'd-flex justify-content-between align-items-center mb-3 border-bottom pb-2';
|
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 = `
|
row.innerHTML = `
|
||||||
<div class="flex-grow-1 me-2">
|
<div class="flex-grow-1 me-2">
|
||||||
<div class="fw-bold text-truncate" style="max-width: 140px;">${item.name}</div>
|
<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>
|
||||||
<div class="d-flex align-items-center bg-light rounded px-1">
|
<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>
|
<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);
|
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);
|
cartTotalPrice.innerText = formatCurrency(total);
|
||||||
checkoutBtn.disabled = false;
|
checkoutBtn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cartDiscountInput) {
|
||||||
|
cartDiscountInput.addEventListener('input', updateCart);
|
||||||
|
}
|
||||||
|
|
||||||
window.removeFromCart = function(index) {
|
window.removeFromCart = function(index) {
|
||||||
cart.splice(index, 1);
|
cart.splice(index, 1);
|
||||||
updateCart();
|
updateCart();
|
||||||
@ -317,7 +381,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
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 custId = selectedCustomerId.value;
|
||||||
|
|
||||||
const orderData = {
|
const orderData = {
|
||||||
@ -325,14 +391,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
order_type: orderType,
|
order_type: orderType,
|
||||||
customer_id: custId || null,
|
customer_id: custId || null,
|
||||||
total_amount: totalAmount,
|
total_amount: totalAmount,
|
||||||
|
discount: discount,
|
||||||
items: cart.map(item => ({
|
items: cart.map(item => ({
|
||||||
product_id: item.id,
|
product_id: item.id,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
unit_price: item.price
|
unit_price: item.price,
|
||||||
|
variant_id: item.variant_id
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show loading state
|
|
||||||
checkoutBtn.disabled = true;
|
checkoutBtn.disabled = true;
|
||||||
checkoutBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Processing...';
|
checkoutBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Processing...';
|
||||||
|
|
||||||
@ -348,10 +415,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
cart = [];
|
cart = [];
|
||||||
|
cartDiscountInput.value = 0;
|
||||||
updateCart();
|
updateCart();
|
||||||
// Reset customer
|
|
||||||
if (clearCustomerBtn) clearCustomerBtn.click();
|
if (clearCustomerBtn) clearCustomerBtn.click();
|
||||||
|
|
||||||
showToast(`Order #${data.order_id} placed!`, 'success');
|
showToast(`Order #${data.order_id} placed!`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showToast(`Error: ${data.error}`, 'danger');
|
showToast(`Error: ${data.error}`, 'danger');
|
||||||
@ -381,4 +447,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
t.show();
|
t.show();
|
||||||
el.addEventListener('hidden.bs.toast', () => el.remove());
|
el.addEventListener('hidden.bs.toast', () => el.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize logic
|
||||||
|
checkOrderType();
|
||||||
});
|
});
|
||||||
@ -32,3 +32,152 @@ function format_currency($amount) {
|
|||||||
$settings = get_company_settings();
|
$settings = get_company_settings();
|
||||||
return $settings['currency_symbol'] . number_format((float)$amount, (int)$settings['currency_decimals']);
|
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'>«</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'>»</a></li>";
|
||||||
|
|
||||||
|
echo '</ul></nav>';
|
||||||
|
} else {
|
||||||
|
echo '<small class="text-muted">Total: ' . $pagination['total_rows'] . '</small>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
293
index.php
293
index.php
@ -1,211 +1,130 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
require_once __DIR__ . '/db/config.php';
|
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();
|
$settings = get_company_settings();
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><?= htmlspecialchars($settings['company_name']) ?> - POS</title>
|
<title><?= htmlspecialchars($settings['company_name']) ?> - Welcome</title>
|
||||||
<?php if (!empty($settings['favicon_url'])): ?>
|
<?php if (!empty($settings['favicon_url'])): ?>
|
||||||
<link rel="icon" href="<?= htmlspecialchars($settings['favicon_url']) ?>">
|
<link rel="icon" href="<?= htmlspecialchars($settings['favicon_url']) ?>">
|
||||||
<?php endif; ?>
|
<?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@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 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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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() ?>">
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
||||||
<style>
|
<style>
|
||||||
body { height: 100vh; overflow: hidden; } /* Fix body for scrolling areas */
|
body {
|
||||||
.scrollable-y { overflow-y: auto; height: 100%; scrollbar-width: thin; }
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
.category-sidebar { height: calc(100vh - 60px); background: #f8f9fa; }
|
min-height: 100vh;
|
||||||
.product-area { height: calc(100vh - 60px); background: #fff; }
|
display: flex;
|
||||||
.cart-sidebar { height: calc(100vh - 60px); background: #fff; border-left: 1px solid #dee2e6; display: flex; flex-direction: column; }
|
flex-direction: column;
|
||||||
.product-card { transition: transform 0.1s; cursor: pointer; }
|
justify-content: center;
|
||||||
.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; }
|
.hero-card {
|
||||||
.category-btn:hover { background-color: #e9ecef; }
|
background: rgba(255, 255, 255, 0.9);
|
||||||
.category-btn.active { background-color: #0d6efd; color: white; }
|
border-radius: 24px;
|
||||||
.search-dropdown { position: absolute; width: 100%; z-index: 1000; max-height: 200px; overflow-y: auto; display: none; }
|
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
|
||||||
</style>
|
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>
|
</head>
|
||||||
<body>
|
<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="container">
|
||||||
<div class="row g-0">
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 col-lg-6">
|
||||||
<!-- Left Sidebar: Categories -->
|
<div class="hero-card p-5 text-center">
|
||||||
<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>
|
<?php if (!empty($settings['logo_url'])): ?>
|
||||||
<button class="category-btn active mb-1" onclick="filterCategory('all', this)">
|
<img src="<?= htmlspecialchars($settings['logo_url']) ?>" alt="Logo" class="company-logo">
|
||||||
<i class="bi bi-grid me-2"></i> All Items
|
<?php else: ?>
|
||||||
</button>
|
<div class="mb-4">
|
||||||
<?php foreach ($categories as $category): ?>
|
<i class="bi bi-shop fs-1 text-primary"></i>
|
||||||
<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>
|
</div>
|
||||||
<div class="card-body p-2">
|
<?php endif; ?>
|
||||||
<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>
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right: Cart & Order Info -->
|
<div class="mt-5 pt-4 border-top">
|
||||||
<div class="col-md-3 col-12 cart-sidebar">
|
<a href="admin/" class="text-muted small text-decoration-none">
|
||||||
|
<i class="bi bi-shield-lock me-1"></i> Staff Login
|
||||||
<!-- Top Section: Customer & Type -->
|
</a>
|
||||||
<div class="p-3 border-bottom bg-white">
|
</div>
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<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>
|
</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>
|
||||||
<div class="modal-body bg-light">
|
|
||||||
<div id="table-list-container" class="row g-3"></div>
|
|
||||||
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
268
pos.php
Normal file
268
pos.php
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user