Order Item Table:
Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('item_id')->constrained()->onDelete('cascade');
$table->unsignedBigInteger('option_value_id')->nullable();
$table->foreign('option_value_id')->references('id')->on('option_values')->onDelete('set null');
$table->integer('quantity');
$table->text('comments')->nullable();
$table->decimal('total_price', 8, 2);
$table->foreignId('table_layout_id')->references('id')->on('option_values')->onDelete('cascade');
$table->timestamps();
});
Table Layout Table:
Schema::create('table_layouts', function (Blueprint $table) {
$table->ulid('id')->primary();
$table->string('table_number');
$table->integer('capacity');
$table->boolean('is_reserved')->default(false);
$table->timestamps();
});
Order Item Controller:
<?php
namespace AppHttpControllers;
use AppModelsOrderItem;
use IlluminateHttpRequest;
use IlluminateSupportFacadesLog;
use IlluminateSupportFacadesValidator;
class OrderItemController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$orderItems = OrderItem::all();
return response()->json($orderItems);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
try {
$validatedData = Validator::make($request->all(), [
'items' => 'required|array',
'items.*.table_layout_id' => 'required|exists:table_layouts,id',
'items.*.item_id' => 'required|exists:items,id',
'items.*.quantity' => 'required|integer|min:1',
'items.*.price' => ['required', 'regex:/^d+(.d{1,2})?$/'],
'items.*.option_value_id' => 'nullable|array',
'items.*.option_value_id.*' => 'exists:option_values,id',
'items.*.comments' => 'nullable|string',
]);
// Check if validation fails and return errors
if ($validatedData->fails()) {
return response()->json(['errors' => $validatedData->errors()], 422);
}
foreach ($request->input('items') as $item) {
OrderItem::create([
'table_layout_id' => $item['table_layout_id'],
'item_id' => $item['item_id'],
'option_value_id' => $item['option_value_id'] ?? null,
'quantity' => $item['quantity'],
'comments' => $item['comments'],
'total_price' => $item['price'], // Ensure 'price' is validated properly
]);
}
return response()->json(['message' => 'Order items created successfully'], 201);
} catch (Exception $e) {
// Log the exception for further investigation
Log::error('Failed to create order items: ' . $e->getMessage());
return response()->json(['error' => 'Failed to create order items.'], 500);
}
}
/**
* Display the specified resource.
*/
public function show(OrderItem $orderItem)
{
return $orderItem->load('order', 'item', 'option', 'optionValue');
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, OrderItem $orderItem)
{
$validatedData = $request->validate([
'table_layout_id' => 'sometimes|nullable|exists:table_layouts,id',
'item_id' => 'sometimes|nullable|exists:items,id',
'quantity' => 'sometimes|nullable|integer|min:1',
'price' => ['sometimes', 'nullable', 'regex:/^d+(.d{1,2})?$/'],
'option_value_id' => 'nullable|array',
'option_value_id.*' => 'exists:option_values,id',
'comments' => 'nullable|string',
]);
$orderItem->update($validatedData);
return response()->json($orderItem);
}
/**
* Remove the specified resource from storage.
*/
public function destroy(OrderItem $orderItem)
{
$orderItem->delete();
return response()->json(['message' => 'Order item deleted successfully']);
}
}
api.php:
// Routes for managing order items
Route::get('order-items', [OrderItemController::class, 'index']);
Route::post('order-items', [OrderItemController::class, 'store']);
Route::get('order-items/{orderItem}', [OrderItemController::class, 'show']);
Route::put('order-items/{orderItem}', [OrderItemController::class, 'update']);
Route::delete('order-items/{orderItem}', [OrderItemController::class, 'destroy']);
web.php:
use AppHttpControllersUserController;
use AppHttpControllersTableLayoutController;
use AppHttpControllersQRCodeController;
use AppModelsTableLayout;
use IlluminateSupportFacadesRoute;
Route::get('/', function () {
return view('welcome');
});
Route::get('/login', function () {
return redirect(route('filament.admin.auth.login'));
})->name('login');
Route::resource('table-layouts', TableLayoutController::class);
Route::resource('qrcodes', QRCodeController::class);
Route::get('/table/{id}', function ($id) {
$tableLayout = TableLayout::findOrFail($id);
$baseUrl = config('app.frontend_base_url');
return redirect("$baseUrl/items?ulid=$id");
})->name('table.show');
pages/cart/index.vue:
<template>
<div class="cart-container">
<div v-if="cartItems.length === 0" class="empty-cart">
<div class="empty-cart-content">
<LazyEmptyCart />
<h3>Your cart is empty</h3>
<p>Add items from the store to continue your order.</p>
<button class="back-to-store-button" @click="goToStore">
Back to Store
</button>
</div>
</div>
<div v-else>
<h2 class="cart-title">Your Cart</h2>
<div class="cart-items">
<div
v-for="(item, index) in cartItems"
:key="item.id"
class="cart-item"
>
<div class="item-details">
<div class="item-photo-container">
<img
:src="
item.url
? item.url
: 'path/to/default-image.jpg'
"
alt="Item Photo"
class="item-photo"
/>
</div>
<div class="item-info">
<h3 class="item-name">{{ item.name }}</h3>
<p class="item-quantity">
Quantity:
<span class="quantity-control">
<button
class="quantity-button"
@click="decreaseQuantity(index)"
>
-
</button>
<span class="quantity-value">{{
item.quantity
}}</span>
<button
class="quantity-button"
@click="increaseQuantity(index)"
>
+
</button>
</span>
</p>
<p
v-if="item.selectedOptions.length"
class="item-options"
>
<span
v-for="option in item.selectedOptions"
:key="option.name"
>
{{ option.name }}: {{ option.value }} (+{{
item.currencySymbol
}}{{ formatPrice(option.price) }})
</span>
</p>
<p class="item-price">
Price:
{{ getItemCurrencySymbol(item) }}{{
formatPrice(getItemBasePrice(item))
}}
</p>
</div>
</div>
<button
class="remove-item-button"
@click="removeItem(index)"
>
Remove
</button>
</div>
</div>
<div class="comment-section">
<textarea
v-model="newComment"
rows="3"
placeholder="Write your comment here"
id="commentInput"
@input="updateComment"
></textarea>
</div>
<div class="cart-summary">
<p class="total-price">
Total:
{{
cartItems[0]
? cartItems[0].currencySymbol
: ""
}}{{ formatPrice(calculateTotalPrice()) }}
</p>
<button class="place-order-button" @click="placeOrder">Place Order</button>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from "vue";
import { useCartStore } from "@/stores/cart";
import { useRouter } from "vue-router";
const router = useRouter();
const cartStore = useCartStore();
const cartItems = computed(() => cartStore.items);
const newComment = ref("");
const comments = ref([]);
const addComment = () => {
if (newComment.value.trim() !== "") {
comments.value.push(newComment.value.trim());
newComment.value = "";
}
};
const updateComment = () => {
cartStore.setComment(newComment.value);
};
const getItemBasePrice = (item) => {
return parseFloat(item.price.replace(/[^d.-]/g, "")) || 0;
};
const getItemCurrencySymbol = (item) => {
return item.price.replace(/[d.,]/g, "") || "$";
};
const calculateItemPrice = (item) => {
let basePrice = getItemBasePrice(item);
item.selectedOptions.forEach((option) => {
basePrice += parseFloat(option.price) || 0;
});
return {
price: basePrice * item.quantity,
currencySymbol: getItemCurrencySymbol(item),
};
};
const formatPrice = (price) => {
return parseFloat(price).toFixed(2);
};
const increaseQuantity = (index) => {
cartStore.increaseQuantity(index);
};
const decreaseQuantity = (index) => {
cartStore.decreaseQuantity(index);
};
const removeItem = (index) => {
cartStore.removeItem(index);
};
const calculateTotalPrice = () => {
return cartItems.value.reduce(
(total, item) => total + calculateItemPrice(item).price,
0
);
};
const goToStore = () => {
router.push("/items");
};
const placeOrder = async () => {
await cartStore.placeOrder();
};
</script>
stores/cart.js:
import { defineStore } from 'pinia';
import axios from 'axios';
import { createOrderItem } from '@/utils/api-fetch';
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
comment: ''
}),
actions: {
increaseQuantity(index) {
this.items[index].quantity++;
},
decreaseQuantity(index) {
if (this.items[index].quantity > 1) {
this.items[index].quantity--;
}
},
addItem(item) {
this.items.push(item);
},
removeItem(index) {
this.items.splice(index, 1);
},
setComment(comment) {
this.comment = comment;
},
calculateTotalPrice() {
return this.items.reduce((total, item) => {
let basePrice = parseFloat(item.price.replace(/[^d.-]/g, "")) || 0;
item.selectedOptions.forEach((option) => {
basePrice += parseFloat(option.price) || 0;
});
return total + (basePrice * item.quantity);
}, 0);
},
async placeOrder() {
const ulid = localStorage.getItem("Ulid");
// Save items, total price, and comment to local storage
localStorage.setItem('items', JSON.stringify(this.items));
localStorage.setItem('totalPrice', JSON.stringify(this.calculateTotalPrice()));
localStorage.setItem('comment', this.comment);
if (ulid) {
const payload = {
items: this.items.map(item => ({
table_layout_id: ulid,
item_id: item.id,
quantity: item.quantity,
price: parseFloat(item.price.replace(/[^0-9.-]+/g, '')),
option_value_id: item.selectedOptions?.map(opt => opt.value_id),
comments: this.comment
})),
};
console.log('Payload:', payload);
try {
const response = await axios.post('http://restaurant_management_system.test/api/v1/order-items', payload);
console.log('Order placed successfully:', response.data);
// Handle the response as needed
} catch (error) {
if (error.response && error.response.data && error.response.data.errors) {
console.error('Validation errors:', error.response.data.errors);
// Display validation errors to the user or handle them as needed
} else {
console.error('Error placing order:', error);
console.error('Error details:', error.response ? error.response.data : error);
}
}
} else {
console.error('Ulid not found in local storage');
}
}
},
});
utils/api-fetch.ts:
import type { NitroFetchOptions } from "nitropack";
// Helper function for API fetch
export function apiFetch<T>(path: string, options?: NitroFetchOptions<string>) {
const cookie = useRequestHeader("cookie");
const baseURL = useRuntimeConfig().public.apiBase;
const headers: Record<string, string> = {};
if (cookie) {
headers["cookie"] = cookie;
}
return $fetch<T>(path, { ...options, headers, baseURL });
}
// Order Items
export function createOrderItem(body: any) {
return apiFetch("/order-items", { method: "POST", body });
}
export function updateOrderItem(id: string, body: any) {
return apiFetch(`/order-items/${id}`, { method: "PUT", body });
}
export function deleteOrderItem(id: string) {
return apiFetch(`/order-items/${id}`, { method: "DELETE" });
}
from the:
async placeOrder() {
const ulid = localStorage.getItem("Ulid");
// Save items, total price, and comment to local storage
localStorage.setItem('items', JSON.stringify(this.items));
localStorage.setItem('totalPrice', JSON.stringify(this.calculateTotalPrice()));
localStorage.setItem('comment', this.comment);
if (ulid) {
const payload = {
items: this.items.map(item => ({
table_layout_id: ulid,
item_id: item.id,
quantity: item.quantity,
price: parseFloat(item.price),
option_id: item.selectedOptions?.map(opt => opt.id),
option_value_id: item.selectedOptions?.map(opt => opt.value_id),
comments: this.comment
})),
totalPrice: this.calculateTotalPrice(),
comment: this.comment
};
console.log('Payload:', payload);
try {
const response = await axios.post('http://restaurant_management_system.test/api/v1/order-items', payload);
console.log('Order placed successfully:', response.data);
// Handle the response as needed
} catch (error) {
if (error.response && error.response.data && error.response.data.errors) {
console.error('Validation errors:', error.response.data.errors);
// Display validation errors to the user or handle them as needed
} else {
console.error('Error placing order:', error);
console.error('Error details:', error.response ? error.response.data : error);
}
}
} else {
console.error('Ulid not found in local storage');
}
}
……here are how the data needs to be passed (posted) onto the table
'item_id' = item_id: item.id
'option_value_id' = option_value_id: item.selectedOptions?.map(opt => opt.value_id)
'quantity' = quantity: item.quantity,
'comments' = comments: this.comment
'total_price' = totalPrice: this.calculateTotalPrice(),
'table_layout_id' = Ulid
Hope you understand this part,
I am successfully able to log the items to localstorage when i press the place order, but i fail to post the data to server side…..
Here is the console error i get (inlcuding the payload)….
Payload: {items: Array(1)}items: Array(1)0: comments: "demo comment"item_id: 1option_value_id: []length: 0[[Prototype]]: Array(0)price: 10quantity: 1table_layout_id: "01J0DPVVMZ4TB580AQ47FFBCAZ"[[Prototype]]: Objectlength: 1[[Prototype]]: Array(0)[[Prototype]]: Objectconstructor: ƒ Object()hasOwnProperty: ƒ hasOwnProperty()isPrototypeOf: ƒ isPrototypeOf()propertyIsEnumerable: ƒ propertyIsEnumerable()toLocaleString: ƒ toLocaleString()toString: ƒ toString()valueOf: ƒ valueOf()__defineGetter__: ƒ __defineGetter__()__defineSetter__: ƒ __defineSetter__()__lookupGetter__: ƒ __lookupGetter__()__lookupSetter__: ƒ __lookupSetter__()__proto__: (...)get __proto__: ƒ __proto__()set __proto__: ƒ __proto__()
cart.js:60
POST http://restaurant_management_system.test/api/v1/order-items 500 (Internal Server Error)
dispatchXhrRequest @ xhr.js:188
xhr @ xhr.js:15
dispatchRequest @ dispatchRequest.js:51
_request @ Axios.js:173
request @ Axios.js:40
httpMethod @ Axios.js:212
wrap @ bind.js:5
placeOrder @ cart.js:60
(anonymous) @ pinia.mjs:1380
store.<computed> @ pinia.mjs:935
placeOrder @ index.vue:178
callWithErrorHandling @ runtime-core.esm-bundler.js:195
callWithAsyncErrorHandling @ runtime-core.esm-bundler.js:202
invoker @ runtime-dom.esm-bundler.js:696
Show 12 more frames
Show less
cart.js:68 Error placing order: AxiosError {message: 'Request failed with status code 500', name: 'AxiosError', code: 'ERR_BAD_RESPONSE', config: {…}, request: XMLHttpRequest, …}
placeOrder @ cart.js:68
await in placeOrder (async)
(anonymous) @ pinia.mjs:1380
store.<computed> @ pinia.mjs:935
placeOrder @ index.vue:178
callWithErrorHandling @ runtime-core.esm-bundler.js:195
callWithAsyncErrorHandling @ runtime-core.esm-bundler.js:202
invoker @ runtime-dom.esm-bundler.js:696
Show 5 more frames
Show less
cart.js:69 Error details: {error: 'Failed to create order items.'}
placeOrder @ cart.js:69
await in placeOrder (async)
(anonymous) @ pinia.mjs:1380
store.<computed> @ pinia.mjs:935
placeOrder @ index.vue:178
callWithErrorHandling @ runtime-core.esm-bundler.js:195
callWithAsyncErrorHandling @ runtime-core.esm-bundler.js:202
invoker @ runtime-dom.esm-bundler.js:696
Show 5 more frames
Show less
The above code is what i tried…. i want the code to output the data into the fields of my order_item table successfully… like this:
'item_id' = item_id: item.id
'option_value_id' = option_value_id: item.selectedOptions?.map(opt => opt.value_id)
'quantity' = quantity: item.quantity,
'comments' = comments: this.comment
'total_price' = totalPrice: this.calculateTotalPrice(),
'table_layout_id' = Ulid
this is just what i assume the values are stored in