Add online menu management, modernized and hardened
Adds a public menu page (menu.php) and an admin CRUD interface (admin/menu.php + admin/menu.js) backed by new menu_categories and menu_items tables, replacing the "menu lives on Facebook" placeholder on the homepage. The admin JS/PHP came out of an AI-assisted pass that used outdated JS (var, .then() chains, string-built HTML) and had some rough edges security-wise, so this also: - Rewrites the admin JS to modern ES (const/let, async/await, template literals, dataset API) and splits it out of admin/menu.php into admin/menu.js for proper separation of concerns - Adds declare(strict_types=1) and PHP 8 match-based action dispatch in place of an if-chain - Validates category_id/item_id/category_id server-side before use, so a bad ID returns a clean JSON error instead of an uncaught PDOException or a null item breaking the client - Rejects non-string POST fields instead of crashing trim() on them - Replaces the category-save handler's full-page location.reload() with in-place DOM updates (panel insert/update, resort by sort_order, keep the item modal's category dropdown in sync), so every admin action is now AJAX with no reload - Sets data-name/data-desc via dataset assignment rather than string- interpolating them into an HTML attribute, closing a stored-XSS path through the category name/description fields Removes seed-menu.php, a leftover stub with no working code left in it after the AI's one-time seeding run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
21ad6c31cf
commit
f1bbf59548
8 files changed
+1382
-27
No files matched your search
+22
-14
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Database connection & schema management for SQLite
|
||||
*/
|
||||
@@ -55,6 +57,26 @@ function init_db_schema(PDO $pdo): void {
|
||||
show_in_hours INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS menu_categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS menu_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER NOT NULL REFERENCES menu_categories(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
price TEXT NOT NULL,
|
||||
badge TEXT NULL,
|
||||
is_available INTEGER NOT NULL DEFAULT 1,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
");
|
||||
|
||||
// Seed default admin user if table is empty (username: admin / password: password)
|
||||
@@ -65,18 +87,4 @@ function init_db_schema(PDO $pdo): void {
|
||||
$insert = $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, 'admin')");
|
||||
$insert->execute(['admin', $defaultPassHash]);
|
||||
}
|
||||
|
||||
// Seed default Labor Day announcement row if table is empty
|
||||
$stmtAnn = $pdo->query("SELECT COUNT(*) as count FROM announcements");
|
||||
$annCount = $stmtAnn->fetchColumn();
|
||||
if ($annCount == 0) {
|
||||
$insertAnn = $pdo->prepare("
|
||||
INSERT INTO announcements (
|
||||
is_active, type, headline, message, start_date, end_date, show_banner, show_in_hours
|
||||
) VALUES (
|
||||
1, 'holiday', 'Labor Day Closure', 'The Old Fitz will be closed Thursday, Sept 3 through Saturday, Sept 5 for the holiday weekend. Doors reopen Thursday, Sept 10 at 4:00 PM!', NULL, NULL, 1, 1
|
||||
)
|
||||
");
|
||||
$insertAnn->execute();
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Menu helper functions
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
function get_full_menu(bool $availableOnly = false): array {
|
||||
$pdo = get_db();
|
||||
|
||||
$catStmt = $pdo->query("SELECT * FROM menu_categories ORDER BY sort_order ASC, id ASC");
|
||||
$categories = $catStmt->fetchAll() ?: [];
|
||||
|
||||
$itemSql = "SELECT * FROM menu_items";
|
||||
if ($availableOnly) {
|
||||
$itemSql .= " WHERE is_available = 1";
|
||||
}
|
||||
$itemSql .= " ORDER BY sort_order ASC, id ASC";
|
||||
|
||||
$itemStmt = $pdo->query($itemSql);
|
||||
$items = $itemStmt->fetchAll() ?: [];
|
||||
|
||||
// Group items by category_id
|
||||
$grouped = [];
|
||||
foreach ($categories as $cat) {
|
||||
$cat['items'] = [];
|
||||
$grouped[$cat['id']] = $cat;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (isset($grouped[$item['category_id']])) {
|
||||
$grouped[$item['category_id']]['items'][] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($grouped);
|
||||
}
|
||||
|
||||
function get_all_categories(): array {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->query("SELECT * FROM menu_categories ORDER BY sort_order ASC, id ASC");
|
||||
return $stmt->fetchAll() ?: [];
|
||||
}
|
||||
|
||||
function get_category_by_id(int $id): ?array {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("SELECT * FROM menu_categories WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$res = $stmt->fetch();
|
||||
return $res ?: null;
|
||||
}
|
||||
|
||||
function category_exists(int $id): bool {
|
||||
if ($id <= 0) {
|
||||
return false;
|
||||
}
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM menu_categories WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
function save_category(array $data, ?int $id = null): int {
|
||||
$pdo = get_db();
|
||||
$name = trim($data['name'] ?? '');
|
||||
$description = trim($data['description'] ?? '');
|
||||
$sortOrder = (int)($data['sort_order'] ?? 0);
|
||||
|
||||
if ($id && $id > 0) {
|
||||
$stmt = $pdo->prepare("UPDATE menu_categories SET name = ?, description = ?, sort_order = ? WHERE id = ?");
|
||||
$stmt->execute([$name, $description, $sortOrder, $id]);
|
||||
return $id;
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO menu_categories (name, description, sort_order) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$name, $description, $sortOrder]);
|
||||
return (int)$pdo->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
function delete_category(int $id): bool {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("DELETE FROM menu_categories WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
|
||||
function get_menu_item_by_id(int $id): ?array {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("SELECT * FROM menu_items WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$res = $stmt->fetch();
|
||||
return $res ?: null;
|
||||
}
|
||||
|
||||
function save_menu_item(array $data, ?int $id = null): int {
|
||||
$pdo = get_db();
|
||||
$categoryId = (int)($data['category_id'] ?? 0);
|
||||
$name = trim($data['name'] ?? '');
|
||||
$description = trim($data['description'] ?? '');
|
||||
$price = trim($data['price'] ?? '');
|
||||
$badge = trim($data['badge'] ?? '');
|
||||
$isAvailable = !empty($data['is_available']) ? 1 : 0;
|
||||
$sortOrder = (int)($data['sort_order'] ?? 0);
|
||||
|
||||
if ($id && $id > 0) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE menu_items SET
|
||||
category_id = ?, name = ?, description = ?, price = ?,
|
||||
badge = ?, is_available = ?, sort_order = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$categoryId, $name, $description, $price, $badge, $isAvailable, $sortOrder, $id]);
|
||||
return $id;
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO menu_items (category_id, name, description, price, badge, is_available, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$categoryId, $name, $description, $price, $badge, $isAvailable, $sortOrder]);
|
||||
return (int)$pdo->lastInsertId();
|
||||
}
|
||||
}
|
||||
|
||||
function toggle_menu_item_availability(int $id): bool {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("UPDATE menu_items SET is_available = CASE WHEN is_available = 1 THEN 0 ELSE 1 END, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
|
||||
function delete_menu_item(int $id): bool {
|
||||
$pdo = get_db();
|
||||
$stmt = $pdo->prepare("DELETE FROM menu_items WHERE id = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
Reference in new issue
Block a user