menuCategories(true); $specials = $this->db->fetchAll( "SELECT s.*, mi.name AS item_name FROM specials s LEFT JOIN menu_items mi ON mi.id = s.menu_item_id WHERE s.active = 1 AND (s.starts_at IS NULL OR s.starts_at <= CURRENT_TIMESTAMP) AND (s.ends_at IS NULL OR s.ends_at >= CURRENT_TIMESTAMP) ORDER BY s.display_order, s.id" ); $featured = $this->db->fetchAll( 'SELECT mi.*, mc.name AS category_name FROM menu_items mi JOIN menu_categories mc ON mc.id = mi.category_id WHERE mi.active = 1 AND mc.active = 1 AND mi.featured = 1 ORDER BY mi.display_order, mi.id LIMIT 6' ); View::render('store/home', $this->shared([ 'title' => 'Keyser comfort food, ready when you are', 'categories' => $categories, 'specials' => $specials, 'featured' => $featured, ])); } public function menu(): void { $query = Security::clean((string) ($_GET['q'] ?? '')); $categories = $this->menuCategories(false, $query); View::render('store/menu', $this->shared([ 'title' => 'Order Online', 'categories' => $categories, 'query' => $query, ])); } public function addToCart(): void { Security::verifyCsrf(); $user = $this->auth->user(); $added = $this->cart->add( (int) ($_POST['menu_item_id'] ?? 0), (int) ($_POST['quantity'] ?? 1), Security::clean((string) ($_POST['notes'] ?? '')), $user ? (int) $user['id'] : null ); Http::flash($added ? 'success' : 'error', $added ? 'Added to your order.' : 'That item is not currently available.'); $returnTo = (string) ($_POST['return_to'] ?? '/menu'); Http::redirect(str_starts_with($returnTo, '/') ? $returnTo : '/menu'); } public function cart(): void { $user = $this->auth->user(); $details = $this->cart->details($user ? (int) $user['id'] : null); View::render('store/cart', $this->shared([ 'title' => 'Your Order', 'cart' => $details, 'totals' => $this->orders->totals($details), ])); } public function updateCart(): void { Security::verifyCsrf(); $user = $this->auth->user(); $quantities = isset($_POST['quantity']) && is_array($_POST['quantity']) ? $_POST['quantity'] : []; $this->cart->update($quantities, $user ? (int) $user['id'] : null); Http::flash('success', 'Your order has been updated.'); Http::redirect('/cart'); } public function removeFromCart(): void { Security::verifyCsrf(); $user = $this->auth->user(); $this->cart->remove((int) ($_POST['cart_item_id'] ?? 0), $user ? (int) $user['id'] : null); Http::flash('success', 'Item removed.'); Http::redirect('/cart'); } public function checkout(): void { $user = $this->auth->user(); $details = $this->cart->details($user ? (int) $user['id'] : null); if (empty($details['items'])) { Http::flash('warning', 'Add something delicious before checking out.'); Http::redirect('/menu'); } View::render('store/checkout', $this->shared([ 'title' => 'Checkout', 'cart' => $details, 'totals' => $this->orders->totals($details), 'taxRates' => $this->db->fetchAll('SELECT * FROM tax_rates WHERE active = 1 ORDER BY id'), 'squareEnabled' => (bool) $this->config->get('square.enabled', false), 'squareAppId' => (string) $this->config->get('square.application_id', ''), 'squareLocationId' => (string) $this->config->get('square.location_id', ''), 'squareEnvironment' => (string) $this->config->get('square.environment', 'sandbox'), ])); } public function placeOrder(): void { Security::verifyCsrf(); $user = $this->auth->user(); $cart = $this->cart->details($user ? (int) $user['id'] : null); $input = [ 'customer_name' => Security::clean((string) ($_POST['customer_name'] ?? '')), 'customer_email' => strtolower(Security::clean((string) ($_POST['customer_email'] ?? ''))), 'customer_phone' => Security::clean((string) ($_POST['customer_phone'] ?? '')), 'pickup_time' => Security::clean((string) ($_POST['pickup_time'] ?? '')), 'special_instructions' => Security::clean((string) ($_POST['special_instructions'] ?? '')), ]; if ( $input['customer_name'] === '' || !filter_var($input['customer_email'], FILTER_VALIDATE_EMAIL) || $input['customer_phone'] === '' ) { Http::old($input); Http::flash('error', 'Name, a valid email address, and phone number are required.'); Http::redirect('/checkout'); } if (!(bool) $this->config->get('ordering.accepting_orders', true)) { Http::flash('error', 'Online ordering is currently paused.'); Http::redirect('/cart'); } try { $order = $this->orders->place( $cart, $input, $user ? (int) $user['id'] : null, (string) ($_POST['square_token'] ?? '') ); $this->mailer->send( $input['customer_email'], 'We received order ' . $order['order_number'], '
Thanks, ' . htmlspecialchars($input['customer_name'], ENT_QUOTES, 'UTF-8') . '.
' . 'Your order ' . htmlspecialchars((string) $order['order_number'], ENT_QUOTES, 'UTF-8') . ' has been paid and sent to the kitchen.
' ); setcookie('fatbottom_cart', '', ['expires' => time() - 3600, 'path' => '/']); $_SESSION['last_order_id'] = (int) $order['id']; Http::redirect('/order/complete'); } catch (\Throwable $error) { Http::old($input); Http::flash('error', $error->getMessage()); Http::redirect('/checkout'); } } public function orderComplete(): void { $orderId = (int) ($_SESSION['last_order_id'] ?? 0); $order = $this->db->fetch('SELECT * FROM orders WHERE id = ?', [$orderId]); if ($order === null) { Http::redirect('/'); } $items = $this->db->fetchAll('SELECT * FROM order_items WHERE order_id = ?', [$orderId]); View::render('store/order-complete', $this->shared([ 'title' => 'Order Received', 'order' => $order, 'items' => $items, ])); } private function menuCategories(bool $homepage, string $query = ''): array { $categories = $this->db->fetchAll( 'SELECT * FROM menu_categories WHERE active = 1 ORDER BY display_order, name' ); foreach ($categories as &$category) { $parameters = [(int) $category['id']]; $where = 'category_id = ? AND active = 1'; if ($query !== '') { $where .= ' AND (name LIKE ? OR description LIKE ?)'; $parameters[] = '%' . $query . '%'; $parameters[] = '%' . $query . '%'; } $limit = $homepage ? ' LIMIT 4' : ''; $category['items'] = $this->db->fetchAll( "SELECT * FROM menu_items WHERE {$where} ORDER BY display_order, name{$limit}", $parameters ); } unset($category); return array_values(array_filter( $categories, static fn (array $category): bool => $category['items'] !== [] )); } }