Autosave: 20260222-101338

This commit is contained in:
Flatlogic Bot 2026-02-22 10:13:38 +00:00
parent 38e82cb192
commit 3ce9ce30e6
23 changed files with 1990 additions and 426 deletions

107
admin/areas.php Normal file
View File

@ -0,0 +1,107 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_area') {
$stmt = $pdo->prepare("INSERT INTO areas (outlet_id, name) VALUES (?, ?)");
$stmt->execute([$_POST['outlet_id'], $_POST['name']]);
header("Location: areas.php");
exit;
}
if (isset($_GET['delete'])) {
$pdo->prepare("DELETE FROM areas WHERE id = ?")->execute([$_GET['delete']]);
header("Location: areas.php");
exit;
}
// Fetch areas with outlet names
$areas = $pdo->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();
// Fetch outlets for dropdown
$outlets = $pdo->query("SELECT id, name FROM outlets ORDER BY name ASC")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Areas</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addAreaModal">
<i class="bi bi-plus-lg"></i> Add Area
</button>
</div>
<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">ID</th>
<th>Name</th>
<th>Outlet</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($areas as $area): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $area['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($area['name']) ?></td>
<td><span class="badge bg-info text-dark"><?= htmlspecialchars($area['outlet_name'] ?? 'N/A') ?></span></td>
<td>
<a href="?delete=<?= $area['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this area?')"><i class="bi bi-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($areas)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted">No areas found. Add one to get started.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Add Area Modal -->
<div class="modal fade" id="addAreaModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Area</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="add_area">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" placeholder="e.g. Main Hall, Patio" required>
</div>
<div class="mb-3">
<label class="form-label">Outlet</label>
<select name="outlet_id" class="form-select" required>
<option value="">Select Outlet</option>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>"><?= htmlspecialchars($outlet['name']) ?></option>
<?php endforeach; ?>
</select>
</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 Area</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

82
admin/categories.php Normal file
View File

@ -0,0 +1,82 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if (isset($_POST['action']) && $_POST['action'] === 'add_category') {
$stmt = $pdo->prepare("INSERT INTO categories (name) VALUES (?)");
$stmt->execute([$_POST['name']]);
header("Location: categories.php");
exit;
}
if (isset($_GET['delete'])) {
$pdo->prepare("DELETE FROM categories WHERE id = ?")->execute([$_GET['delete']]);
header("Location: categories.php");
exit;
}
$categories = $pdo->query("SELECT * FROM categories ORDER BY sort_order")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Categories</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCategoryModal">
<i class="bi bi-plus-lg"></i> Add Category
</button>
</div>
<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">ID</th>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $cat): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $cat['id'] ?></td>
<td><?= htmlspecialchars($cat['name']) ?></td>
<td>
<a href="?delete=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure? This might break products linked to this category.')"><i class="bi bi-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Add Category Modal -->
<div class="modal fade" id="addCategoryModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Category</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="add_category">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Category</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

107
admin/company.php Normal file
View File

@ -0,0 +1,107 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
$settings = get_company_settings();
// Handle Update
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$company_name = $_POST['company_name'] ?? '';
$address = $_POST['address'] ?? '';
$phone = $_POST['phone'] ?? '';
$email = $_POST['email'] ?? '';
$vat_rate = $_POST['vat_rate'] ?? 0;
$currency_symbol = $_POST['currency_symbol'] ?? '$';
$currency_decimals = $_POST['currency_decimals'] ?? 2;
try {
// Check if row exists (it should, from our functions.php logic or migration)
$exists = $pdo->query("SELECT COUNT(*) FROM company_settings")->fetchColumn();
if ($exists) {
$stmt = $pdo->prepare("UPDATE company_settings SET company_name=?, address=?, phone=?, email=?, vat_rate=?, currency_symbol=?, currency_decimals=?, updated_at=NOW()");
$stmt->execute([$company_name, $address, $phone, $email, $vat_rate, $currency_symbol, $currency_decimals]);
} else {
$stmt = $pdo->prepare("INSERT INTO company_settings (company_name, address, phone, email, vat_rate, currency_symbol, currency_decimals) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$company_name, $address, $phone, $email, $vat_rate, $currency_symbol, $currency_decimals]);
}
$message = '<div class="alert alert-success">Company settings updated successfully!</div>';
// Refresh settings
$settings = [
'company_name' => $company_name,
'address' => $address,
'phone' => $phone,
'email' => $email,
'vat_rate' => $vat_rate,
'currency_symbol' => $currency_symbol,
'currency_decimals' => $currency_decimals
];
} catch (Exception $e) {
$message = '<div class="alert alert-danger">Error updating settings: ' . htmlspecialchars($e->getMessage()) . '</div>';
}
}
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Company Profile</h2>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="POST">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Company Name</label>
<input type="text" name="company_name" class="form-control" value="<?= htmlspecialchars($settings['company_name'] ?? '') ?>" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($settings['email'] ?? '') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Phone</label>
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars($settings['phone'] ?? '') ?>">
</div>
<div class="col-md-12 mb-3">
<label class="form-label">Address</label>
<textarea name="address" class="form-control" rows="3"><?= htmlspecialchars($settings['address'] ?? '') ?></textarea>
</div>
</div>
<hr class="my-4">
<h5 class="mb-3">Financial Settings</h5>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">VAT Rate (%)</label>
<div class="input-group">
<input type="number" step="0.01" name="vat_rate" class="form-control" value="<?= htmlspecialchars($settings['vat_rate'] ?? 0) ?>">
<span class="input-group-text">%</span>
</div>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Currency Symbol</label>
<input type="text" name="currency_symbol" class="form-control" value="<?= htmlspecialchars($settings['currency_symbol'] ?? '$') ?>" placeholder="e.g. $, €, £">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Decimal Places</label>
<input type="number" name="currency_decimals" class="form-control" value="<?= htmlspecialchars($settings['currency_decimals'] ?? 2) ?>" min="0" max="4">
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -0,0 +1,5 @@
</div> <!-- End Main Content -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

116
admin/includes/header.php Normal file
View File

@ -0,0 +1,116 @@
<?php
// admin/includes/header.php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Simple active link helper
function isActive($page) {
return basename($_SERVER['PHP_SELF']) === $page ? 'active' : '';
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Foody Admin Panel</title>
<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.0/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;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>">
<style>
body { font-family: 'Inter', sans-serif; background-color: #f8f9fa; }
.sidebar { height: 100vh; position: fixed; top: 0; left: 0; width: 250px; background: #fff; border-right: 1px solid #eee; padding-top: 1rem; z-index: 1000; }
.sidebar .nav-link { color: #555; padding: 0.75rem 1.5rem; font-weight: 500; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #FF6B6B; background: #FFF0F0; border-right: 3px solid #FF6B6B; }
.main-content { margin-left: 250px; padding: 2rem; }
@media (max-width: 768px) {
.sidebar { transform: translateX(-100%); transition: transform 0.3s ease; }
.sidebar.show { transform: translateX(0); }
.main-content { margin-left: 0; }
}
.stat-card { border: none; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.04); transition: transform 0.2s; }
.stat-card:hover { transform: translateY(-3px); }
.icon-box { width: 48px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; }
</style>
</head>
<body>
<!-- Mobile Header -->
<nav class="navbar navbar-light bg-white border-bottom d-md-none fixed-top">
<div class="container-fluid">
<a class="navbar-brand fw-bold text-dark" href="index.php">Foody<span class="text-primary">Admin</span></a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<!-- Sidebar -->
<div class="offcanvas-md offcanvas-start sidebar" tabindex="-1" id="sidebarMenu">
<div class="d-flex align-items-center justify-content-between px-4 pb-3 border-bottom d-none d-md-flex">
<a href="index.php" class="text-decoration-none">
<h4 class="fw-bold m-0 text-dark">Foody<span style="color: #FF6B6B;">.</span></h4>
</a>
</div>
<div class="px-4 py-3 d-md-none">
<h5 class="fw-bold">Menu</h5>
</div>
<ul class="nav flex-column mt-2">
<li class="nav-item">
<a class="nav-link <?= isActive('index.php') ?>" href="index.php">
<i class="bi bi-grid me-2"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('orders.php') ?>" href="orders.php">
<i class="bi bi-receipt me-2"></i> Orders (POS)
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('products.php') ?>" href="products.php">
<i class="bi bi-box-seam me-2"></i> Products
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('categories.php') ?>" href="categories.php">
<i class="bi bi-tags me-2"></i> Categories
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php">
<i class="bi bi-shop me-2"></i> Outlets
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('areas.php') ?>" href="areas.php">
<i class="bi bi-geo-alt me-2"></i> Areas
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('tables.php') ?>" href="tables.php">
<i class="bi bi-ui-checks-grid me-2"></i> Tables
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('loyalty.php') ?>" href="loyalty.php">
<i class="bi bi-people me-2"></i> Loyalty
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('company.php') ?>" href="company.php">
<i class="bi bi-building me-2"></i> Company
</a>
</li>
<li class="nav-item mt-4 border-top pt-2">
<a class="nav-link text-muted" href="../index.php" target="_blank">
<i class="bi bi-box-arrow-up-right me-2"></i> View Site
</a>
</li>
</ul>
</div>
<div class="main-content pt-5 pt-md-4">

166
admin/index.php Normal file
View File

@ -0,0 +1,166 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
// Fetch Dashboard Stats
$today = date('Y-m-d');
// Total Revenue Today
$stmt = $pdo->prepare("SELECT SUM(total_amount) FROM orders WHERE DATE(created_at) = ? AND status != 'cancelled'");
$stmt->execute([$today]);
$revenueToday = $stmt->fetchColumn() ?: 0;
// Total Orders Today
$stmt = $pdo->prepare("SELECT COUNT(*) FROM orders WHERE DATE(created_at) = ?");
$stmt->execute([$today]);
$ordersToday = $stmt->fetchColumn();
// Active Outlets
$outletsCount = $pdo->query("SELECT COUNT(*) FROM outlets")->fetchColumn();
// Total Products
$productsCount = $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
// Recent Orders
$recentOrders = $pdo->query("SELECT o.*,
(SELECT GROUP_CONCAT(p.name SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items
FROM orders o ORDER BY created_at DESC LIMIT 5")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">Dashboard</h2>
<p class="text-muted">Welcome back, Admin!</p>
</div>
<div>
<a href="orders.php" class="btn btn-primary"><i class="bi bi-plus-lg me-1"></i> New Order</a>
</div>
</div>
<div class="row g-4 mb-4">
<!-- Revenue Card -->
<div class="col-md-3">
<div class="card stat-card h-100 p-3">
<div class="d-flex align-items-center">
<div class="icon-box bg-success bg-opacity-10 text-success me-3">
<i class="bi bi-currency-dollar"></i>
</div>
<div>
<h6 class="text-muted mb-0">Today's Revenue</h6>
<h3 class="fw-bold mb-0">$<?= number_format($revenueToday, 2) ?></h3>
</div>
</div>
</div>
</div>
<!-- Orders Card -->
<div class="col-md-3">
<div class="card stat-card h-100 p-3">
<div class="d-flex align-items-center">
<div class="icon-box bg-primary bg-opacity-10 text-primary me-3">
<i class="bi bi-receipt"></i>
</div>
<div>
<h6 class="text-muted mb-0">Orders Today</h6>
<h3 class="fw-bold mb-0"><?= $ordersToday ?></h3>
</div>
</div>
</div>
</div>
<!-- Outlets Card -->
<div class="col-md-3">
<div class="card stat-card h-100 p-3">
<div class="d-flex align-items-center">
<div class="icon-box bg-warning bg-opacity-10 text-warning me-3">
<i class="bi bi-shop"></i>
</div>
<div>
<h6 class="text-muted mb-0">Active Outlets</h6>
<h3 class="fw-bold mb-0"><?= $outletsCount ?></h3>
</div>
</div>
</div>
</div>
<!-- Products Card -->
<div class="col-md-3">
<div class="card stat-card h-100 p-3">
<div class="d-flex align-items-center">
<div class="icon-box bg-info bg-opacity-10 text-info me-3">
<i class="bi bi-box-seam"></i>
</div>
<div>
<h6 class="text-muted mb-0">Total Products</h6>
<h3 class="fw-bold mb-0"><?= $productsCount ?></h3>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Orders Table -->
<div class="card border-0 shadow-sm rounded-3">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 fw-bold">Recent Orders</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Type</th>
<th>Table/Customer</th>
<th>Total</th>
<th>Status</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentOrders as $order): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $order['id'] ?></td>
<td>
<?php
$badge = match($order['order_type']) {
'dine-in' => 'bg-info',
'delivery' => 'bg-warning',
'drive-thru' => 'bg-purple', // custom class or just use primary
default => 'bg-secondary'
};
?>
<span class="badge <?= $badge ?> text-dark bg-opacity-25 border border-<?= str_replace('bg-', '', $badge) ?>"><?= ucfirst($order['order_type']) ?></span>
</td>
<td>
<?php if ($order['table_number']): ?>
Table <?= htmlspecialchars($order['table_number']) ?>
<?php else: ?>
<?= htmlspecialchars($order['customer_name'] ?? 'Guest') ?>
<?php endif; ?>
</td>
<td class="fw-bold">$<?= number_format((float)$order['total_amount'], 2) ?></td>
<td>
<span class="status-badge status-<?= $order['status'] ?> badge rounded-pill">
<?= ucfirst($order['status']) ?>
</span>
</td>
<td class="text-muted small"><?= date('M d, H:i', strtotime($order['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($recentOrders)): ?>
<tr><td colspan="6" class="text-center py-4 text-muted">No recent orders found.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="card-footer bg-white text-center py-3">
<a href="orders.php" class="text-decoration-none fw-medium">View All Orders</a>
</div>
</div>
<?php include 'includes/footer.php'; ?>

55
admin/loyalty.php Normal file
View File

@ -0,0 +1,55 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$customers = $pdo->query("SELECT * FROM loyalty_customers ORDER BY points DESC")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Loyalty Program</h2>
<button class="btn btn-outline-primary" disabled>
<i class="bi bi-gear"></i> Settings (Coming Soon)
</button>
</div>
<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">ID</th>
<th>Name</th>
<th>Email</th>
<th>Points Balance</th>
<th>Joined</th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $customer): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $customer['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($customer['name']) ?></td>
<td><a href="mailto:<?= htmlspecialchars($customer['email']) ?>" class="text-decoration-none"><?= htmlspecialchars($customer['email']) ?></a></td>
<td>
<span class="badge bg-warning text-dark border border-warning">
<i class="bi bi-star-fill small me-1"></i> <?= $customer['points'] ?> pts
</span>
</td>
<td class="text-muted small"><?= date('M d, Y', strtotime($customer['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($customers)): ?>
<tr>
<td colspan="5" class="text-center py-5 text-muted">No loyalty members yet.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

106
admin/orders.php Normal file
View File

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if (isset($_POST['action']) && $_POST['action'] === 'update_status') {
$order_id = $_POST['order_id'];
$new_status = $_POST['status'];
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
$stmt->execute([$new_status, $order_id]);
header("Location: orders.php");
exit;
}
$orders = $pdo->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();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Order Management</h2>
<span class="badge bg-success bg-opacity-10 text-success border border-success px-3 py-2 rounded-pill">
<i class="bi bi-circle-fill small me-1"></i> Live
</span>
</div>
<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">ID</th>
<th>Source</th>
<th>Items</th>
<th>Total</th>
<th>Status</th>
<th>Time</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach ($orders as $order): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $order['id'] ?></td>
<td>
<?php if ($order['table_number']): ?>
<span class="badge bg-secondary">Table <?= htmlspecialchars($order['table_number']) ?></span>
<?php else: ?>
<span class="badge bg-info text-dark"><?= ucfirst($order['order_type']) ?></span>
<?php endif; ?>
</td>
<td><small class="text-muted"><?= htmlspecialchars($order['items_summary']) ?></small></td>
<td class="fw-bold"><?= format_currency($order['total_amount']) ?></td>
<td>
<span class="badge rounded-pill status-<?= $order['status'] ?>">
<?= ucfirst($order['status']) ?>
</span>
</td>
<td class="text-muted small"><?= date('H:i', strtotime($order['created_at'])) ?></td>
<td>
<form method="POST" class="d-flex gap-2">
<input type="hidden" name="order_id" value="<?= $order['id'] ?>">
<input type="hidden" name="action" value="update_status">
<?php if ($order['status'] === 'pending'): ?>
<button type="submit" name="status" value="preparing" class="btn btn-sm btn-primary">
<i class="bi bi-play-fill"></i> Start
</button>
<button type="submit" name="status" value="cancelled" class="btn btn-sm btn-outline-danger">
<i class="bi bi-x"></i>
</button>
<?php elseif ($order['status'] === 'preparing'): ?>
<button type="submit" name="status" value="ready" class="btn btn-sm btn-warning text-dark">
<i class="bi bi-check-circle"></i> Ready
</button>
<?php elseif ($order['status'] === 'ready'): ?>
<button type="submit" name="status" value="completed" class="btn btn-sm btn-success">
<i class="bi bi-check-all"></i> Complete
</button>
<?php else: ?>
<span class="text-muted small">-</span>
<?php endif; ?>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<tr>
<td colspan="7" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
No active orders.
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

75
admin/outlet_edit.php Normal file
View File

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

89
admin/outlets.php Normal file
View File

@ -0,0 +1,89 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if (isset($_POST['action']) && $_POST['action'] === 'add_outlet') {
$stmt = $pdo->prepare("INSERT INTO outlets (name, address) VALUES (?, ?)");
$stmt->execute([$_POST['name'], $_POST['address']]);
header("Location: outlets.php");
exit;
}
if (isset($_GET['delete'])) {
$pdo->prepare("DELETE FROM outlets WHERE id = ?")->execute([$_GET['delete']]);
header("Location: outlets.php");
exit;
}
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY id DESC")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Outlets</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addOutletModal">
<i class="bi bi-plus-lg"></i> Add Outlet
</button>
</div>
<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">ID</th>
<th>Name</th>
<th>Address</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($outlets as $outlet): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $outlet['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($outlet['name']) ?></td>
<td><small class="text-muted"><?= htmlspecialchars($outlet['address']) ?></small></td>
<td>
<a href="outlet_edit.php?id=<?= $outlet['id'] ?>" class="btn btn-sm btn-outline-secondary me-1"><i class="bi bi-pencil"></i></a>
<a href="?delete=<?= $outlet['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this outlet?')"><i class="bi bi-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Add Outlet Modal -->
<div class="modal fade" id="addOutletModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Outlet</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="add_outlet">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Address</label>
<textarea name="address" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Outlet</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

136
admin/product_edit.php Normal file
View File

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

137
admin/product_variants.php Normal file
View File

@ -0,0 +1,137 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if (!isset($_GET['product_id'])) {
header("Location: products.php");
exit;
}
$product_id = $_GET['product_id'];
// Fetch Product Details
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch();
if (!$product) {
header("Location: products.php");
exit;
}
// Handle Add Variant
if (isset($_POST['action']) && $_POST['action'] === 'add_variant') {
$name = $_POST['name'];
$price_adj = $_POST['price_adjustment'];
$stmt = $pdo->prepare("INSERT INTO product_variants (product_id, name, price_adjustment) VALUES (?, ?, ?)");
$stmt->execute([$product_id, $name, $price_adj]);
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']]);
header("Location: product_variants.php?product_id=$product_id");
exit;
}
$variants = $pdo->prepare("SELECT * FROM product_variants WHERE product_id = ? ORDER BY price_adjustment ASC");
$variants->execute([$product_id]);
$variants = $variants->fetchAll();
include 'includes/header.php';
?>
<div class="mb-4">
<a href="products.php" class="text-decoration-none text-muted mb-2 d-inline-block"><i class="bi bi-arrow-left"></i> Back to Products</a>
<div class="d-flex justify-content-between align-items-center">
<div>
<h2 class="fw-bold mb-0">Variants: <?= htmlspecialchars($product['name']) ?></h2>
<p class="text-muted mb-0">Manage sizes, extras, or options.</p>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addVariantModal">
<i class="bi bi-plus-lg"></i> Add Variant
</button>
</div>
</div>
<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">Variant Name</th>
<th>Price Adjustment</th>
<th>Final Price (Est.)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($variants as $variant): ?>
<tr>
<td class="ps-4 fw-medium"><?= htmlspecialchars($variant['name']) ?></td>
<td>
<?php if ($variant['price_adjustment'] > 0): ?>
<span class="text-danger">+ <?= format_currency($variant['price_adjustment']) ?></span>
<?php elseif ($variant['price_adjustment'] < 0): ?>
<span class="text-success">- <?= format_currency(abs((float)$variant['price_adjustment'])) ?></span>
<?php else: ?>
<span class="text-muted">No change</span>
<?php endif; ?>
</td>
<td class="text-muted">
<?= format_currency($product['price'] + $variant['price_adjustment']) ?>
</td>
<td>
<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>
<?php endforeach; ?>
<?php if (empty($variants)): ?>
<tr>
<td colspan="4" class="text-center py-5 text-muted">No variants defined (e.g., Small, Large, Spicy).</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Add Variant Modal -->
<div class="modal fade" id="addVariantModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add 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="add_variant">
<div class="mb-3">
<label class="form-label">Variant Name (e.g., Large, Extra Cheese)</label>
<input type="text" name="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" class="form-control" value="0.00" 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">Save Variant</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

160
admin/products.php Normal file
View File

@ -0,0 +1,160 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Add Product
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_product') {
$name = $_POST['name'];
$category_id = $_POST['category_id'];
$price = $_POST['price'];
$description = $_POST['description'];
// Image handling
$image_url = 'https://placehold.co/400x300?text=' . urlencode($name);
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/products/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$fileInfo = pathinfo($_FILES['image']['name']);
$fileExt = strtolower($fileInfo['extension']);
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($fileExt, $allowedExts)) {
$fileName = uniqid('prod_') . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
if (move_uploaded_file($_FILES['image']['tmp_name'], $targetFile)) {
$image_url = 'assets/images/products/' . $fileName;
}
}
}
$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>';
} else {
$message = '<div class="alert alert-danger">Error adding product.</div>';
}
}
// Handle Delete
if (isset($_GET['delete'])) {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM products WHERE id = ?")->execute([$id]);
header("Location: products.php");
exit;
}
// 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
$categories = $pdo->query("SELECT * FROM categories ORDER BY name")->fetchAll();
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
</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>
</div>
</div>
</div>
<!-- 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>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" enctype="multipart/form-data">
<div class="modal-body">
<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>
</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>
</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>
</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>
</form>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

113
admin/tables.php Normal file
View File

@ -0,0 +1,113 @@
<?php
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_table') {
$stmt = $pdo->prepare("INSERT INTO tables (area_id, name, capacity) VALUES (?, ?, ?)");
$stmt->execute([$_POST['area_id'], $_POST['name'], $_POST['capacity']]);
header("Location: tables.php");
exit;
}
if (isset($_GET['delete'])) {
$pdo->prepare("DELETE FROM tables WHERE id = ?")->execute([$_GET['delete']]);
header("Location: tables.php");
exit;
}
// Fetch tables with area names
$tables = $pdo->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();
// Fetch areas for dropdown
$areas = $pdo->query("SELECT id, name FROM areas ORDER BY name ASC")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Tables</h2>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addTableModal">
<i class="bi bi-plus-lg"></i> Add Table
</button>
</div>
<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">ID</th>
<th>Name</th>
<th>Area</th>
<th>Capacity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($tables as $table): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $table['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($table['name']) ?></td>
<td><span class="badge bg-secondary"><?= htmlspecialchars($table['area_name'] ?? 'N/A') ?></span></td>
<td><?= htmlspecialchars($table['capacity']) ?> pax</td>
<td>
<a href="?delete=<?= $table['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this table?')"><i class="bi bi-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($tables)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">No tables found. Add one to get started.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Add Table Modal -->
<div class="modal fade" id="addTableModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Table</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="add_table">
<div class="mb-3">
<label class="form-label">Table Name/Number</label>
<input type="text" name="name" class="form-control" placeholder="e.g. T1, Window 5" required>
</div>
<div class="mb-3">
<label class="form-label">Area</label>
<select name="area_id" class="form-select" required>
<option value="">Select Area</option>
<?php foreach ($areas as $area): ?>
<option value="<?= $area['id'] ?>"><?= htmlspecialchars($area['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Capacity (Pax)</label>
<input type="number" name="capacity" class="form-control" value="4" min="1" required>
</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 Table</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

32
api/order.php Normal file
View File

@ -0,0 +1,32 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
echo json_encode(['success' => false, 'error' => 'No data provided']);
exit;
}
try {
$pdo = db();
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_number, total_amount, status) VALUES (?, ?, ?, 'pending')");
$stmt->execute([1, $data['table_number'] ?? '1', $data['total_amount']]);
$order_id = $pdo->lastInsertId();
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (?, ?, ?, ?)");
foreach ($data['items'] as $item) {
$item_stmt->execute([$order_id, $item['id'], $item['quantity'], $item['price']]);
}
$pdo->commit();
echo json_encode(['success' => true, 'order_id' => $order_id]);
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -1,302 +1,165 @@
:root {
--primary-color: #1A1A1A;
--accent-color: #E63946;
--secondary-bg: #F5F5F5;
--text-primary: #1A1A1A;
--text-secondary: #666666;
--white: #FFFFFF;
--border-radius: 8px;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
body { body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px; background-color: var(--secondary-bg);
color: var(--text-primary);
line-height: 1.5;
margin: 0; margin: 0;
min-height: 100vh; padding: 0;
} }
.main-wrapper { .navbar {
display: flex; background-color: var(--white);
align-items: center; border-bottom: 1px solid #EAEAEA;
justify-content: center; padding: 1rem 0;
min-height: 100vh; }
.brand-logo {
font-weight: 700;
font-size: 1.5rem;
color: var(--primary-color);
text-decoration: none;
letter-spacing: -0.5px;
}
.menu-category-title {
font-weight: 700;
margin: 2rem 0 1rem;
font-size: 1.25rem;
border-bottom: 2px solid var(--accent-color);
display: inline-block;
}
.product-card {
background: var(--white);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow);
transition: transform 0.2s ease;
border: 1px solid #EAEAEA;
height: 100%;
}
.product-card:hover {
transform: translateY(-4px);
}
.product-image {
width: 100%; width: 100%;
padding: 20px; height: 180px;
box-sizing: border-box; background-color: #EEEEEE;
position: relative; object-fit: cover;
z-index: 1;
} }
@keyframes gradient { .product-info {
0% { padding: 1.25rem;
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
} }
.chat-container { .product-name {
width: 100%; font-weight: 600;
max-width: 600px; font-size: 1.1rem;
background: rgba(255, 255, 255, 0.85); margin-bottom: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.3); }
border-radius: 20px;
.product-desc {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1rem;
min-height: 3rem;
}
.product-price {
font-weight: 700;
color: var(--accent-color);
font-size: 1.1rem;
}
.btn-add {
background-color: var(--primary-color);
color: var(--white);
border-radius: var(--border-radius);
padding: 0.5rem 1rem;
font-weight: 600;
border: none;
}
.btn-add:hover {
background-color: #333333;
}
.cart-sidebar {
background: var(--white);
height: 100vh;
position: sticky;
top: 0;
border-left: 1px solid #EAEAEA;
padding: 2rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 85vh;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
overflow: hidden;
} }
.chat-header { .cart-item {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
font-weight: 700;
font-size: 1.1rem;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #EEEEEE;
} }
.chat-messages { .cart-item-name {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.message {
max-width: 85%;
padding: 0.85rem 1.1rem;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
color: #fff;
border-bottom-right-radius: 4px;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
}
.chat-input-area input {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 0.75rem 1rem;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
}
.chat-input-area button {
background: #212529;
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.blob {
position: absolute;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
filter: blur(80px);
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@keyframes move {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
}
.admin-link {
font-size: 14px;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.admin-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
}
/* Admin Styles */
.admin-container {
max-width: 900px;
margin: 3rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 1;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
} }
.form-control { .cart-item-price {
width: 100%; font-weight: 700;
padding: 0.75rem 1rem; font-size: 0.9rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
} }
.form-control:focus { .cart-total {
outline: none; margin-top: auto;
border-color: #23a6d5; padding-top: 1rem;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); border-top: 2px solid var(--primary-color);
font-weight: 700;
font-size: 1.25rem;
display: flex;
justify-content: space-between;
}
.order-badge {
background: var(--accent-color);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
}
/* Admin Styles */
.admin-table-container {
background: var(--white);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 1.5rem;
}
.status-pending { color: #FFA500; font-weight: 600; }
.status-preparing { color: #007BFF; font-weight: 600; }
.status-ready { color: #28A745; font-weight: 600; }
.toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1050;
} }

View File

@ -1,39 +1,131 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form'); let cart = [];
const chatInput = document.getElementById('chat-input'); const cartItemsContainer = document.getElementById('cart-items');
const chatMessages = document.getElementById('chat-messages'); const cartTotalPrice = document.getElementById('cart-total-price');
const checkoutBtn = document.getElementById('checkout-btn');
const mobileCartBtn = document.getElementById('mobile-cart-btn');
const appendMessage = (text, sender) => { // Helper for currency formatting
const msgDiv = document.createElement('div'); function formatCurrency(amount) {
msgDiv.classList.add('message', sender); // Fallback if settings not defined
msgDiv.textContent = text; const settings = (typeof COMPANY_SETTINGS !== 'undefined') ? COMPANY_SETTINGS : { currency_symbol: '$', currency_decimals: 2 };
chatMessages.appendChild(msgDiv); const symbol = settings.currency_symbol || '$';
chatMessages.scrollTop = chatMessages.scrollHeight; const decimals = parseInt(settings.currency_decimals || 2);
return symbol + parseFloat(amount).toFixed(decimals);
}
// Add to cart
document.querySelectorAll('.add-to-cart').forEach(button => {
button.addEventListener('click', (e) => {
const product = {
id: e.target.dataset.id,
name: e.target.dataset.name,
price: parseFloat(e.target.dataset.price),
quantity: 1
}; };
chatForm.addEventListener('submit', async (e) => { const existing = cart.find(item => item.id === product.id);
e.preventDefault(); if (existing) {
const message = chatInput.value.trim(); existing.quantity++;
if (!message) return; } else {
cart.push(product);
}
appendMessage(message, 'visitor'); updateCart();
chatInput.value = ''; showToast(`${product.name} added to cart!`);
});
});
try { function updateCart() {
const response = await fetch('api/chat.php', { if (cart.length === 0) {
cartItemsContainer.innerHTML = '<p class="text-center text-muted mt-5">Your cart is empty.</p>';
cartTotalPrice.innerText = formatCurrency(0);
checkoutBtn.disabled = true;
if (mobileCartBtn) mobileCartBtn.innerText = `View Cart (${formatCurrency(0)})`;
return;
}
cartItemsContainer.innerHTML = '';
let total = 0;
cart.forEach(item => {
const itemTotal = item.price * item.quantity;
total += itemTotal;
const div = document.createElement('div');
div.className = 'cart-item';
div.innerHTML = `
<div>
<div class="cart-item-name">${item.name} x${item.quantity}</div>
<div class="text-muted small">${formatCurrency(item.price)} ea</div>
</div>
<div class="cart-item-price">${formatCurrency(itemTotal)}</div>
`;
cartItemsContainer.appendChild(div);
});
cartTotalPrice.innerText = formatCurrency(total);
checkoutBtn.disabled = false;
if (mobileCartBtn) mobileCartBtn.innerText = `View Cart (${formatCurrency(total)})`;
}
// Checkout
checkoutBtn.addEventListener('click', () => {
if (cart.length === 0) return;
const table_id = new URLSearchParams(window.location.search).get('table') || '1';
// Calculate total
const totalAmount = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0);
const orderData = {
table_number: table_id,
total_amount: totalAmount,
// Only send necessary fields
items: cart.map(item => ({
product_id: item.id,
quantity: item.quantity,
unit_price: item.price
}))
};
// Use existing api/order.php
fetch('api/order.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }) body: JSON.stringify(orderData)
}); })
const data = await response.json(); .then(res => res.json())
.then(data => {
// Artificial delay for realism if (data.success) {
setTimeout(() => { cart = [];
appendMessage(data.reply, 'bot'); updateCart();
}, 500); showToast('Order placed successfully! Please wait for preparation.', 'success');
} catch (error) { } else {
console.error('Error:', error); showToast('Error: ' + (data.error || 'Unknown error'), 'danger');
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
} }
})
.catch(err => {
console.error(err);
showToast('Failed to place order.', 'danger');
}); });
});
function showToast(message, type = 'dark') {
const toastContainer = document.getElementById('toast-container');
if (!toastContainer) return;
const toastId = 'toast-' + Date.now();
const toastHtml = `
<div id="${toastId}" class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement);
toast.show();
toastElement.addEventListener('hidden.bs.toast', () => toastElement.remove());
}
}); });

15
db/company_settings.sql Normal file
View File

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS company_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
company_name VARCHAR(255) NOT NULL DEFAULT 'My Restaurant',
address TEXT,
phone VARCHAR(50),
email VARCHAR(255),
vat_rate DECIMAL(5, 2) DEFAULT 0.00,
currency_symbol VARCHAR(10) DEFAULT '$',
currency_decimals INT DEFAULT 2,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO company_settings (company_name, address, phone, vat_rate, currency_symbol, currency_decimals)
SELECT 'My Restaurant', '123 Food Street', '555-0199', 10.00, '$', 2
WHERE NOT EXISTS (SELECT 1 FROM company_settings);

View File

@ -15,3 +15,5 @@ function db() {
} }
return $pdo; return $pdo;
} }
require_once __DIR__ . '/../includes/functions.php';

39
db/init.php Normal file
View File

@ -0,0 +1,39 @@
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = db();
// Execute schema
$sql = file_get_contents(__DIR__ . '/schema.sql');
$pdo->exec($sql);
// Check if data exists
$stmt = $pdo->query("SELECT COUNT(*) FROM outlets");
if ($stmt->fetchColumn() == 0) {
// Seed Outlets
$pdo->exec("INSERT INTO outlets (name, address) VALUES ('Main Downtown', '123 Main St'), ('Westside Hub', '456 West Blvd')");
// Seed Categories
$pdo->exec("INSERT INTO categories (name, sort_order) VALUES ('Burgers', 1), ('Sides', 2), ('Drinks', 3)");
// Seed Products
$pdo->exec("INSERT INTO products (category_id, name, description, price) VALUES
(1, 'Signature Burger', 'Juicy beef patty with special sauce', 12.99),
(1, 'Veggie Delight', 'Plant-based patty with fresh avocado', 11.50),
(2, 'Truffle Fries', 'Crispy fries with truffle oil and parmesan', 5.99),
(3, 'Craft Cola', 'House-made sparkling cola', 3.50)");
// Seed Variants
$pdo->exec("INSERT INTO product_variants (product_id, name, price_adjustment) VALUES
(1, 'Double Patty', 4.00),
(1, 'Extra Cheese', 1.00),
(3, 'Large Portion', 2.00)");
echo "Database initialized and seeded successfully.";
} else {
echo "Database already initialized.";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}

82
db/schema.sql Normal file
View File

@ -0,0 +1,82 @@
CREATE TABLE IF NOT EXISTS outlets (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
address TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
sort_order INT DEFAULT 0
);
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
category_id INT,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
image_url VARCHAR(255),
FOREIGN KEY (category_id) REFERENCES categories(id)
);
CREATE TABLE IF NOT EXISTS product_variants (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT,
name VARCHAR(255) NOT NULL,
price_adjustment DECIMAL(10, 2) DEFAULT 0.00,
FOREIGN KEY (product_id) REFERENCES products(id)
);
CREATE TABLE IF NOT EXISTS areas (
id INT AUTO_INCREMENT PRIMARY KEY,
outlet_id INT,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (outlet_id) REFERENCES outlets(id)
);
CREATE TABLE IF NOT EXISTS tables (
id INT AUTO_INCREMENT PRIMARY KEY,
area_id INT,
name VARCHAR(50) NOT NULL,
capacity INT DEFAULT 4,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (area_id) REFERENCES areas(id)
);
CREATE TABLE IF NOT EXISTS orders (
id INT AUTO_INCREMENT PRIMARY KEY,
outlet_id INT,
table_id INT,
table_number VARCHAR(50),
order_type ENUM('dine-in', 'delivery', 'drive-thru') DEFAULT 'dine-in',
status ENUM('pending', 'preparing', 'ready', 'completed', 'cancelled') DEFAULT 'pending',
total_amount DECIMAL(10, 2) NOT NULL,
customer_name VARCHAR(255),
customer_phone VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (outlet_id) REFERENCES outlets(id),
FOREIGN KEY (table_id) REFERENCES tables(id)
);
CREATE TABLE IF NOT EXISTS order_items (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT,
product_id INT,
variant_id INT,
quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
-- Loyalty
CREATE TABLE IF NOT EXISTS loyalty_customers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255) UNIQUE,
points INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

34
includes/functions.php Normal file
View File

@ -0,0 +1,34 @@
<?php
// Function to fetch company settings from DB
function get_company_settings() {
static $settings = null;
if ($settings === null) {
$pdo = db();
try {
$stmt = $pdo->query("SELECT * FROM company_settings LIMIT 1");
$settings = $stmt->fetch(PDO::FETCH_ASSOC);
} catch (Exception $e) {
// Log error or ignore if table doesn't exist yet
}
// Default values if no settings found
if (!$settings) {
$settings = [
'company_name' => 'My Restaurant',
'address' => '123 Food Street',
'phone' => '555-0199',
'email' => 'info@restaurant.com',
'vat_rate' => 0.00,
'currency_symbol' => '$',
'currency_decimals' => 2
];
}
}
return $settings;
}
// Function to format currency using settings
function format_currency($amount) {
$settings = get_company_settings();
return $settings['currency_symbol'] . number_format((float)$amount, (int)$settings['currency_decimals']);
}

217
index.php
View File

@ -1,150 +1,101 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
@ini_set('display_errors', '1'); require_once __DIR__ . '/db/config.php';
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; $pdo = db();
$now = date('Y-m-d H:i:s'); $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> <!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>New Style</title> <title>Menu - <?= htmlspecialchars($settings['company_name']) ?></title>
<?php <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<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;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style> <link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head> </head>
<body> <body>
<main> <nav class="navbar">
<div class="card"> <div class="container d-flex justify-content-between align-items-center">
<h1>Analyzing your requirements and generating your website…</h1> <a href="index.php" class="brand-logo"><?= htmlspecialchars($settings['company_name']) ?></a>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <div class="d-flex align-items-center">
<span class="sr-only">Loading…</span> <span class="badge bg-dark me-3">Table <?= htmlspecialchars($table_id) ?></span>
<a href="admin/orders.php" class="btn btn-sm btn-outline-dark">Staff POS</a>
</div> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main> </nav>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <div class="container mt-4">
</footer> <div class="row">
<!-- Menu Column -->
<div class="col-lg-8">
<h1 class="mb-4 fw-bold">Order Now</h1>
<?php foreach ($categories as $category): ?>
<h2 class="menu-category-title"><?= htmlspecialchars($category['name']) ?></h2>
<div class="row g-4 mb-5">
<?php foreach ($all_products as $product):
if ($product['category_id'] == $category['id']): ?>
<div class="col-md-6 col-lg-4">
<div class="product-card">
<img src="https://picsum.photos/seed/<?= $product['id'] ?>/400/300" alt="<?= htmlspecialchars($product['name']) ?>" class="product-image">
<div class="product-info">
<h3 class="product-name"><?= htmlspecialchars($product['name']) ?></h3>
<p class="product-desc"><?= htmlspecialchars($product['description']) ?></p>
<div class="d-flex justify-content-between align-items-center">
<span class="product-price"><?= format_currency($product['price']) ?></span>
<button class="btn btn-add btn-sm add-to-cart"
data-id="<?= $product['id'] ?>"
data-name="<?= htmlspecialchars($product['name']) ?>"
data-price="<?= $product['price'] ?>">
Add to Cart
</button>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<!-- Cart Column -->
<div class="col-lg-4 d-none d-lg-block">
<div class="cart-sidebar">
<h4 class="fw-bold mb-4">Your Order</h4>
<div id="cart-items" class="flex-grow-1 overflow-auto">
<p class="text-center text-muted mt-5">Your cart is empty.</p>
</div>
<div class="cart-total mt-3">
<span>Total</span>
<span id="cart-total-price"><?= format_currency(0) ?></span>
</div>
<button class="btn btn-dark w-100 btn-lg mt-4" id="checkout-btn" disabled>Place Order</button>
</div>
</div>
</div>
</div>
<!-- Mobile Cart Trigger -->
<div class="fixed-bottom d-lg-none p-3 bg-white border-top">
<button class="btn btn-dark w-100 btn-lg" id="mobile-cart-btn">View Cart (<?= format_currency(0) ?>)</button>
</div>
<div class="toast-container" id="toast-container"></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>